Repository navigation
integrations: xChk registry — approval-outcome crosswalk, fixtures and receipt verifier (from trace-spec#560; stacked on #338) - #350
Conversation
dinakarjs
left a comment
There was a problem hiding this comment.
Reviewed draft head 7523105 for the migration and its public documentation/test boundary. Independent Linux/Python 3.12 run with released agentrust-trace 0.11.0 and agentrust-trace-tests 0.6.2: 57 tests pass, 1 live-key-fetch test skipped. This is a bounded software fixture run, not independent registry execution, full private-report verification, trusted-root/hybrid-signature completion or execution-effect acceptance. Current returned hosted workflows are action_required, not successful CI.
The destination now contains the crosswalk, eight record cases/seven bundles, fixture README/expected data, updated verifier/regressions, NOTICE and folded adopter wording. #338 remains the separate Trust Record layer. Please retain this draft's explicit private-assessment dependency and obtain current-head assessment before ready/merge; migration and passing fixtures do not resolve that hold. No confidential reproduction details are included here.
Two public documentation corrections are also needed:
- CROSSWALK.md still links ../../spec/trace-v0.2.md and ../../tests/test_xchk_approval_outcome_fixtures.py, neither of which exists from the new directory. Use the public TRACE specification URL and tests/test_approval_outcome_fixtures.py.
- Align verifier obligations 5-6 and the conformance-fixture description with the Boundary/README statement that decision.amendment is informational and its scope is not yet authenticated. They currently instruct a reader to treat the amendment's command as the approved/executed comparison target, while the boundary forbids letting that unsigned value change approval scope. Describe the existing fixture label as a worked example under the stated limitations until the private assessment settles the relying-party contract; do not strengthen it into acceptance by prose.
This closes my migration-location check, not the security assessment or maintainer acceptance. Reviewed with OpenAI Codex assistance.
…rint; a pinned private key is refused and never echoed (19 Oct 2026) CodeQL (py/clear-text-logging-sensitive-data, security-extended) flagged the final print in xchk_trace_record.py on this head (dd97be8) and on agentrust-io#350's. Its "secret" heuristic matches any name containing "trusted", and two such names reached the printed verdict: the library result's trusted_key_thumbprint attribute and this module's trusted_origin() function. One is the RFC 7638 thumbprint of a PUBLIC key; the other is the https origin the caller passed with --registry. Neither is secret. Changes: - verification(): the thumbprint is computed with the library's own jwk_thumbprint() over the key verified with (agentrust-trace 0.11.0, the floor). A test asserts it equals the library result's thumbprint. - trusted_origin() is renamed anchor_origin(), for the origin the relying party anchors trust in. Output field names are unchanged (trusted_origin, trusted_key_thumbprint). - Real hardening found while tracing the flow: a pinned key file carrying the private member d is refused with fixed text ("pin the public key only") and nothing of it is echoed; a key file that does not decode is reported by error class only (a decode error's message can quote bytes of the file). Same change as agentrust-io#350's 6013421 for these two files, so the stacked branch rebases onto this cleanly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… public specification, not a path that does not exist from this directory (19 Oct 2026) Review of agentrust-io#350 (dinakarjs, 15:03Z) named it; the same review's second broken link was fixed in 7eb34f3 and this one was missed. lychee, with the repository's own arguments, over integrations/xchk-registry: 20 OK, 0 errors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ription and the 07 README aligned with the boundary: decision.amendment is informational, its scope not yet authenticated (19 Oct 2026) Review of agentrust-io#350 (dinakarjs, 15:03Z), second correction, as written: the obligations no longer tell a reader to treat the amendment's command as the approved or executed comparison target. Step 5 says decision.amendment is informational and a relying party must not let it widen or narrow the approval; step 6 compares the executed call with record.paramsHash only. The verifier's amendment checks are described as what they are, a refusal of an amendment the sealed record does not commit to, which does not authenticate scope. The 07 fixture's label, "approval confirmed, as amended", is described as a worked example under the stated limitations until the private assessment settles the relying-party contract. The boundary bullet returns to its statement: informational, not yet authenticated, under separate assessment, the one open item; its further limits are kept as limits. No code change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6013421 to
9b1622e
Compare
|
Thanks, @dinakarjs. Head is
Also on this head since |
dinakarjs
left a comment
There was a problem hiding this comment.
Rechecked exact head 9b1622e. Both documentation corrections from my review are now addressed: the specification URL and local test link are corrected, and obligations 5–6 plus the fixture descriptions retain the informational, unauthenticated-scope boundary for decision.amendment. This closes those two documentation findings.
Independent offline run using Python 3.12.14, released agentrust-trace 0.11.0 and agentrust-trace-tests 0.6.2: 73 tests passed, 1 opt-in live check skipped. Existing environment reused; this was not a fresh dependency-lock rebuild. No live registry or independently observed execution evidence is claimed.
The private-assessment dependency remains open, so this is not approval or clearance to merge. All 14 returned current-head hosted workflow runs report action_required, including CodeQL and xchk-registry conformance; please have a maintainer authorize those runs. Retain draft status until the assessment and current-head review are complete. Reviewed with OpenAI Codex assistance.
… Level 0) (#338) * integrations: xChk registry, a Trust Record per run (record-producer, Level 0) An xChk registry licenses AI agents under a verified person and gates each act. This listing covers one thing: the software-only Level 0 Trust Record the registry mints per run, with one approval-outcome reference per act a person was asked about, and how a relying party verifies it with released agentrust-trace against the key the registry publishes (pinned, or fetched under the record's kid; never the key the record itself carries). One committed record from a live run, a pinned key, five offline tests, an opt-in live key check, and the Level 0 conformance suite in the workflow. What a reference resolves to is out of scope here and said so; that is a separate integration. Signed-off-by: minitru <bbunix@gmail.com> * xchk-registry: a finite record age, the revocation outcome reported, a stale-record test (review of #338, 18 Oct 2026) The verifier passed max_age_seconds=None, which switched off the library's 24-hour check; section 3.2.2 requires a finite maximum age. Now MAX_AGE_SECONDS = 86400 (the library's default), --max-age overrides it and must be positive, and --now re-verifies a retained record at its own time instead of widening the bound. The output carries the revocation outcome as the library reports it, no_check_performed here since no bundle or store is given (3.2.3), and the reason for a refusal. Tests: a record one second past the bound is invalid, the bound cannot be 0, negative or None, the CLI refuses --max-age 0, a month-old record is stale at the default, the revocation outcome is printed; the offline tests pin now at the fixture's own minute. README: the age bound and --now, and the conformance command gets the explicit --max-age the workflow already uses, because the example record is historical. * xchk-registry: the published key comes from the subject's origin, never from a server the record names (18 Oct 2026) The default path fetched the key document from the resolver the record itself carried, so a record could name the server its key would be fetched from: the embedded-key problem one hop away. Now the trusted origin is the relying party's (--registry) or the origin under the record's did:web subject; the record's resolver must agree with it and is never followed. The fetch is https only, follows no redirect, checks the status and reads at most 256 KB. The output names the trusted origin, and a refusal says why. Tests: a resolver pointing elsewhere, a subject moved to another host, an http origin, a redirect and an oversize document all end in no-key with nothing fetched from the record's server; did:web parsing incl. an encoded port. Live check (REGISTRY_LIVE=1) passes against in.xchk.io. * xchk-registry: the trust origin is the relying party's alone; profile fields and the as-of time in every verdict; a malformed key document is no key (review of #338, round 3, 18 Oct 2026) Deriving the published-key origin from the record's did:web subject (858aa7f) only showed the record agreed with itself: a record signed under a publisher of its own choosing names that publisher in its subject too. Now --registry is the one source of the origin; without it or --jwk there is no trust anchor and the answer is no-key. The record's subject and resolver must agree with the origin given. Negative control: a record signed end to end by a key the attacker made, with its own subject and resolver and its host publishing the key, is no-key without an anchor, no-key against the relying party's registry, and invalid against the pinned real key; nothing is fetched from the attacker's host in any case. A verified result carries the profile and the complete accepted_profiles set (section 3.3) and the trusted key's thumbprint. Every verdict carries verified_at, the instant it was made against, and verified_at_overridden, so a historical verdict is never the same JSON as a current one; tested as a stale-current / historical-verified pair. key_from_published refuses every malformed document shape without raising, and the fetch wraps it besides. 12 tests pass including the live key fetch. * xchk-registry: the as-of time on every verdict, no-key included (review of #338: 'for every verdict'; 18 Oct 2026) * xchk-registry: a stale comment that still described the subject-derived origin, removed; the docstring says what a verdict carries; one instant for the as-of time (18 Oct 2026) * xchk-registry: one verification instant, frozen at the boundary (review of #338 by dinakarjs, 18 Oct 2026) With --now absent the clock was read twice: once in verify_with_key_source for the verdict's verified_at and again in verification for the instant handed to verify_record, and the verdict's value overwrote the library's. A clock answering 100 then 101 gave the library 101 while the verdict said 100. Now an Instant is frozen once where the request enters (verify_with_key_source, or main for the CLI) and the same object reaches the library call and every emitted verdict, no-key included; the clock is read at most once per verdict; whether the caller overrode the time is preserved. Regressions: the two-value clock with a spy on verify_record on the pinned, published and no-key paths, and through the CLI, with and without --now; each asserts the recorded instant equals the one supplied to the verifier. 15 tests pass including the live key fetch. * xchk-registry: every input fails closed with a verdict, found by mutation, not one review at a time (18 Oct 2026) Four review rounds had found input shapes one at a time (a stale record, a malformed key document, two clock reads), so this time every top-level field of the record, its key fields and its reference fields were set to each of fourteen odd values and run through main() on the pinned, published and no-anchor paths: 40 mutations crashed, all on the pinned or published path (a record that is not an object, cnf or cnf.jwk not an object, references not a list, a resolver that is not a string), and the pinned-key file crashed on anything but an object. Now: a record that is not an object is `invalid`; a pinned key that is not an Ed25519 JWK is `no-key`, as is a key document of any other shape; references and resolver are read only when shaped; any exception the library raises on a shape it did not foresee is `invalid` with the exception named; a record or key file that is not JSON is a verdict, not a traceback; --registry must be an https origin and nothing more (no credentials, path, query or fragment); --now must be non-negative. The verdict's `kid` is always the key the record names, `pinned_kid` the pinned key's own, and the thumbprint the key actually used. The mutation loop is a test (798 runs, every one a verdict with the frozen instant), beside tests for each input. 18 tests pass under 3.11 and 3.12 including the live key fetch; Level 0 conformance passes; the repository's Validate and Lint steps pass. * xchk-registry: origin parsing refused not raised; a did:web subject under the origin required; every approval-outcome resolver checked, order-independent (review of 2dcb159 by noah-ing and dinakarjs, 18 Oct 2026) Three findings on the last head, each outside the shapes the previous sweep covered. (1) https_origin let urlsplit and .port raise on a bad bracket, a non-numeric or out-of-range port: a traceback instead of the promised verdict. Both are caught and refused with the reason; the netloc must also be exactly host or host:port, so a trailing colon or an empty port is refused too. (2) A record whose subject was absent or not did:web passed the origin check, because only a parseable, different did:web origin was refused; a signed synthetic record with subject did:key verified. The subject must now be a did:web under the trusted origin, and a signed control for each shape says so. (3) Only the first approval-outcome resolver was checked, so a second, off-origin reference verified and the result depended on the order; every approval-outcome reference must now resolve at the origin, one that names no resolver is a refusal, references of other kinds are ignored, and a signed control covers each ordering with a valid twin. Then the sweep that should have come first: 43 origin strings x 19 subjects x 8 reference sets on signed records, every run a verdict, a fetch only when the origin is well formed and the record agrees with it; the same origins through the CLI, pinned and published. 22 tests pass on 3.11 and 3.12 including the live key fetch; Validate and Lint steps pass; Level 0 conformance passes. * xchk-registry: https on 443 is one origin with or without the port (RFC 6454); a malformed --registry is refused even beside a pinned key (18 Oct 2026) * xchk-registry: an origin given beside a pinned key is enforced, not just parsed; resolvers compared as origins; the sweep pins a key too and judges by an oracle of its own (review of 09c436b by noah-ing, 18 Oct 2026) With --jwk the registry was only parsed, so a record for another registry verified under the pinned key while --registry named a third. Now an origin that is given is enforced whichever way the key comes: the record's subject must be a did:web under it and every approval-outcome resolver must be that origin, compared in canonical form, so an explicit :443 or a different case is the same origin and a resolver with a path or another scheme is not one. Controls: the committed record with its own registry and with another, pinned both times; signed records with a non-did:web subject and an off-origin second reference, pinned, refused by the origin and verified by the key alone; the explicit-443 twin. The sweep now runs every combination pinned as well as published, and its expectation of what an origin string means is written from RFC 6454 in the test, not taken from https_origin; that oracle found one more case on the way, a leading space, which urlsplit strips and the verifier accepted: whitespace and control characters are now refused. 23 tests pass on 3.11 and 3.12 including the live key fetch; Validate and Lint steps and Level 0 conformance pass. * xchk-registry README: --registry beside --jwk enforces both; 443 is the same origin (18 Oct 2026) * xchk-registry README: no placeholder URL for the link checker to fetch (18 Oct 2026) * xchk-registry: CodeQL clear-text-logging alert on the CLI's verdict print; a pinned private key is refused and never echoed (19 Oct 2026) CodeQL (py/clear-text-logging-sensitive-data, security-extended) flagged the final print in xchk_trace_record.py on this head (dd97be8) and on #350's. Its "secret" heuristic matches any name containing "trusted", and two such names reached the printed verdict: the library result's trusted_key_thumbprint attribute and this module's trusted_origin() function. One is the RFC 7638 thumbprint of a PUBLIC key; the other is the https origin the caller passed with --registry. Neither is secret. Changes: - verification(): the thumbprint is computed with the library's own jwk_thumbprint() over the key verified with (agentrust-trace 0.11.0, the floor). A test asserts it equals the library result's thumbprint. - trusted_origin() is renamed anchor_origin(), for the origin the relying party anchors trust in. Output field names are unchanged (trusted_origin, trusted_key_thumbprint). - Real hardening found while tracing the flow: a pinned key file carrying the private member d is refused with fixed text ("pin the public key only") and nothing of it is echoed; a key file that does not decode is reported by error class only (a decode error's message can quote bytes of the file). Same change as #350's 6013421 for these two files, so the stacked branch rebases onto this cleanly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Signed-off-by: minitru <bbunix@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the verifier and fixture verdict path at draft head 9b1622e42d5daee54e001e4b9d2a6ddff9a79280. My independent offline negative cases found that the strict verifier's success can be promoted to a stronger receipt-assurance verdict than its report supports. I placed the exact synthetic matrix, affected lines, and regression request in the existing private assessment thread for the maintainers; please use that thread for the details and keep the draft pending until this distinction is fixed and rerun. This does not reassert the already-addressed #337 approval-evidence cases or claim a forged record, live registry failure, or production impact.
The focused local run was 73 passed, 1 skipped, with changed-file Ruff clean. I did not run the repository-wide suite or hosted CI. Current-head workflows still await maintainer authorization, and the private assessment remains a dependency. Requesting changes on this head.
…erifier, moved here from trace-spec#560 (19 Oct 2026) A crosswalk for one registry's receipts belongs with that registry's integration (maintainers, trace-spec#560). This is the replacement for the part of closed agentrust-io#337 that agentrust-io#338 left out, stacked on agentrust-io#338. - CROSSWALK.md: field mapping, verifier obligations, boundary; two verifier rules added — the signed ask must name the ceremony the sealed record and the registry-signed binding commit to, and absence of failure is not confirmation (--require-approval). - examples/approval-outcome/: the eight records and seven bundles from #560, README, expected.json. - verify_receipt.py: the registry's offline verifier, vendored as served (MIT, in NOTICE), current version. - tests: the fixture test reworked (finite explicit age bound; the approval verdict requires the approval evidence to have been present and checked; mutations for missing evidence, a same-credential cross-ceremony assertion, unsigned outer labels) and the verifier's own regressions. - NOTICE, README (adopter line folded in; what is deliberately not here), pyproject test extras. Not in this change: a packaged relying-party verdict; under the repository's private assessment, follows here. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… fixture verdict reads the verifier's structured result (19 Oct 2026) The amended case was inconsistent: the fixture test selected approval-confirmed-as-amended from the presence of the outer decision.amendment.command, the README said that command is what the person approved, and the cross-walk said the verifier does not authenticate amendment scope. Reconciled on what the bundle carries: - verify_receipt.py (as the registry serves it): when decision.amendment.command is present, record.paramsHash must equal sha256 over the canonical action binding the registry hashed for the amended act, and that binding's command must be the amended command (first 2000 UTF-16 units). The registry serves the binding as amendment.boundAs, since the record's tool and summary are capped, trimmed copies; without it the binding is rebuilt from the record (exact under the 400-char cap, as for 07). When decision.actionParamsHash is present, amendment.originalParamsHash must equal it. Any failure contradicts the approval. --json gives a structured report (the checks that ran, the pending count, the approval evidence still missing, amendment.committed, and boundIdentity: the person and proofing level from the registry-signed binding only, with a transcript note when the readable labels differ). A bundle with wrong field types ends in a failed check, never a traceback (type-mutation sweep over every field of three bundles: 14,625 mutations, 0 crashes). - tests/test_approval_outcome_fixtures.py: _assess runs the verifier with --json and takes `amended` from report.amendment.committed, never from the bundle text; the commitment is recomputed independently with rfc8785. New: an amendment typed into 01 after issue, and 07 with its amended command or originalParamsHash edited or removed, are approval-contradicted; 07 as committed is as amended. 08 with its challenge removed is pinned at approval-unverified (never confirmed); a relabelled bundle reports the bound identity. - tests/test_verify_receipt_regressions.py: the same mutations through the verifier, a served preimage that is wrong or beside an edited command, a long untrimmed summary, an ask with no digest, malformed types. - README and CROSSWALK: 07's row and paragraph say what makes it an approval with changes; step 5 states the checks; the boundary says what is NOT established: the person signed the ask as proposed and the amended text is committed by the registry's seal; an amendment removed from a bundle leaves a plain approval that verifies; the unamended record.paramsHash and the signed actionParamsHash are unrelated digests here. The cross-walk's link to the fixture test now resolves. pytest integrations/xchk-registry/tests: 69 passed, 1 skipped (3.11); ruff --target-version py39 --select E4,E7,E9,F clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is refused; an amendment on a record that seals no digest is pending, not a failure (19 Oct 2026)
CodeQL (py/clear-text-logging-sensitive-data, security-extended) flagged the CLI's final print. Its "secret" heuristic
matches any name containing "trusted", and two such names reached the printed verdict: the library result's
trusted_key_thumbprint attribute and this module's trusted_origin() function. One is the RFC 7638 thumbprint of a
PUBLIC key; the other is the https origin the caller passed with --registry. Neither is secret. Changes:
- verification(): the thumbprint is now computed with the library's own jwk_thumbprint() over the key verified with
(present in agentrust-trace 0.11.0, the floor). A test asserts it equals the library result's thumbprint.
- trusted_origin() is renamed anchor_origin(), for the origin the relying party anchors trust in. Output field names
are unchanged (trusted_origin, trusted_key_thumbprint).
- Real hardening found while tracing the flow: a pinned key file carrying the private member d is refused with
fixed text ("pin the public key only") and nothing of it is echoed; a key file that does not decode is reported by
error class only (a decode error's message can quote bytes of the file).
- A scan of both modules' identifiers against CodeQL's secret and password name rules leaves one match,
urlsplit().password, used only in an "is not None" refusal.
Verifier (as the registry serves it, byte-identical): a ceremony record sealed before 19 Sep 2026 carries no
paramsHash, so its seal commits to no command. An amendment there was failed; four genuine September receipts on
the registry failed this way. It is now a pending step, not checkable, never confirmed. CROSSWALK step 5 says so;
regression from 07 with paramsHash removed.
pytest integrations/xchk-registry/tests: 73 passed, 1 skipped (3.11); ruff --target-version py39 --select
E4,E7,E9,F clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… public specification, not a path that does not exist from this directory (19 Oct 2026) Review of agentrust-io#350 (dinakarjs, 15:03Z) named it; the same review's second broken link was fixed in 7eb34f3 and this one was missed. lychee, with the repository's own arguments, over integrations/xchk-registry: 20 OK, 0 errors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ription and the 07 README aligned with the boundary: decision.amendment is informational, its scope not yet authenticated (19 Oct 2026) Review of agentrust-io#350 (dinakarjs, 15:03Z), second correction, as written: the obligations no longer tell a reader to treat the amendment's command as the approved or executed comparison target. Step 5 says decision.amendment is informational and a relying party must not let it widen or narrow the approval; step 6 compares the executed call with record.paramsHash only. The verifier's amendment checks are described as what they are, a refusal of an amendment the sealed record does not commit to, which does not authenticate scope. The 07 fixture's label, "approval confirmed, as amended", is described as a worked example under the stated limitations until the private assessment settles the relying-party contract. The boundary bullet returns to its statement: informational, not yet authenticated, under separate assessment, the one open item; its further limits are kept as limits. No code change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…val without it is approval-unsealed, never confirmed (20 Oct 2026) Review of agentrust-io#350 at 9b1622e (noah-ing): with the approval evidence intact, removing only the seal, only the Merkle proof, both, or only the registry's seal signature still let `verify_receipt.py --require-approval` exit 0 and the fixture verdict return approval-confirmed. Approval evidence present does not make the receipt sealed. - verify_receipt.py (byte-identical to the copy the registry serves): `sealChainMissing` in the --json report names the missing part (the seal, the Merkle path from the record to it, the registry's signature over it); --require-approval exits 3 when it is non-empty, result `seal-chain-incomplete`, kept apart from `approval-evidence-missing` (whose wording is unchanged). The default run is unchanged. - Fixture verdict: approved, approval evidence present, seal chain incomplete -> `approval-unsealed`, distinct from `approval-unverified` and from `approval-confirmed`. - Tests: the four deletion mutations as negative regressions, both at the verifier and through the fixture verdict; the sealed control still confirms. - CROSSWALK: the "absence of failure is not confirmation" rule now states the seal-chain half. Rebased on main after agentrust-io#338 merged (main's agentrust-io#338 tree is identical to the base this was built on). pytest integrations/xchk-registry/tests: 78 passed, 1 skipped (3.11); ruff --target-version py39 --select E4,E7,E9,F clean; lychee over the directory 0 errors; type-mutation sweep 14,625 mutations, 0 crashes; every earlier review-matrix row keeps its verdict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9b1622e to
b43b884
Compare
|
Thanks, @noah-ing. Head is You were right:
Run on
The draft stays a draft pending the private assessment, and the hosted workflows still need a maintainer to authorize them. |
dinakarjs
left a comment
There was a problem hiding this comment.
Completed the public seal-chain follow-up at exact head b43b884. The new report field, strict exit/result and fixture verdict keep an unsealed approval distinct from confirmed approval; the four published deletion cases and sealed positive control are present. The previously corrected links and informational amendment-scope wording remain.
Independent offline run on Python 3.12.14 with released agentrust-trace 0.11.0 and agentrust-trace-tests 0.6.2: 78 passed, 1 opt-in live test skipped. Isolated in-memory removal of only the strict seal-chain guard causes the new behavioral assertion to fail, while the valid and denied controls continue passing. No mutation was saved to source. Existing environment reused; no fresh lock rebuild or live registry campaign.
All 12 returned current-head hosted workflow runs now succeed, including CodeQL, links and xchk-registry conformance; this supersedes my earlier action_required checkpoint. No additional blocker established in this public delta. Noah's private-assessment review remains for Noah/maintainers to clear; this comment does not substitute for that review or grant ready/merge clearance. References: the exact-head CROSSWALK.md verifier obligations/boundary and the emitted verify_receipt.py report contract. Reviewed with Codex assistance.
…verify; the vendored verifier checks the decision's citation The fixture's verdict helper computed trace_record_verifies and reported it, but never let it decide the verdict, so a record with one signed field changed still reached approval-confirmed through its intact bundle (review of agentrust-io#350 at b43b884). A record that does not verify is now record-invalid before any bundle is read, with or without one, and the crosswalk's step 1 says so: nothing an invalid record carries, the reference included, is evidence. The vendored verify_receipt.py is the served one again (byte-identical to https://in.xchk.io/verify/verify_receipt.py): section 5b, the decision block must cite the sealed record's attestationHash. Tests: intact-versus-tampered regression (and the same record with no bundle); a decision citing another ceremony's record is refused. 80 passed, 1 live test skipped (py3.11); CI's ruff selection clean; lychee 0 errors. Replayed: the seal-chain sweep (every deletion refused in strict mode, control passes), all 128 single-field changes to the 8 fixture records end record-invalid, the verifier's contract and golden-corpus tests (73 passed), and the 16 live receipts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, @noah-ing, and I'm sorry this needed another round. The fixture's verdict helper checked the Trust Record, reported the result, and then did not let it decide the verdict. That is the same kind of gap as the seal chain last round, and I should have found it myself instead of leaving it for review. Head is
Checked: 80 passed and the 1 live test skipped on Python 3.11; CI's ruff selection clean; lychee 0 errors. Replayed every seal-chain deletion (strict mode refuses each, the control passes), all 128 single-field changes to the eight fixture records (each |
dinakarjs
left a comment
There was a problem hiding this comment.
Public-delta review at 2e1da6801e950517d23edb044751643e9c9caa3a, compared with b43b88423d413b0283ab0fcbcc164a660f8ced67:
The invalid-Trust-Record early return now prevents an intact referenced bundle from producing approval-confirmed for a tampered record. The new decision-citation check also refuses a decision citing another ceremony's record. No additional blocker identified in these two changes.
Validation from this exact checkout:
- Full xChk test selection: 80 passed, 1 skipped (live-key fetch); released agentrust-trace 0.11.0 / agentrust-trace-tests 0.6.2 in the existing local environment.
- Ruff with the repository's E4/E7/E9/F selection and whitespace checks pass.
- Independently disabled each new guard in memory: its corresponding added regression fails behaviorally. Original functions restored. With both guards enabled the paired valid controls pass.
- An additional spy control confirms an invalid record returns record-invalid with reference_resolves unset before any bundle load or bundle-verifier call.
- All 12 returned exact-head technical workflows succeed.
Scope remains bounded: fixture composition is not a packaged relying-party assessor; amendments remain informational, independent key trust and full composite/hardware verification remain prerequisites, and no live run is claimed. I did not access the private assessment. Its existing dependency/hold remains unchanged; this COMMENT review is not merge approval or private clearance.
Small documentation follow-up: the PR body's “Checked locally” paragraph still reports the earlier 57-test run; please update it when next editing the description so the stated validation matches the current head.
…mendment is committed only when its citation was checked (20 Oct 2026, private review of agentrust-io#350). Section 5b compared decision.attestationHash with the record's only when it was present, so a decision block with the citation removed skipped the check and the fixture verdict stayed approval-confirmed. Every ceremony record the registry has written since the ledger began (7 Aug 2026) carries the attestation hash, sealed before the record is appended, and every decision block it issues cites it: a decision block without the citation was never issued, and it is refused. Same class, found by looking for it: in 5c, without decision.actionParamsHash the amendment's citation leg defaulted to 'cites' and the report said committed. Now the citation is pending (amendment.citesAsk null), the amendment is not committed, and --require-approval refuses; the fixture assessor already ran strict mode before reading 'committed', so no fixture verdict changed. The verifier is the served one again (identical file); the crosswalk's step 4 names the citation check; regressions cover removed, null and empty citation, default and strict, and the digest removed with and without the challenge. 82 passed, 1 skipped; ruff clean; lychee 0 errors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Head
The verifier is byte-identical to the served one, which changed the same way (its CHANGELOG on in.xchk.io has the entry). Checked from this checkout: |
|
Thanks for the follow-up. I reviewed current head |
dinakarjs
left a comment
There was a problem hiding this comment.
Rechecked public delta 2e1da68..7020d82: a present decision block without its attestation-hash citation is refused, and an amendment lacking the ask digest remains pending/not committed, with strict approval refusal. The added tests retain intact positive controls and cover removed/null/empty citation values. The PR description now reports its current 82-test run.
Independent local Python 3.12.14 run with released agentrust-trace 0.11.0 and agentrust-trace-tests 0.6.2: 82 passed, 1 opt-in live-key test skipped. CI-selected Ruff and whitespace checks pass. Returned current-head technical checks, including fixed/floating conformance, links and CodeQL, succeed; the approval gate still fails. I did not independently rerun the author's guard-removal trials or live receipts in this pass.
I also read Noah's latest confirmation that his citation/amendment-state concerns are addressed at this head. No additional blocker found in this public delta. Existing draft/maintainer gates can proceed normally; this comment does not provide independent registry-root, hybrid-signature, hardware or execution assurance, or assert closure of a private advisory.
carloshvp
left a comment
There was a problem hiding this comment.
Requesting changes at exact head 7020d824af7cddfb94d27b103e2c9cef327b94c3. I reproduced an additional verifier-report boundary problem in independent negative controls and filed the exact reproducer, supported/invalid controls, affected paths and regression request through the repository's private security reporting channel: GHSA-c7vm-7mqv-w9fj.
The 82 committed integration tests pass with one opt-in live test skipped. pip check, 48 manifest validations, compatibility, generated index/catalog parity, the repository CI Ruff subset and diff whitespace checks also pass. Those positive results do not cover the additional failing boundary in the private report.
Please retain the merge hold until a named maintainer has triaged that report, the verification/reporting guard is repaired, and the independent negative controls are committed and rerun at the final head. The older closed #337 assessment does not settle this new exact-head result. Detailed reproduction stays in the private report under SECURITY.md.
This is a finding in the unmerged verifier and its worked fixture-assessment path. I did not contact the live registry, establish a deployed affected version, forge a TRACE record, or demonstrate an unapproved external action. Community tier and all existing independent-key, hardware, and target-effect limitations remain applicable.
Reviewed with OpenAI Codex.
rajnisht7
left a comment
There was a problem hiding this comment.
The overall changes looks good, a few things needs to be resolved:
- Hybrid signature can be downgraded, Fixture declares alg = ed25519+ml-dsa-65 and carries both signatures. Stripping only the ML-DSA-65 entry (leaving Ed25519 + alg field + keys intact) still yields exit 0 / result: verifies, including under --require-approval. registry_verify only checks signatures that are present; it does not require the full set implied by the declared hybrid alg / pinned keys. Seal hash does not cover the signature container, so this is an unsigned carrier mutation.
- Derived report fields stay actionable after global failure. On a receipt that fails (e.g. corrupted seal signature), boundIdentity and amendment.committed are still fully populated. A caller that reads those fields without first checking exit/result can treat them as verified.
Both sit in the same “report / assurance boundary” class that @carloshvp already flagged on this head (GHSA-c7vm-7mqv-w9fj). The 82 passing tests don’t cover the hybrid-strip mutation or “derived fields after failure”.
drk-ram
left a comment
There was a problem hiding this comment.
Head 7020d82. Merge-base 4eb3464 (the #338 merge); 26 commits behind main, and the merge is clean. I have not seen GHSA-c7vm-7mqv-w9fj. This review is independent of it and does not clear it. rajnisht7's hybrid-strip and derived-fields findings match the code as I read it; I did not rerun them and have not repeated them.
Windows, Python 3.11.9, agentrust-trace 0.11.0, cryptography 47.0.0, rfc8785 0.1.4.
- ruff --target-version py39 --select E4,E7,E9,F on integrations/xchk-registry: clean.
- pytest integrations/xchk-registry/tests as shipped: 20 failed, 62 passed, 1 skipped. Cause: verify_receipt.py prints
json.dumps(report, ensure_ascii=False)and its human lines contain U+2194 and an em dash. Under a cp1252 stdout that is a UnicodeEncodeError and no JSON, exit 1. The tests run it with-I, which ignores PYTHONUTF8 and PYTHONIOENCODING, so they cannot work around it. Withensure_ascii=Trueon that one print: 82 passed, 1 skipped, which matches the PR's numbers. Linux CI never sees this. The fix belongs in the served verifier (reconfigure stdout to UTF-8). Failing tests on a locale is the lesser problem: a verifier that dies with exit 1 on a traceback is indistinguishable from "a check failed".
-
Strict mode and the approval-confirmed verdict do not depend on the registry's key. I re-keyed fixture 01: my own Ed25519 key in every place the registry key appears (including the root statement's key list), keyId recomputed, seal hash, identity binding and profile hash re-signed, record untouched.
verify_receipt.py --json --require-approvalon it: exit 0, result "verifies", failed 0, pending 6. The genuine bundle gives the same exit, result and pending count. _assess() on the re-keyed bundle returns approval-confirmed, because the record, the reference digest and the strict result are all unchanged. CROSSWALK step 4 says as much in its limits (the keys the script checks are the ones the bundle carries). But--require-approvaland the verdict name read as stronger than that, and the fixtures give the relying party no way to supply the registry key. #338's xchk_trace_record already takes --registry and --jwk for the record. Please do the same for the bundle in the assessor: the relying party supplies the registry key ids it pinned (from /.well-known/xchk-keys.json), the bundle's keys must be in that set, and otherwise the verdict is a distinct approval-consistent, key not pinned. Add the re-keyed bundle as a case. If that is out of scope here, rename the verdict and say in the README that confirmed means consistent under the bundle's own keys. -
13 of the 28 verifier guards I mutated are not pinned by any test. I replaced each check's condition with True, one at a time, and ran all 82 tests (with the ASCII print above so the Windows crash does not skew it). Killed (15): ceremony transplant (both ask-v1 attestation checks), strict gate, strict seal-chain gate, decision citation required, citation equals the record's, amendment citation, committed flag, binding names the assertion's key, record hash, challenge, ask action digest, sealed amended command, bound command, exit-3-versus-1 precedence. Survived (13): binding names this record hash; binding names the credential; the passkey assertion signature; the user-verified flag; webauthn.get type; root statement lists the pinned keys; Merkle path reaches the root; seal hash recomputes; ask carries the prompt; pinned keyId recomputes; identity provider agrees with the binding; profile hash matches; unsigned profile is a failure. In the assessor, 5 of 7 mutations die; the survivors are the committed flag on an amended verdict and the resolver comparison.
To be sure these are unpinned rather than dead, I ran the real verifier on bundles altered one field at a time: assertion signature, UV flag cleared, seal.count, a Merkle step, binding.recordHash, binding.credentialId, decision.prompt, profile signature removed. Each exits 1 with the right FAIL. So those guards work; the tests just do not pin them. The PR describes the verifier's tests as generated from a contract of what each piece of evidence must do when removed or altered. Removal is covered; alteration mostly is not. A parametrized "alter this field, expect exit 1 and this FAIL" test over the list above closes it.
Hosted checks on this head are green except gate, which is the approval gate. I will re-review at the next head.
…s nobody, UTF-8 whatever the console, and the evidence contract with every guard pinned by its FAIL line (2 Nov 2026; review by rajnisht7 and drk-ram). - registry_verify requires every signature the receipt's signed fields name (the sealed profile's signing dial, else the seal's windowStart against the 24 Sep 2026 switch to hybrid); a missing half is a failed check. The alg label and the key list are unsigned and are not consulted. - boundIdentity and amendment are null whenever the exit is not 0. - stdout is UTF-8 from the first line: under a cp1252 console the verifier exited 1 with a UnicodeEncodeError, the exit of a failed check. - verdict-contract.json and its test, which generated the xchk-app regressions since 20 Oct, now live beside the verifier here; every row names the FAIL line it expects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Nov 2026). A required signature half that cannot be checked here is a strict-mode refusal, not a pass; the identity binding reads its own signed mode when the registry writes one; `withheld` names the fields a failed report leaves null; the verifier is the xchk-app owner byte for byte; the anchor chain, the --artifact check and the profile dial get tests. - Without dilithium-py the ML-DSA slot is a WAIT, and a required WAIT counted as present: garbage in that slot passed even --require-approval. Now `--require-approval` refuses (exit 3) and names the half in sealChainMissing (seal, profile) or approvalEvidenceMissing (binding); the default run stays a WAIT like every unverifiable step. With dilithium-py present the same garbage fails. - A binding is signed when the bundle is fetched, under the registry's mode then, so a seal from before 24 Sep 2026 carries a hybrid binding and the seal's date cannot require the binding's second half. The verifier reads a signed `signing` field inside binding.fields when present (registry change to follow); until then the seal's set is the proxy, and CROSSWALK says how far that reaches. - `withheld: ["boundIdentity", "amendment"]` accompanies the nulls on every non-zero exit, so absent and withheld read apart. - This copy had lacked the 22 Oct batched-anchor change of the owner; it is now identical to verify/verify_receipt.py in xchk-app (sha256 of both files equal), and the FAIL line for a bad signature says "DOES NOT verify". - Tests: the pure required_algs / binding_required rules, an edited dial does not relax the requirement, the uncheckable half under a stubbed ML-DSA, withheld, the --artifact check. The contract's two binding rows pin whole lines. A guard-mutation sweep (each ok() condition replaced by True) found, on round one, the artifact check, the identity-provider guard and the five anchor-chain guards unpinned; fixture 01 carries no anchor proof and no IdP block, so those two groups remain unpinned here and are named rather than claimed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thank you, @rajnisht7 and @drk-ram, for the detailed explanations: each finding came with the exact mutation, the observed exit and the reason, which is what let us fix the class and not just the instance. Thanks as well to @carloshvp, @noah-ing and @dinakarjs for the earlier rounds. Every one of these has made xChk better. Head
|
…. Since e7b3283 a required ML-DSA-65 half that cannot be checked is a strict-mode refusal, not a pass, so a test environment without the post-quantum verifier fails 13 tests for the wrong reason. dilithium-py joins the [test] extra the CI installs, tests/conftest.py stops before the first test with the reason and the install line, and the README says so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ot declared by the bundle (2 Nov 2026; review of agentrust-io#350 by drk-ram). He re-signed fixture 01 under his own Ed25519 and ML-DSA-65 keys, rewrote the pinned key list and the root statement's list to match, and --require-approval still exited 0 while the assessor said approval-confirmed: every key the verifier checks with is the one the bundle carries, so every check proved consistency under the bundle's own keys. CROSSWALK step 4 said as much; the verdict name said more. - verify_receipt.py (vendored, as the registry now serves it): --keys xchk-keys.json, the registry's key document saved once from /.well-known/xchk-keys.json. Every key the bundle pins must be in it, active or retired, by alg, keyId and raw key, and the bundle's root key must be the document's root; an unlisted or revoked key is a FAIL that names the key. A document that cannot be read is exit 2. The --json report carries keyDocument (null when none was supplied, else {registry, servedAt, allPinned}). Without --keys the verdict is unchanged and the transcript says the keys were checked against themselves only. - The assessor pins the registry's key document (examples/approval-outcome/xchk-keys/, as served on 2 Nov 2026) and runs the verifier with --keys; a bundle whose keys are not in it, or a relying party that pinned none, gets approval-consistent-key-not-pinned, never approval-confirmed. Outcome field registry_keys_pinned added. - Fixture 09: 01 re-signed under keys that are not the registry's (tests/rekey_fixture.py rebuilds it; cryptography + dilithium-py). Its Trust Record is 01's. Its line in expected.json, and registry_keys_pinned on every case, were written by hand: the generator runs on the registry and was not re-run. - verdict-contract.json: three rows that run with --keys, each pinned by its FAIL line; byte-identical to xchk-app's verify/verdict-contract.json. - CROSSWALK.md, README.md and the fixtures README say what confirmed now means and what consistent means. Rebased onto round two (d0c0f38); the vendored verify_receipt.py stays the xchk-app owner byte for byte (71b2fd9ea there). Tests: see the next line. Companion change in xchk-app (verify/verify_receipt.py, verify-receipt.html, examples/agt-xchk-adapter/verify_receipt.py), same date. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Tests after the rebase: 151 passed, 1 skipped in 133.79s (0:02:13), with dilithium-py; ruff (E4,E7,E9,F, py39) clean.
…2026). - verify_receipt.py (still the xchk-app owner byte for byte): a revoked key is refused however the document says it (the `revoked` list, a `revokedAt`, or a `status` other than active or retired left in place in `keys`), and a revoked entry published without its public key, as the registry serves them, is matched by alg and key id, so a revocation never reads as "never published". The right key id under a different public key fails by that reason. A bundle carrying a root key when the document publishes none is a failed check, not a skipped one, and allPinned is false on a root-only mismatch. A document whose entries are not keys is unreadable (exit 2); a copy saved with a byte-order mark loads. - Stated as limits, in CROSSWALK.md and the fixtures README: a retired key's notBefore/notAfter are not compared with the seal's time; the document's `registry` is never compared with the bundle's `wellKnown` (only key material decides); a bundle that pins no key leaves allPinned null. - Assessor: a pinned document the verifier cannot read is an assertion error naming the document, not a TypeError. - expected.json: case 09 in the generator's field order. The fixtureNote in 09 and in rekey_fixture.py is the same text in both repositories and names no path. - Tests: the three revocation shapes, the different-key case, the root-only mismatch and the missing root, the BOM and non-key document shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Head
Same checks as the last head: an independent read of the change, a guard-mutation sweep (survivors unchanged: the identity-provider guard and the anchor chain, an anchored fixture for which is in progress), ruff clean, 155 tests. The vendored verifier is byte-identical to the served one. |
…get a real anchored receipt to pin them (2 Nov 2026). A guard-mutation sweep (each ok( condition replaced by True, then the suite) found the whole Ravencoin and OpenTimestamps anchor chain unpinned: the manifest path to the anchored root, the OP_RETURN carrying that root, sha256d(tx) = txid, the merkleblock header against the shipped header, the partial Merkle tree, and the 32-byte OTS root. No fixture carried anchor.proof: the 17 Oct cases predate the registry's batched anchoring (22 Oct). examples/approval-outcome/anchored.bundle.json is a receipt approved by the same person on 29 Oct 2026, fetched from the registry's bundle endpoint on 2 Nov 2026 exactly as served: a batched anchor with a 5-step manifest path (1 of 32 seals), the raw transaction, header and merkleblock, and an OTS proof upgraded to Bitcoin block 973698. It is not a crosswalk case and expected.json does not list it (the fixtures test globs 0*.json). A contract row may now name its own fixture; seven rows do, one per guard, each pinned by its FAIL line. Three need an op because a middle-character swap lands on a neighbour: the OP_RETURN root inside the transaction, the first branch hash of the merkleblock (its header is 120 bytes), and the final sha256 of the OTS proof retagged as sha1. verdict-contract.json, the fixture and verify_receipt.py are the xchk-app owner byte for byte. Not pinned by this: the unbatched OP_RETURN seal-hash branch (no unbatched receipt carries anchor.proof) and the identity-provider guard, the sweep's expected survivor. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
noah-ing
left a comment
There was a problem hiding this comment.
Reviewed current head 9feb3c27ec863ce4dae94b872f183d4455eb104e. I reran the 155 committed integration tests (1 opt-in live test skipped), the repository's Ruff selection, dependency consistency, and diff whitespace checks. Independent controls with and without the optional post-quantum verifier show that the earlier unchecked-signature-role case no longer produces a confirmed approval on this head.
I also reproduced one remaining fail-closed verifier-input case in a synthetic control. I sent the exact input, observed result, scope limits, and requested regression to the maintainers. Please keep this PR blocked until they resolve that case or document a reasoned disposition, then re-review the final head. This is an offline finding in the unmerged verifier; it does not establish a live registry failure or a forged TRACE record.
|
Thanks @noah-ing for the quick turnaround on |
…fixture (2 Nov 2026). The guard-mutation sweep's last expected survivor was the check that approver.identity (the readable copy) equals the identity-provider trio inside the registry-signed binding. anchored.bundle.json carries it: the receipt was approved while Entra vouched for the approver, so its binding names identityProvider, tenantId and subjectId. Two contract rows tamper approver.identity.subjectId and .tenantId on it, each pinned by the guard's FAIL line, and the strict-mode test asserts the guard ran OK on the fixture. With the guard's condition replaced by True exactly those two tests fail; restored, 165 passed, 1 skipped (was 155 at 9feb3c2, 163 with the anchor rows). Still unpinned, stated: the unbatched OP_RETURN seal-hash branch. Every receipt the registry serves that carries anchor.proof is batched (the 17 Oct cases were re-anchored into the 22 Oct batch), so no genuine unbatched fixture exists to commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Head
Still unpinned, stated rather than claimed: the unbatched OP_RETURN seal-hash branch. Every receipt the registry serves that carries 165 passed, 1 skipped (was 155). Ruff on the two edited files reports the same findings as before them. The vendored verifier is unchanged from |
drk-ram
left a comment
There was a problem hiding this comment.
Follow-up verification of exact head 6991a57de57fa646416f1f81f047c3509d9d7b09, covering the changes since my review of 7020d82, including 9feb3c27ec863ce4dae94b872f183d4455eb104e, a59f780 and 6991a57.
What is resolved
My item 1 — relying-party key pinning: resolved for the documented offline model. --keys <xchk-keys.json> checks the bundle's keys against the separately selected registry document by algorithm, key identifier and raw public key, and compares a supplied bundle root against the document root. The committed re-keyed case 09 can remain self-consistent under its own keys, but supplying the genuine registry document makes verification fail and prevents approval-confirmed. The assessor returns approval-consistent-key-not-pinned both for that outside key set and when no document was pinned. The genuine pinned control still confirms. Revoked keys, revoked entries without raw bytes, different raw bytes under the same identifier, unreadable documents and root mismatches have committed refusal controls. Current code also refuses a bundle root when the selected document publishes no usable root; it does not silently call that pinned.
My verifier-guard coverage finding: the original thirteen named verifier guards now have effective contract controls. I independently replaced each named ok(...) condition with True in memory and executed the corresponding committed regression against the genuine fixture and mutant. The original thirteen are killed, including identity-provider agreement, binding record/credential linkage, assertion signature, UV, ceremony type, root key listing, Merkle path, seal hash, prompt, key-id recomputation, profile hash and unsigned profile.
For the newly anchored fixture, the manifest path, batched OP_RETURN root, transaction hash, merkleblock/header equality, partial Merkle tree and 32-byte OpenTimestamps replay-root controls are also killed. Across those original and added controls, 19 distinct conditions were killed by 25 contract cases. Checking the guard's own FAIL line matters here: changing the OP_RETURN payload also breaks the transaction hash, so an exit-code-only assertion would not establish OP_RETURN coverage. Subject and tenant alterations similarly kill the identity-provider guard on the genuine anchored receipt.
There is a counting boundary to preserve: the fixture pins the batched OP_RETURN branch. The separate unbatched seal-hash OP_RETURN branch at verify_receipt.py:999 has no corresponding committed fixture/contract row and remains outside this coverage. The author's latest reply discloses that limit. I would not summarize this as every anchor branch being pinned.
The UTF-8 output regression, required hybrid-signature halves and withholding of derived identity/amendment fields on nonzero exits also pass the committed suite.
What is not resolved
@noah-ing's review of 9feb3c2 reports a separate fail-closed verifier-input case sent privately to the maintainers and explicitly asks that this PR remain blocked pending a fix or reasoned disposition. I have not received that reproducer and am not representing this review as an independent reproduction or clearance of it.
The subsequent two commits do not change verify_receipt.py: a59f780 adds the anchored fixture and anchor contract coverage; 6991a57 adds the identity-provider controls on that fixture. The author's 13:52:57Z reply explicitly says the privately reported input case has not reached them. I found no subsequent disposition on this PR. Those commits therefore do not close that outstanding review item.
Please coordinate that reproducer through the existing private channel and either fix the affected input handling with a regression on the final head, or record a reasoned maintainer disposition sufficient for the reporting reviewer to reassess it. No sensitive payload needs to be posted publicly. Until then, I am maintaining a request-changes verdict, while recording the verified closures above.
Verification
Detached checkout of the exact reviewed head, Windows/Python 3.11, with the required dilithium-py dependency available:
python -m pytest integrations\xchk-registry\tests -q --tb=short -o addopts= --basetemp ..\.followup-review-20261102\pytest-xchk: 165 passed, 1 skipped, 250.05 seconds. The skipped case is the opt-in live check; this was an offline run.python -m ruff check --target-version py39 --select E4,E7,E9,F integrations\xchk-registry: passed.git diff --check 7020d82..HEAD: passed.- Independent in-memory condition-bypass checks: 19 killed, with the unbatched branch explicitly not covered.
These results establish the stated offline evidence and regression boundaries, not live registry behavior, chain membership confirmation, key-validity-window enforcement or clearance of the separately reported input case.
The crosswalk, fixtures and verifier for xChk sealed receipts as
approval-outcomereferences, moved here from agentrust-io/trace-spec#560 at the maintainers' direction (a crosswalk for one registry's receipts belongs with that registry's integration). This is the replacement for the part of closed #337 that #338 deliberately left out.Stacked on #338. This branch is #338's head plus the files below; until #338 merges, the diff shows #338's files too. The new files are all under
integrations/xchk-registry/:CROSSWALK.md: the field mapping, verifier obligations and boundary, from #560, with two verifier rules added (below).examples/approval-outcome/: the eight Trust Records and seven bundles from #560 (one case per way the composition can fail),README.md,expected.json.verify_receipt.py: the registry's offline receipt verifier, vendored as served (MIT, listed inNOTICE), at the current version.tests/test_approval_outcome_fixtures.py: #560's fixture test, reworked: a finite explicit age bound for the historical records (neverNone); the approval verdict requires the approval evidence to have been present and checked, not only "nothing failed"; mutation tests for missing evidence, for an assertion from another ceremony by the same credential, and for unsigned outer labels.tests/test_verify_receipt_regressions.py: the verifier's own regressions on the same two real bundles.NOTICE,README.md,pyproject.toml(test extras): updated. #560'sADOPTERS.mdline is folded into the README (this repository has no ADOPTERS file).Two verifier rules, with regressions:
ask-v1, theattestationIdthe passkey signed over must equal the one in the sealed record and the one in the registry-signed binding, not only the unsigned outerapprover.attestationId. Without this, a genuine answer by the same credential to a different ask, placed beside this record's genuine binding, passed every check. Refused now, on exactly those two checks.--require-approvalexits 3 unless the approval evidence (approver block, assertion with key, signed binding, ask-v1 challenge) was present and checked. The fixture test uses it for the approval verdict; a bundle stripped of its approver isapproval-unverified, never confirmed.Not in this PR, by design: a packaged relying-party
assess()for this integration. The composition logic lives in the fixture test as the worked example. A packaged verdict with structured verifier results, identity taken only from the signed binding, and an authenticated amendment scope is under the repository's private assessment and follows in this directory once settled; this PR carries that assessment as its dependency and should take current-head review.decision.amendmentis informational until then, as the crosswalk's boundary states.Checked locally (head
7020d82, 20 Oct 2026):ruff --target-version py39 --select E4,E7,E9,Fclean;pytest integrations/xchk-registry/tests82 passed, 1 skipped (the live key fetch) on Python 3.11 with agentrust-trace 0.11.0 and agentrust-trace-tests 0.6.2; lychee with links.yml's arguments 0 errors; the vendored verifier is byte-identical to the served one and its tests are generated from a written contract of what each piece of evidence must do when removed or altered; each new guard disabled in turn, its regression fails.🤖 Generated with Claude Code