Skip to content

Host-receipt recorder: output_sha256 digests the published excerpt only - #492

Merged
seathatflowsinourveins merged 3 commits into
mainfrom
claude/host-receipt-excerpt-digest-20260929
Sep 29, 2026
Merged

seathatflowsinourveins merged 3 commits into
mainfrom
claude/host-receipt-excerpt-digest-20260929

Conversation

@seathatflowsinourveins

Copy link
Copy Markdown
Collaborator

Scope

scripts/host_receipts.py record now stores output_sha256 as the sha256 of output_excerpt, the sanitized output cut at 400 characters, so the digest binds only the bytes the receipt publishes.

Before this change it hashed the raw output. Anyone could put a guessed home directory or user name back where the receipt shows ~ or <user>, hash the result and compare it with output_sha256; the digest confirmed the guess whenever the output fit in the excerpt.

  • Lane: lane:foundation. No shared hot file is edited except manifests/evidence.json (re-registration, last commit).
  • Changed: scripts/host_receipts.py (the recording loop and its comment), adoption/host-receipt.schema.json (the output_sha256 description), tests/test_host_receipts.py (OutputDigestBasisTests, 4 tests), manifests/evidence.json.
  • Not changed: any receipt already recorded (they are append-only), the schema's structure (additionalProperties stays false), and every other recorder (see Residuals).

Why the digest covers the excerpt only

  • The raw digest is the defect above.
  • A digest of the whole sanitized output is also wrong. The first version of this fix used it, and a GPT-6 review found the oracle: printf '%0500d\nalice bob\n' 0 gives two accounts identical public excerpts, but the unpublished suffix sanitizes differently (the name equal to the account becomes <user>), so hashing the known text with either name replaced identifies the account. A digest must never depend on text the receipt does not publish.
  • The cost: the digest binds the published 400 characters, not the whole output. Two outputs that share their first 400 sanitized characters share a digest. No consumer relies on a raw-output basis (the reviewer searched validators, verifiers, report builders and tests).
  • No per-command basis marker is recorded. The schema's command items forbid additional properties, so a marker would be a structural schema change that a checkout on the older schema rejects. The schema description states the date instead: from 2026-09-29; receipts recorded before then, or by an older copy of the recorder, hash the raw output.

SOTA sources

  • Article 29 Data Protection Working Party, Opinion 05/2014 on Anonymisation Techniques (WP216), section 4 "Pseudonymisation", page 20 (read 2026-09-29): a hash function cannot be reversed, but "if the range of input values the hash function are known they can be replayed through the hash function in order to derive the correct value for a particular record", and hash functions "are subject to brute force attacks". A home directory or user name is such a small known range.
  • Python hashlib: sha256 over UTF-8 bytes, which is what the recorder already used.

Evidence-class table

Claim Evidence class Command / result
The 4 new tests fail on the previous recorder where they should local integration, measured 2026-09-29 Against origin/main's scripts/host_receipts.py: python3 -B -m unittest -v tests.test_host_receipts.OutputDigestBasisTests runs 4 tests and FAILED (failures=5), all 5 in the two tests that assert the excerpt basis. The plain-digest compatibility control passes on both. The two-account probe passes on the previous recorder (its raw digests are equal); it guards the rejected whole-sanitized design
The tests pass on the fix local integration, measured 2026-09-29 python3 -B -m unittest tests.test_host_receipts: 184 tests OK; python3 -B scripts/host_receipts.py validate: exit 0, status passed
The other modules that read host receipts pass local integration, measured 2026-09-29 test_component_matrix 63, test_adoption_bootstrap_macos 161 (32 skips, environment-gated), test_capability_gate 27, test_catalog_freshness_propose 80, test_record_verdicts 125, test_receipt_staleness 30, test_hardware_profile 53, test_verdict_review_gate 167, test_landscape 95, test_platform_status 50, test_token_e2e_receipt_checks 14, test_verdict_flip_candidates 10: all OK, run one module at a time with TMPDIR outside /tmp
The registry is consistent local integration, measured 2026-09-29 python3 -B scripts/validate.py, scripts/evidence_manifest.py --check and scripts/component_matrix.py --check exit 0 (run by the merge script)

Review

  • GPT-6 (gpt-6-astra, effort max, codex exec -s read-only; the model is the -m request, not an observed resolution) reviewed the first version (digest of the whole sanitized output): needs_changes. In scope: 1 major (the suffix oracle above; fixed by digesting the excerpt only, with the reviewer's probe as a test) and 1 minor (catalog_revision does not reliably mark the digest basis; the schema description no longer implies it does). Outside this change: 2 major and 1 minor, listed under Residuals. It confirmed the original short-output attack is closed and found no consumer that assumes a raw-output basis.
  • Claude: an Opus builder in an owned worktree and an independent Opus verifier that re-ran the checks reviewed the first version. Neither reviewed the final excerpt-only version.
  • Residual review gap: the final version has had no second independent review (bounded loop: one review, one repair round). Its rejected-design probe is a test.

Residuals

  • Receipts recorded before 2026-09-29 keep raw-output digests. Whether to re-record or accept them is the receipt owners' decision. This PR publishes no count of them.
  • The same class in other recorders, not changed here: scripts/native_token_ci.py (raw stdout, stderr and artifact digests beside sanitized representations, around lines 718 and 744) and tools/sota-convergence/landscape-sweep/usage_record.py (a raw-output digest beside a stripped transcript-directory prefix, line 77; tests/test_landscape_sweep_harness.py around line 2062 enforces that raw basis).
  • The cmd field records the command text as given, so a literal user name or home path inside a command survives. An --identity equal to a user name is confirmable through its documented identity digest.
  • The digest binds 400 characters, so it is not an integrity check on the whole output.

🤖 Generated with Claude Code

Scout and others added 3 commits September 29, 2026 00:11
output_sha256 was the sha256 of the raw command output, while output_excerpt is the sanitized output cut at 400 characters. When an output fits in the excerpt, a reader could put a guessed home directory or user name back in place of the sanitized token, hash the result and compare it with output_sha256, which confirms the guess offline. The digest is now the sha256 of output_excerpt itself, so it never depends on unpublished text. A digest of the sanitized but unpublished rest of a long output was rejected in review: for a command whose output is known, the sanitized text changes with the account name, so hashing a guess of it tells accounts apart.

Receipts recorded before this change keep their digests (append-only). The schema's command items forbid additional properties, so no per-command basis marker is added; the code comment and the schema description record the basis change and its date.

Tests: a home-path output is digested sanitized and not raw, and the offline probe no longer reproduces the digest; a long output is digested as its excerpt, not as the whole sanitized or raw output; two accounts running the same command with the same public excerpt get the same digest; an output without a home path keeps the plain digest.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…t, branch files re-registered)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…t, branch files re-registered)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Sep 29, 2026
@seathatflowsinourveins

Copy link
Copy Markdown
Collaborator Author

Review record, 2026-09-29 (native-agent-stack-10). Merge candidate: head d8039693 (the fix commit bfdb77da plus merges of main, latest b1be50c8, by the hot-file protocol).

  • First version (digest of the whole sanitized output), reviewed by two families, read-only: GPT-6 (gpt-6-astra, effort max, codex exec -s read-only; the model is the -m request, not an observed resolution) returned needs_changes: 1 in-scope major (the unpublished suffix of a long output made the digest a user-name oracle, reproduced with printf '%0500d\nalice bob\n' 0), 1 in-scope minor (catalog_revision does not reliably mark the digest basis), and 2 major plus 1 minor about other recorders and fields (listed under the body's Residuals). A Claude builder and an independent Opus verifier had passed that version. The digest was then changed to the sha256 of output_excerpt, the published bytes only, and the reviewer's probe became a test.
  • Final version: GPT-6 (same settings) returned approve_with_nits at this head: no defect in the code, the tests, the schema description or the registry change; one nit, that the body's twelve-module "all OK" counts have no retained output. Those counts are the coordinator's reported local integration results and their output is not retained in this PR; the four OutputDigestBasisTests and the 184 tests.test_host_receipts tests are in the tree and reproducible with the commands in the body. No second Claude review of the final version was run (bounded loop: one review, one repair round).
  • Checks on the merge candidate (local integration checks, not upstream tests): python3 -B -m unittest tests.test_host_receipts 184 OK; python3 -B scripts/host_receipts.py validate exit 0, 182 receipts; scripts/validate.py, scripts/evidence_manifest.py --check and scripts/component_matrix.py --check exit 0. Against origin/main's recorder the four new tests fail (5 failures in the two excerpt-basis tests). Hosted required checks are read with gh pr checks 492 --required immediately before merging.
  • Follow-ups this PR does not do: receipts already on main keep raw-output digests (owner decision, no counts published here); Add macOS host receipts for 9 token-efficiency components + jcodemunch-mcp (issue #276) #392's receipts are re-recorded on the Mac with this recorder; scripts/native_token_ci.py and tools/sota-convergence/landscape-sweep/usage_record.py have the same class of digest.

@seathatflowsinourveins
seathatflowsinourveins merged commit c0ca46d into main Sep 29, 2026
25 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/host-receipt-excerpt-digest-20260929 branch September 29, 2026 05:15
seathatflowsinourveins added a commit that referenced this pull request Oct 11, 2026
…ence, blueprints and manifests (#1010)

* Re-derive 10 published digests that hashed unmasked bytes, and guard evidence, blueprints and manifests

Correction #161: the 2026-10-11 masked-digest audit of main at 7b76a88 found 8 sha256 values under evidence/artifacts that hash bytes from before sanitization. Its residual list found 2 more under blueprints. Each sits beside a copy in which the home directory is replaced by ~ or the user name by <user>. Anyone could put a guessed value back, hash the result and confirm the guess offline.

Each value is now the sha256 of the published bytes. That is the basis scripts/host_receipts.py record has used for output_sha256 since #492 (scripts/host_receipts.py:1012-1029 at 7b76a88).

- sota-refresh-20260925 markitdown (2), mcporter (3) and rtk (1) results.json: files[].run_copy_sha256 is now the sha256 of the committed file, the same value as the entry's sha256. byte_identical_to_run_copy stays false. Each record gets a corrections entry; mcporter had no list, so one is added.
- The withdrawn nemotron vLLM use receipt -2: commands[1] and commands[4] output_sha256 are now the sha256 of output_excerpt (UTF-8), and its README records the change. This removes no oracle, because 4 registered evidence/hosts receipts keep the same digests (commands[1]'s in 4, commands[4]'s in 3).
- blueprints/retrieval-quality-v2/review-293-repro-20260926.json: inputs.files[0].as_run_sha256 is now the committed driver's sha256. A corrections entry in the record's own shape records the change.
- The withdrawn byterover-cli install receipt: commands[0].output_sha256 is now the excerpt digest. retained-outputs.json, the byterover-cli README and the trials README record the exception to "kept byte-for-byte".

The sha256 of a file before this change would confirm a guess the same way: rebuild the old file from a guess, hash it and compare. Every digest of an old file is therefore re-pinned or dropped:
- the three evidence/receipts qualification receipts re-pin results.json in provenance[], as #314 did;
- retained-outputs.json gives the new receipt bytes for both private_original and published;
- the review-293 correction records no as_recorded_sha256;
- manifests/evidence.json re-registers the 13 files through host_receipts.register_file.

tests/test_published_digest_privacy.py walks every JSON file under evidence/, blueprints/ and manifests/. It flags a 64-hex digest that hashes none of the published texts or files in its own or enclosing object, but does hash one of them once a home directory or name is put back. The masks are ~, <home>, <HOME>, <host-home>, $HOME, ${HOME}, <user>, <USER> and $USER, under /home/<name> and /Users/<name>. Names come from LANDING_PROHIBITED_NAMES. Without them, as in CI, that check skips.

LEGACY_SITES freezes the 106 remaining sites by path, key and 8-hex prefix, with a count assertion:
- 96 output_sha256 values in evidence/hosts receipts recorded before 2026-09-29, append-only since #492;
- 10 that the audit's probe could not reach, because it put back only ~ and <user> and read only files named in the digest's own object.

A new site fails. A listed site that is gone also fails until it is removed, so the list only shrinks. Two checks run in CI too: every listed site is still present, and the 10 fixed sites hash the published bytes.

Fail-before: on the 7b76a88 tree, the guard reports exactly the 10 fixed sites and the fixed-site check fails 10 subtests, with or without names. After this change, all 11 tests pass, or 10 pass and 1 skips without names.

Not changed: git history keeps the old digests. No rewrite is proposed, because the exposure is a home directory or account name, not a credential.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Re-pin readiness to the re-pinned receipts and enforce the digest guard in pre-push (#1010 round 1)

B1 (CC adjudication of #1010 round 1): catalogs/north-star/readiness.json still pinned the pre-change sha256 of the three qualification receipts this PR re-pins, in 15 places. Each receipt had five: the repo:<receipt>@<digest> key, that entry's sha256 and expected_sha256, and two nested receipt objects. Rebuilt from the new bytes and a guessed name, each old receipt matched its pin, which is the second-order oracle of correction #161. Each pin now names the receipt's current sha256:
- markitdown-018: d9a1a348 -> 74d066a1;
- mcporter-0141: 5379b1e3 -> 9b69956e;
- rtk-050: 9de293b6 -> b47326a1.
Byte counts are unchanged. tools/north-star/build_readiness.py --write was not used, because it would also fold in readiness drift that predates this PR.

tests/test_published_digest_privacy.py:
- ReadinessPinTests needs no names. Every readiness binding of a receipt changed since the merge base with origin/main must hash that receipt's current bytes. It fails at 7a03593 (15 stale pins) and passes after.
- The guard is now enforced. scripts/git-hooks/pre-push runs it on each pushed tip with PUBLISHED_DIGEST_GUARD_HOST_NAMES=1, so the names come from scripts/host_name_scan.py host_names(), the runtime source of #934's scan (docs/decisions/2026-10-09-host-name-scan.md); NATIVE_AGENT_HOST_NAMES_JSON replaces them in fixtures. The hook's runner refuses a skip, and an unavailable source errors. CI keeps the name-free tests.
- The legacy check is one-directional, so it works in a clone whose names differ. An unlisted site fails. A listed site the names do not reproduce is reported only as a count. test_the_frozen_list_only_shrinks still fails, without names, once a listed digest is gone from its file.
- The mask set adds the repository's existing markers <user-home>, <SCRATCH_HOME>, %USERPROFILE% (as C:\Users\<name>, C:/Users/<name> or /mnt/c/Users/<name>), <WSL_USER> and <hostname>. Every subset of up to four present masks is tried; beyond four, all of them or one at a time.
  - Measured with LANDING_PROHIBITED_NAMES (3 names) and with host_names() (5 names), the guard finds exactly the 106 listed sites both times. There is no new site, so the list and LEGACY_COUNT are unchanged.
- Caching texts and resolved paths halves the whole-tree walk, from 24 s to 11 s with 3 names.

tests/test_pre_push_gate.py overlays the guard module on the fixture tip, as it does the scanner sources. A clean push runs the guard once. A committed receipt whose output_sha256 hashes its excerpt with the fixture home put back is refused without echoing the name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant