Skip to content

fix(orb): validate the sender-supplied ingest timestamps before they reach orb_signals - #10156

Merged
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
shin-core:fix/orb-ingest-timestamp-validation-10028
Jul 31, 2026
Merged

fix(orb): validate the sender-supplied ingest timestamps before they reach orb_signals#10156
loopover-orb[bot] merged 1 commit into
JSONbored:mainfrom
shin-core:fix/orb-ingest-timestamp-validation-10028

Conversation

@shin-core

Copy link
Copy Markdown
Contributor

What & why

handleOrbIngest is the open, unauthenticated fleet-calibration collector, and its header states the posture: every field of an incoming event is whitelist- or range-validated before storage. Two fields were the exception:

typeof event.decision_timestamp === "string" ? event.decision_timestamp : null,
typeof event.outcome_timestamp === "string" ? event.outcome_timestamp : null,
typeof event.outcome_timestamp === "string" ? event.outcome_timestamp : null,  // also written to sent_at

A bare typeof === "string" — no length cap, no format check. Any string up to 1 MiB × 500 events per request is persisted three times per row. And decision_timestamp is not inert: it is the day bucket for the public fleet-accuracy trend and the retention rollup key (substr(COALESCE(decision_timestamp, received_at), 1, 10)). So a malformed value reachable today from a self-host build that emits one:

  1. Passes the window filter regardless of ageWHERE COALESCE(...) >= ?1 is a lexicographic string compare, and "unknown" sorts above every ISO date.
  2. Is then silently dropped in JSsubstr(...,1,10) is unparseable → !Number.isFinite(dayMs)continue. The signal is excluded from the published accuracy series with no warning.
  3. Accumulates permanent junk — the retention fold writes that prefix as a day in orb_signal_rollups' composite primary key, a row no window query reads again and no prune removes.

The fix

Add MAX_TIMESTAMP_CHARS = 64 (alongside the existing MAX_HASH_CHARS / MAX_VERDICT_CHARS block) and a pure normalizeIngestTimestamp that returns the value only when it is a length-capped, Date.parse-able instant, otherwise null — routing all three binds through it. This mirrors the REUSE_DAY_PATTERN + clampReuseCount pair already in this file, one field family over.

A malformed timestamp is stored NULL rather than dropping the event, so COALESCE(decision_timestamp, received_at) falls back to the server-side received_at and the signal still counts toward the trend — the behaviour difference that matters (today the signal is silently lost). Validation only: no migration, no schema change, and every other field's whitelist/clamp behaviour, the per-event skip rules, and the accepted count are unchanged.

Tests (test/integration/orb-ingest.test.ts)

  • normalizeIngestTimestamp("2026-07-30T12:00:00.000Z") → unchanged; "unknown", a 65-char string, 12345, undefinednull (all three branches, both arms).
  • A malformed decision_timestamp: "unknown" event is still accepted ({ accepted: 1 }) with the stored column NULL; an over-length (65+) timestamp likewise stored NULL and accepted.
  • A well-formed pair is stored verbatim in all three columns (decision_timestamp, outcome_timestamp, sent_at) — the currently-correct behaviour pinned.
  • REGRESSION: after a malformed-timestamp ingest, COALESCE(decision_timestamp, received_at) on the stored row parses to a finite instant — the signal survives into the public trend.
  • All new assertions fail on main.

Validation

  • Diff coverage on src/orb/ingest.ts is 100% line and branch.
  • npm run typecheck clean for these files; npm run engine-parity:drift-check passes (host-only file, not a twin); npm run dead-exports:check clean; the orb-ingest suite (55 tests) green.
  • git diff --check clean; no migration / schema / generated-artifact change.

Closes #10028

…reach orb_signals

handleOrbIngest is the open, unauthenticated fleet-calibration collector, and every
field is whitelist- or range-validated before storage — except decision_timestamp
and outcome_timestamp, which got a bare typeof === 'string' check: no length cap, no
format check (outcome_timestamp is also written verbatim into sent_at). decision_timestamp
is the day bucket for the public fleet-accuracy trend and the retention rollup key, so a
non-instant string (e.g. 'unknown') sorts above every ISO date in the lexicographic window
bound, is then dropped in JS as unparseable — silently excluding the signal from the
published trend — and lands a permanent junk day in orb_signal_rollups' composite PK.

Add MAX_TIMESTAMP_CHARS (64) and a pure normalizeIngestTimestamp that returns the value
only when it is a length-capped, Date.parse-able instant, routing all three binds through
it — mirroring the REUSE_DAY_PATTERN + clampReuseCount pair one field family over. A
malformed timestamp is stored NULL rather than dropping the event, so COALESCE(decision_timestamp,
received_at) falls back to the server clock and the signal still counts. Validation only:
no migration, no schema change, and every other field's behaviour is unchanged.

Closes JSONbored#10028
@shin-core
shin-core requested a review from JSONbored as a code owner July 31, 2026 09:44
@loopover-orb

loopover-orb Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-31 10:11:13 UTC

2 files · 1 AI reviewer · no blockers · readiness 98/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR adds a bounded, Date.parse-validated normalizeIngestTimestamp() and routes decision_timestamp/outcome_timestamp/sent_at through it before they hit orb_signals, replacing a bare typeof-string check. The fix is well-targeted at the root cause (the timestamp is now validated at the same ingest layer as every other field, per the file's own stated posture), it correctly falls back to NULL rather than dropping the whole event, and the tests exercise the real INSERT path via handleOrbIngest rather than fabricating state. It closes issue #10028, which is explicitly referenced in the PR title/description and the linked-issue metadata.

Nits — 4 non-blocking
  • src/orb/ingest.ts: Date.parse is lenient and accepts non-ISO formats (e.g. 'July 30, 2026', 'Jul 30 2026 GMT'), so a well-formed-looking but sloppy sender value could still land in decision_timestamp in a shape that doesn't match the substr(...,1,10) day-bucket assumption discussed in the PR description — consider anchoring to an ISO-8601 regex in addition to Date.parse if strict day-bucket correctness matters.
  • test/integration/orb-ingest.test.ts: the existing 'stores decision_timestamp + outcome_timestamp' test at line ~145 predates this PR and wasn't updated to also cover a boundary case at exactly MAX_TIMESTAMP_CHARS (64 chars), only the 65+ over-length case is covered.
  • Consider adding a unit test for a Date.parse-lenient but non-ISO string (e.g. 'July 30, 2026') to document whether that's intentionally accepted, since the PR's own motivation centers on the day-bucket substr(...,1,10) assumption which a non-ISO instant could still violate.
  • src/orb/ingest.ts:127 — the doc comment is long (6 lines); consider trimming to the non-obvious parts (why null vs drop, the mirrored REUSE_DAY_PATTERN convention) since the length-cap/Date.parse mechanics are already clear from the 2-line body.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #10028
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 78 registered-repo PR(s), 60 merged, 0 issue(s).
Contributor context ✅ Confirmed Gittensor contributor shin-core; Gittensor profile; 78 PR(s), 0 issue(s).
Improvement ✅ Minor risk: clean · value: minor · LLM: moderate
Linked issue satisfaction

Addressed
The diff adds MAX_TIMESTAMP_CHARS=64 alongside the other max-char constants and a pure normalizeIngestTimestamp that rejects non-strings, over-length values, and non-parseable dates while returning valid values unchanged, exactly mirroring the REUSE_DAY_PATTERN/clampReuseCount precedent, and routes all three binds (decision_timestamp, outcome_timestamp, sent_at) through it without rejecting the ev

Review context
  • Author: shin-core
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 78 PR(s), 0 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Keep the PR focused and include validation evidence before maintainer review.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.16%. Comparing base (4ce09ed) to head (a020b4c).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main   #10156       +/-   ##
===========================================
- Coverage   92.05%   80.16%   -11.90%     
===========================================
  Files         931      283      -648     
  Lines      114046    58843    -55203     
  Branches    27543     6999    -20544     
===========================================
- Hits       104988    47171    -57817     
- Misses       7759    11381     +3622     
+ Partials     1299      291     -1008     
Flag Coverage Δ
backend 100.00% <100.00%> (+4.31%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/orb/ingest.ts 100.00% <100.00%> (ø)

... and 781 files with indirect coverage changes

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 1a2b41a into JSONbored:main Jul 31, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

orb(ingest): validate the sender-supplied decision_timestamp/outcome_timestamp before they reach orb_signals

1 participant