Skip to content

Public hygiene: steady spend-refusal test, strictjson range, RFC 3339 range note - #243

Merged
matt-dean-git merged 3 commits into
mainfrom
fix/public-hygiene-1016
Oct 16, 2026
Merged

matt-dean-git merged 3 commits into
mainfrom
fix/public-hygiene-1016

Conversation

@matt-dean-git

Copy link
Copy Markdown
Member

Head SHA: d2b8076 (base main 348ed79)

Three commits, one per task.

A. TestSpendRefusalDoesNotEchoTheAmount flaked (c76cfac)

Cause: the check was strings.Contains(json(receipt input), "777"). token_id is the first 12 hex chars of a SHA-256 over a freshly minted token, so it is random on every run. CI run 38937620498 attempt 1 failed at spend_gate_test.go:214 with "token_id":"7775a9f2092b"; nothing in that receipt carries the amount. On main, -count=4000 failed 4 times (token ids 057777fcfd12, 3ac2e777c899, bb9f64a83777, ced40e777ff3).

Fix: amountEchoes decodes the refusal and the receipt input (UseNumber) and checks every key, string and number on its own. A value echoes the amount if it:

  • contains the literal 777.31 (a decimal point cannot occur in a hex id),
  • has a word that is a plain decimal equal to 777.31, to its whole part 777, or to its counter units 77731000000, or
  • is a JSON number equal to one of those (7.7731e2 counts).

Words that mix letters and digits (hex ids, rcpt_1) are identifiers and are not compared. New TestAmountEchoesIsExact uses the CI receipt as it was logged (it passes) and plants 7 echoes and a key echo. Each one is flagged at its own path.

Proof:

  • New check, -count=10000: 0 failures. -race -count=2000: 0 failures.
  • A temporary loop ran the refusal until token_id held 777 (777bd4a15e8e, 00777057483e, 9d360cb77733). On each one the old check would fail and the new check found nothing.
  • Defects planted in spend_gate.go fail by assertion:
    • receipt approval_id = amount → at [$.approval_id]
    • receipt control_symbol = units → at [$.control_symbol]
    • message + "(you asked for $777.31)" → at [$.error.message]
    • error data asked: 777 → at [$.error.data.asked]

B. strictjson accepted numbers above math.MaxFloat64 (398e5ba)

Cause: pkg/toolcontrols/strictjson.go number() bounded only the decimal exponent of the leading digit (at most 308). So 9e308, 1.8e308 and -9e308 passed, which review comment 6095724731 on #227 pointed out. number() is the reader's only number parser. It serves CheckCallLossless and CallHash, which are called from the mcpserver approval gate (approval_gate.go:91 and :135).

Fix: when the leading digit is at 10^308, the magnitude is compared exactly (big.Rat) with math.MaxFloat64. A larger number is refused with the same ErrCallUnreadable "number is outside the exact range" as 1e999. The doc comments now give the right maximum (they said 1e308 was the largest finite binary64). The bound is exact: 1.7976931348623158e308 is refused, even though ParseFloat rounds it down to MaxFloat64, and 1.7976931348623157e308 is accepted.

Proof: TestNumbersAboveMaxFloat64AreRefused checks the error type and exact message from both entry points.

  • Refused: ±9e308, 1e309, 1.8e308, ±1.7976931348623158e308, 9E+308, 18e307, 0.9e309, nested values (positive and negative), a value in _meta, and arguments itself.
  • Accepted: the max in 3 spellings, ±1e308, 1.79e308 and 0e999.

Run against main's strictjson.go, the test fails for every refused case except 1e309, which main already refused.

Note: at the MCP approval gate, main already refused 9e308 before CheckCallLossless runs. argrules.CheckCallShape walks the call with encoding/json Token(), which overflows on that number. So this fix closes the gap for direct callers of CheckCallLossless and CallHash. I did not add a gate-level test because it passes on main.

C. parse_rfc3339 range note (d2b8076)

The docstring now says years 0001 to 9999, as ruled on #226 and #485. It also says year 0000 is malformed, and so is any value whose UTC instant leaves that range once its offset is applied. Both copies (tools/ and demo/) carry the same docstring, which test_parse_rfc3339_identical_across_copies requires. Tests at both ends of the range already existed, so I labelled them and added none. No behaviour change.

The sha256 of demo/verify_evidence_pack.py in demo/SOURCE-MANIFEST.json is refreshed, the same way 4744f27 did it.

Local gates

  • gofmt -l on changed Go files: exit 0, no output
  • go vet ./...: exit 0
  • go test -race -count=1 ./pkg/toolcontrols/... ./pkg/mcpserver/...: exit 0
  • python -B -m unittest discover -s tools -p 'test_*.py': exit 0 (70 tests)
  • python -B -m unittest discover -s demo/tests -p 'test_*.py': exit 0 (8 tests)

Python runs used 3.11.15 with rfc8785 0.1.4 and cryptography 50.0.0, the versions ci.yml installs.

Matt Dean added 3 commits October 16, 2026 14:53
TestSpendRefusalDoesNotEchoTheAmount looked for "777" as a substring of the
serialized refusal and receipt input. The token id is the first 12 hex chars
of a SHA-256 over a freshly minted token, so about 1 run in 400 the id held
777 (7775a9f2092b in CI run 38937620498) and the test failed with nothing
echoed.

amountEchoes decodes the document and checks every key, string and number on
its own: a value echoes the amount if it contains the literal 777.31, if one
of its plain-number words equals the amount, its whole part or its counter
units, or if it is a JSON number equal to one of those. Identifiers that mix
letters and digits are not compared. TestAmountEchoesIsExact pins the CI
receipt (passes) and seven planted echoes plus a key echo (each flagged).
The strict reader bounded only the decimal exponent of a number's leading
digit (at most 308), so 9e308, 1.8e308 and -9e308 were accepted although
they are above math.MaxFloat64 and encoding/json refuses them as overflow
(review comment 6095724731 on #227). In the top decade the magnitude is now
compared exactly with math.MaxFloat64 and a larger one is refused with the
same ErrCallUnreadable "number is outside the exact range" as 1e999.
1.7976931348623157e308 and everything below stay allowed.

number() is the only place the reader parses numbers; it serves
CheckCallLossless and CallHash (callers: mcpserver approval gate).
Reviewers ruled on #226 and #485 that parse_rfc3339 supports years 0001 to
9999. The docstring (identical in tools/ and demo/, which
test_parse_rfc3339_identical_across_copies pins) now says so, including that
a value whose UTC instant leaves that range after its offset is malformed.
Both ends are already tested (ACCEPT 0001-01-01T00:00:00Z and
9999-12-31T23:59:59.999999999Z; RANGE_ERRORS year 0000 and the two offset
overflows); they are now labelled. No behaviour change.
demo/SOURCE-MANIFEST.json: sha256 of demo/verify_evidence_pack.py refreshed,
as in 4744f27.
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
satgate Ready Ready Preview Oct 16, 2026 6:58pm UTC

@matt-dean-git

Copy link
Copy Markdown
Member Author

Independent review (profile satgate-security-reviewer, xai-oauth grok-4.7), round 1
Verdict: PASS
Head SHA: d2b8076
Blockers: none

Base: 348ed79. Tree: 1394aacc6fd38916858f0381d1b3c05d9adbc27a. Merge-base equals base. Did not approve, merge, label, deploy, or start Actions.

Scope: 7 files (demo/SOURCE-MANIFEST.json, demo/verify_evidence_pack.py, pkg/mcpserver/spend_gate_test.go, pkg/toolcontrols/strictjson.go, pkg/toolcontrols/strictjson_test.go, tools/test_rfc3339_fraction.py, tools/verify_evidence_pack.py). git diff --name-only base..head -- satgate-landing/ is empty.

Task A: amountEchoes (pkg/mcpserver/spend_gate_test.go:248) walks every key, string, and json.Number. It still flags decimal dollars (literal 777.31 and a plain-decimal word), whole dollars 777, and counter units 77731000000 (const units at spend_gate_test.go:167, same 1e8 as amountUnits). Nested maps and arrays are walked. Hex and mixed letter-digit words are not compared, so a token id that only contains the digits 777 is not an echo. Base go test ./pkg/mcpserver/ -run '^TestSpendRefusalDoesNotEchoTheAmount$' -count=3000 failed 10 times, all spend_gate_test.go:214, each token_id containing 777, zero line-219 hits. Head -count=3000 passed (1.842s). TestAmountEchoesIsExact passed. Two live plants in the OverLimit branch of pkg/mcpserver/spend_gate.go, each restored (worktree clean after): nested refusal data units 77731000000 failed at spend_gate_test.go:214 path $.error.data.wrap.units; receipt control_symbol "pair 777.31" failed at spend_gate_test.go:219 path $.control_symbol.

Task B: number() (pkg/toolcontrols/strictjson.go:311) is the only number reader; value() at strictjson.go:122 sends every number there, and object/array recurse through value(). At lead10 == 308 (strictjson.go:393) the magnitude, sign stripped, is compared with math.MaxFloat64; larger returns the same unreadable() ErrCallUnreadable "number is outside the exact range" as 1e999 (strictjson.go:88). Checked on head and base, four wraps (object, array, nested, top): 9e308, -9e308, 1.8e308, 1e+309, 1E309, -0.9e309, 9E+308, 18e307, 99e307, 17976931348623158e292 all refused on head with that error; 1e309-class spellings were already refused on base. A 309-digit mantissa is refused on both as "number too long" (maxNumberText 64), still ErrCallUnreadable. In-range spellings accepted on both, including 1.7976931348623157e308, 1.7976931348623157e+308, 1.7976931348623157E+308, 1e+308, 1E308, -1.5e+308, 10e307, 17976931348623157e292, 0.17976931348623157e+309, 1.797693134862315708e308. 2.2e-308 is refused on both (pre-existing underflow bound, not this diff). 1.7976931348623158e308 is newly refused: its exact value is above MaxFloat64, though ParseFloat rounds it down to MaxFloat64. That is the documented stricter cutoff, not an in-range rejection. Callers CheckCallLossless (strictjson.go:62) and CallHash (approval.go:245; approval_gate.go:91 and :135) share readStrict, so they get ErrCallUnreadable.

Task C: AST of both verify_evidence_pack.py files is unchanged once the new parse_rfc3339 docstring is stripped. tools/test_rfc3339_fraction.py adds one comment line above the existing 0001 and 9999 cases. Recomputed sha256 of demo/verify_evidence_pack.py is 74b6c5d56bed973da9bc6cd9c90d680d523ec26dcce3ba5f182eda853495ec02, equal to demo/SOURCE-MANIFEST.json:91. The old hash b35f5474 is gone from the tree. demo/tests/test_public_demo_static.py:153 recomputes the pin from the file bytes. satgate.io does not vendor the script: satgate-landing/app/.well-known/satgate/route.ts:65 points at the GitHub blob of tools/verify_evidence_pack.py, and the verify page only tells the reader to run that file. It does not need a site edit. The enterprise dashboard copy at dashboard/public/verify_evidence_pack.py (docs curl cloud.satgate.io/verify_evidence_pack.py) is an older fromisoformat parser, not this function. This PR does not change verifier behavior, so that served file does not need this docstring patch.

Gates, from the head worktree: gofmt -l on the three changed Go files printed nothing (exit 0). go vet ./... exit 0. go test -race -count=1 ./pkg/toolcontrols/... ./pkg/mcpserver/... exit 0 (toolcontrols 2.461s, mcpserver 36.936s). /tmp/ph-venv Python 3.11.15: unittest discover -s tools -p 'test_.py' 70 OK; discover -s demo/tests -p 'test_.py' 8 OK. Same commands as .github/workflows/ci.yml:30 and :33.

CI: gh pr checks 243 and the commit check-runs are green on d2b8076. Actions run 38951466423 (pull_request, conclusion success, headSha matches). Go job 117143620318 (log: ok pkg/mcpserver 52.117s, ok pkg/toolcontrols 8.953s). Node.js SDK job 117143620186. Python SDK job 117143620529. Vercel Preview Comments check-run 117143671526. Vercel deployment pass has no Actions run id. This public workflow has no wave1-guards job. PR is OPEN, not draft, MERGEABLE, mergeStateStatus BLOCKED, reviewDecision REVIEW_REQUIRED (branch policy, not a failed check).

@matt-dean-git
matt-dean-git merged commit 4f7f276 into main Oct 16, 2026
5 checks passed

This branch was successfully deployed

1 active deployment
Preview — d2b80762 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant