Skip to content

integrations: xChk registry — approval-outcome crosswalk, fixtures and receipt verifier (from trace-spec#560; stacked on #338) - #350

Draft
minitru wants to merge 7 commits into
agentrust-io:mainfrom
minitru:xchk-approval-outcome-crosswalk
Draft

minitru wants to merge 7 commits into
agentrust-io:mainfrom
minitru:xchk-approval-outcome-crosswalk

Conversation

@minitru

@minitru minitru commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

The crosswalk, fixtures and verifier for xChk sealed receipts as approval-outcome references, 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 in NOTICE), at the current version.
  • tests/test_approval_outcome_fixtures.py: #560's fixture test, reworked: a finite explicit age bound for the historical records (never None); 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's ADOPTERS.md line is folded into the README (this repository has no ADOPTERS file).

Two verifier rules, with regressions:

  1. With rule ask-v1, the attestationId the passkey signed over must equal the one in the sealed record and the one in the registry-signed binding, not only the unsigned outer approver.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.
  2. The verifier's exit 0 means no check failed; pending steps are listed, not passed. --require-approval exits 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 is approval-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.amendment is informational until then, as the crosswalk's boundary states.

Checked locally: ruff --target-version py39 --select E4,E7,E9,F clean; pytest integrations/xchk-registry/tests 57 passed, 1 skipped (the live key fetch) on Python 3.11.14 with agentrust-trace 0.11.0 and agentrust-trace-tests 0.6.2; the vendored verifier swept with every top-level, approver, binding, seal, merkle and decision field removed in turn, default and strict, with no traceback (one found and fixed: a missing record hash was a crash, now a failed check).

🤖 Generated with Claude Code

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread integrations/xchk-registry/xchk_trace_record.py Fixed
minitru added a commit to minitru/integrations that referenced this pull request Oct 19, 2026
…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>
minitru added a commit to minitru/integrations that referenced this pull request Oct 19, 2026
… 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>
minitru added a commit to minitru/integrations that referenced this pull request Oct 19, 2026
…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>
@minitru
minitru force-pushed the xchk-approval-outcome-crosswalk branch from 6013421 to 9b1622e Compare October 19, 2026 19:36
@minitru

minitru commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, @dinakarjs. Head is 9b1622e, restacked on #338's 7646077. Your two documentation corrections, as written:

  • Links. Both fixed: the specification link points at the public TRACE v0.2 document, and the test link is tests/test_approval_outcome_fixtures.py. lychee with the repository's own arguments over integrations/xchk-registry: 20 OK, 0 errors.
  • Amendment. Verifier obligations 5 and 6 and the conformance-fixture description now say decision.amendment is informational and its scope is not yet authenticated. Step 6 compares the executed call with record.paramsHash only. The 07 label, "approval confirmed, as amended", is described as a worked example under the stated limitations, in CROSSWALK.md and the fixture README, until the private assessment settles the relying-party contract. The boundary bullet returns to that statement and keeps its limits as limits. The PR description already carries the assessment as a dependency and is unchanged.

Also on this head since 7523105, for the record: the verifier refuses an amendment the sealed record does not commit to (a refusal only, and described as one); --json structured output; malformed-type inputs end in a verdict rather than a traceback; the CodeQL alert, in the same two files as #338; and tests for each. 73 passed, 1 skipped under Python 3.11; ruff with the repository's flags clean. The vendored verify_receipt.py is byte-identical to the one the registry serves. The draft stays a draft pending the assessment.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

imran-siddique pushed a commit that referenced this pull request Oct 19, 2026
… 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>

@noah-ing noah-ing left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

minitru and others added 6 commits October 19, 2026 19:47
…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>
@minitru
minitru force-pushed the xchk-approval-outcome-crosswalk branch from 9b1622e to b43b884 Compare October 19, 2026 23:55
@minitru

minitru commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, @noah-ing. Head is b43b884, rebased on main now that #338 is merged. Main's #338 files are identical to the base this was built on, so #350 is its own six commits.

You were right: --require-approval established the approval evidence, not the seal chain. The changes:

  • Verifier: the --json report gains sealChainMissing, naming what is absent: the seal, the Merkle path from the record to it, or the registry's signature over it. Strict mode now exits 3 when it is non-empty, with result seal-chain-incomplete, kept apart from approval-evidence-missing, whose wording is unchanged. The default run is unchanged.
  • Fixture verdict: an approved record with its approval evidence intact and its seal chain incomplete is approval-unsealed, distinct from approval-unverified and from approval-confirmed.
  • Crosswalk: the "absence of failure is not confirmation" rule now states the seal-chain half.
  • Regressions: your four deletions from 01 (seal, Merkle proof, both, the registry's seal signature) at the verifier (test_approval_evidence_without_the_seal_chain_is_not_a_sealed_approval) and through the fixture verdict (test_an_approval_without_its_seal_chain_is_not_confirmed, four parametrized cases). The sealed control still confirms.

Run on b43b884:

  • pytest integrations/xchk-registry/tests: 78 passed, 1 skipped, Python 3.11.
  • ruff check integrations/xchk-registry --target-version py39 --select E4,E7,E9,F: clean.
  • lychee over the directory with the repository's arguments: 0 errors.
  • Beyond your four, through the real CLI: the Merkle proof absent or not a list, an empty seal object, and blanked signatures all exit 3 in strict mode. Removing the whole registrySignature block fails outright, since the profile and binding signatures can no longer be checked either.
  • Every row of your earlier matrix keeps its verdict. A type-mutation sweep over every field of three bundles, 14,625 mutations in both modes, ends in 0 tracebacks.
  • The verifier is byte-identical to the one the registry serves at in.xchk.io and xchk.me, deployed with this change. Six live receipts approved with changes all carry a complete seal chain, so none is newly refused.

The draft stays a draft pending the private assessment, and the hosted workflows still need a maintainer to authorize them.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

minitru commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

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 2e1da68, one commit on b43b884:

  • No verdict from a record that does not verify. _assess() returns record-invalid before reading any bundle when the Trust Record fails verification, with or without a bundle. The crosswalk's step 1 now says the same: nothing an invalid record carries, the reference included, is evidence.
  • Regression. The intact 01 record confirms. The same record with one signed field changed, bundle and reference untouched, is record-invalid.
  • Verifier section 5b. The vendored verify_receipt.py is the served one again, byte for byte: the decision block must cite the sealed record's attestationHash. Its regression uses another real ceremony's value.

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 record-invalid), the verifier's contract tests and 16 live receipts.

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

This branch has not been deployed

No deployments
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.

4 participants