Skip to content

fix(engine): reject non-finite agent-sdk usage so it can't crash the iterate loop (#5827) - #6078

Merged
JSONbored merged 1 commit into
JSONbored:mainfrom
shin-core:fix/agent-sdk-usage-non-finite-guard
Jul 15, 2026
Merged

fix(engine): reject non-finite agent-sdk usage so it can't crash the iterate loop (#5827)#6078
JSONbored merged 1 commit into
JSONbored:mainfrom
shin-core:fix/agent-sdk-usage-non-finite-guard

Conversation

@shin-core

Copy link
Copy Markdown
Contributor

Summary

Closes #5827.

packages/loopover-engine/src/miner/agent-sdk-driver.ts extracted the driver's usage fields from the SDK
result message with a bare typeof === "number" check:

const turnsUsed = typeof resultMessage?.num_turns === "number" ? resultMessage.num_turns : undefined;
const costUsd  = typeof resultMessage?.total_cost_usd === "number" ? resultMessage.total_cost_usd : undefined;

That does not reject NaN, Infinity, or a negative number the way cli-subprocess-driver.ts's
finiteNonNegativeNumber already does for the analogous untrusted CLI-stdout fields. A malformed Agent SDK
result message (num_turns: -1, NaN, …) therefore reached accumulateAttemptUsage
(attempt-metering.ts) unguarded. That primitive deliberately throws a RangeError on negative/non-finite
input — and the call site in runIterateLoopCore (iterate-loop.ts) sits outside the loop's driver/
self-review try/catch blocks, so the throw rejected the whole runIterateLoopCore promise before any decision
was logged
, violating the loop's documented "every iteration's decision is recorded before this function returns
control" guarantee (worse than a normal governed rejection — the attempt's outcome is never logged at all).
tokensFromResultMessage had the same typeof-only gap.

Fix (root cause + defense in depth):

  • agent-sdk-driver.ts: add a local finiteNonNegativeNumber helper mirroring cli-subprocess-driver.ts's of
    the same name, and use it for turnsUsed, costUsd, and the tokensFromResultMessage fields. An invalid value
    degrades to undefined — the driver's existing "field absent" contract — instead of propagating downstream.
  • iterate-loop.ts: clamp the values feeding accumulateAttemptUsage with a local finiteNonNegativeUsage
    helper (invalid → 0), so no current or future driver can crash the loop instead of being governed.
  • attempt-metering.ts's throw-on-invalid contract is unchanged (its own direct callers/tests still rely on it) —
    this fix is about never handing it invalid input, not weakening the primitive.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format, for example fix(api): restore profile access checks.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • I linked a currently open issue this PR resolves (e.g. Closes #123) — a linked open issue is required for every contributor PR.

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally; codecov/patch requires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries

If any required check was skipped, explain why:

  • Ran the full npm run test:ci gate plus npm audit --audit-level=moderate (0 vulnerabilities). The new helper branches (valid vs. out-of-contract) are covered on both arms. Note packages/loopover-engine/src/** is measured by Codecov through the vitest suite (not the engine's own node --test suite), so the driver + loop guards are tested from both surfaces: engine node --test (test/agent-sdk-driver.test.ts, test/iterate-loop.test.ts) and vitest (test/unit/agent-sdk-driver.test.ts, new test/unit/iterate-loop-usage-guard.test.ts). The regression tests were confirmed to FAIL against the pre-fix source.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests.
  • API/OpenAPI/MCP behavior is updated and tested where needed.
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks.
  • Visible UI changes include a UI Evidence section below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.
  • Public docs/changelogs are updated where needed; changelogs are only edited for release-prep PRs.

Notes on the Safety boxes: this is a backend-only hardening of a miner-runtime driver + orchestrator — no UI, API/OpenAPI, or auth/CORS/session surface. The applicable negative-path testing is the drivers' own out-of-contract-input paths, which the new tests cover (negative/NaN/Infinity values degrade safely; the loop still records its decision). The fix strengthens a fail-closed guarantee rather than relaxing it.

UI Evidence

Not applicable — backend-only change to the miner engine; no visible UI, frontend, docs, or extension surface.

Notes

  • Regression tests: packages/loopover-engine/test/agent-sdk-driver.test.ts and its vitest mirror test/unit/agent-sdk-driver.test.ts cover num_turns/total_cost_usd = -1/NaN/Infinity → undefined, and a negative/NaN/Infinity token field being ignored rather than poisoning the sum. packages/loopover-engine/test/iterate-loop.test.ts and its vitest mirror test/unit/iterate-loop-usage-guard.test.ts cover the loop still completing (decision logged, poisoned axis clamped to 0, valid axis preserved) rather than rejecting uncaught. All new tests were verified to fail on the pre-fix source.

…iterate loop (JSONbored#5827)

agent-sdk-driver.ts extracted num_turns/total_cost_usd (and the usage token
fields) with a bare `typeof === "number"` check, so a malformed Agent SDK result
message (num_turns: -1, NaN, Infinity) passed an out-of-contract value straight
through. That value fed accumulateAttemptUsage in iterate-loop.ts's
runIterateLoopCore, which deliberately throws a RangeError on negative/non-finite
input — and that call sits outside the loop's driver/self-review try/catch, so
the throw rejected runIterateLoopCore before any decision was logged, violating
the loop's "every iteration's decision is recorded before returning" guarantee.

Harden the driver's usage extraction with a finiteNonNegativeNumber helper
mirroring cli-subprocess-driver.ts's existing one — an invalid value degrades to
undefined (the driver's "field absent" contract). Add a matching call-site clamp
in runIterateLoopCore so no current or future driver can crash the loop instead
of being governed. attempt-metering.ts's throw-on-invalid contract is unchanged;
this only stops feeding it invalid input.

Regression tests cover negative/NaN/Infinity num_turns, total_cost_usd, and token
fields, and the loop completing (decision logged, poisoned axis clamped) rather
than rejecting uncaught. Because Codecov measures the engine src via the vitest
suite (not the engine's node --test suite), the driver + loop guards are covered
from both test surfaces.
@shin-core
shin-core requested a review from JSONbored as a code owner July 15, 2026 08:32
@superagent-security

Copy link
Copy Markdown
Contributor

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@superagent-security superagent-security Bot added the contributor:flagged Contributor flagged for review by trust analysis. label Jul 15, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

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

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.23%. Comparing base (3358efd) to head (5e580d8).
⚠️ Report is 10 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #6078       +/-   ##
===========================================
+ Coverage   78.14%   95.23%   +17.09%     
===========================================
  Files         595      595               
  Lines       46998    47010       +12     
  Branches    15015    15010        -5     
===========================================
+ Hits        36726    44772     +8046     
+ Misses       8132     1493     -6639     
+ Partials     2140      745     -1395     
Flag Coverage Δ
shard-1 43.99% <0.00%> (-0.04%) ⬇️
shard-2 36.60% <100.00%> (+0.11%) ⬆️
shard-3 32.05% <0.00%> (+0.10%) ⬆️
shard-4 33.12% <83.33%> (?)
shard-5 32.22% <83.33%> (+0.72%) ⬆️
shard-6 44.50% <16.66%> (?)

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

Files with missing lines Coverage Δ
...ages/loopover-engine/src/miner/agent-sdk-driver.ts 100.00% <100.00%> (ø)
packages/loopover-engine/src/miner/iterate-loop.ts 83.33% <100.00%> (+3.98%) ⬆️

... and 207 files with indirect coverage changes

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

loopover-orb Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Warning

🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-15 08:53:14 UTC

6 files · 1 AI reviewer · no blockers · readiness 98/100 · CI green · unstable

⏸️ Suggested Action - Manual Review

Review summary
This PR adds a `finiteNonNegativeNumber` guard to `agent-sdk-driver.ts` (mirroring the existing `cli-subprocess-driver.ts` helper) and a defense-in-depth `finiteNonNegativeUsage` clamp at the `accumulateAttemptUsage` call site in `iterate-loop.ts`, closing off a real path where a malformed Agent SDK result (`num_turns: -1`, `NaN`, etc.) would reach `accumulateAttemptUsage` unguarded and throw a `RangeError` outside the loop's try/catch, rejecting `runIterateLoopCore` before any decision was logged. The fix is correctly layered — reject at the source in the driver, and clamp again defensively at the loop's metering call site so any future driver can't reintroduce the same crash — and is backed by targeted unit tests in both the vitest and node:test suites exercising negative/NaN/Infinity for turns, cost, and token-usage fields. The change is narrowly scoped to the described bug and issue #5827, with no unrelated modifications.

Nits — 4 non-blocking
  • The new `finiteNonNegativeUsage` in iterate-loop.ts (iterate-loop.ts:~282) duplicates `finiteNonNegativeNumber` in agent-sdk-driver.ts almost verbatim except for the undefined-vs-0 return; consider extracting one shared primitive that both call sites parameterize the fallback for, since a third driver will otherwise reimplement it a third time.
  • The doc comment above `finiteNonNegativeUsage` (iterate-loop.ts) is quite long for a private helper — could be trimmed to the non-obvious part (why clamping and not throwing) since the RangeError contract is already documented at `attempt-metering.ts`.
  • Consider hoisting `finiteNonNegativeNumber`/`finiteNonNegativeUsage` into a small shared util imported by both `agent-sdk-driver.ts`, `cli-subprocess-driver.ts`, and `iterate-loop.ts` to avoid a third copy diverging in the future.
  • The test suite covers -1/NaN/Infinity but not `-Infinity`; a quick addition would round out the finite-check coverage matrix, though it's low-value given `Number.isFinite` already handles it symmetrically.
Flagged checks (non-blocking)
  • Contributor trust — Contributor flagged for review
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #5827
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: 57 registered-repo PR(s), 26 merged, 0 issue(s).
Contributor context ✅ Confirmed Gittensor contributor shin-core; Gittensor profile; 57 PR(s), 0 issue(s).
Gate result ✅ Passing No configured blocker found.
Improvement ✅ Minor risk: clean · value: minor
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: 57 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 &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; 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://gittensory.aethereal.dev/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

@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.

@JSONbored JSONbored added contributor:verified Contributor passed trust analysis. and removed contributor:flagged Contributor flagged for review by trust analysis. labels Jul 15, 2026
@JSONbored
JSONbored merged commit 0b73c32 into JSONbored:main Jul 15, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(engine): agent-sdk-driver usage fields can crash iterate-loop's budget accumulator

2 participants