Repository navigation
feat: ActionIntent adopts CNP-0-JCS (Trinity half) - #27
Conversation
Trinity half of the joint adoption slice. Pairs with myc `ec6a754e`.
ccc26b8b460fe2debf0ad069d55ec170a78b7b70861f1f54c03e401e4576c3be
The vendored contract is replaced wholesale with MYC's current one — the shared
domain check, the closed member set, set semantics for requested_effects, and
the CNP-0-JCS encoder — so the two copies are byte-identical where it matters.
**A LIVE parity test replaces two matching constants.** `src/action_intent_parity_test.ts`
imports MYC's module when the submodule is present and compares canonical TEXT,
canonical BYTES, then digests, over eight inputs chosen for where two
implementations could plausibly diverge: astral scalars (UTF-16 vs code-point
ordering), duplicated effects, both permutations, empty arrays, escapes, BMP
non-ASCII. It also asserts both boundaries refuse the same out-of-domain values
and that neither reaches a digest.
Two constants in two files prove the constants match. Verified the test detects
real drift: renaming MYC's `numeric_profile` value turns it red on canonical
text. Verified it skips honestly with the submodule absent — core CI stays
self-sufficient, and a skipped parity check is never counted as passing. CI gates
that: with submodules present, a SKIPPED line fails the job, so the gate cannot
pass vacuously.
**The CLI validates runtime input fail-closed.** `warrant intent` and
`warrant admit --intent` parsed JSON and cast to ActionIntent. Parsing says the
bytes are JSON; it says nothing about whether they are an intent. Both now run
`validateIntent` and report the reason instead of letting a bare RangeError reach
the caller.
**The superseded digest is an `intent_mismatch` at the gate**, not a near miss —
that is the shape a stale grant takes when there is no dual-hash path.
**The canonical bytes are judged by the ratified verifier-only path**
(`probes/cnp-0-seed-v0/ts/reject.ts`), which shares no code with the encoder and
cannot repair what it reads. Checking them against our own encoder would only
say the encoder agrees with itself. The check is discriminating: the same value
with members in source order is rejected.
Scope held: the proposal-body `stable()` in this same file is deliberately NOT
migrated and now says so — it recomputes a commitment MYC already wrote with that
stringification, so changing it alone would reject every existing proposal.
Receipt envelope, registry amendments and effectVerdictHash untouched. No status
claim moved.
The submodule pin is the myc candidate for joint review; it becomes the myc merge
SHA mechanically once that side lands.
./t check READY: 579 tests, 367 chords valid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An
x8788_network reflects the new cross-substrate parity surface. Separate commit from the adoption itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An
PR Summary by QodoAdopt CNP-0-JCS for ActionIntent commitments
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
1. Parity gate never runs
|
| - name: Verify ActionIntent cross-substrate parity exercised | ||
| if: steps.subm.outputs.present == 'true' |
There was a problem hiding this comment.
1. Parity gate never runs 🐞 Bug ☼ Reliability
The new parity step is guarded by steps.subm.outputs.present == 'true' inside verify, but that job explicitly checks out with submodules: false, so the condition is always false and implementation drift can merge undetected. The separate cross-substrate job initializes MYC but never runs this test.
Agent Prompt
## Issue description
The live ActionIntent parity gate is unreachable because it is conditional on submodules being present in the `verify` job, while that job deliberately checks out without submodules.
## Issue Context
The `cross-substrate` job initializes all public submodules and sets up Deno, making it the effective location for this mandatory adoption gate. Run the parity command there unconditionally after initialization, retaining the checks that reject `SKIPPED` output and require all four tests to pass.
## Fix Focus Areas
- .github/workflows/ci.yml[155-166]
- .github/workflows/ci.yml[321-365]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Joint gate — round 1/2: changes requestedReviewed exact candidates:
Positive evidence reproduced:
Three blockers remain. Fix only these; the next review is round 2/2. P1 — raw JSON destroys forbidden evidence before validationBoth CLI boundaries call Executed duplicate-member attack: {"verb":"deny","verb":"apply","target_substrate":"myc",
"args_commitment":"c1","input_commitments":["a","b"],
"requested_effects":["receipt","write"]}Results:
Executed invalid-UTF-8 attack: byte
This violates strict I-JSON and the ratified duplicate-member / invalid-UTF-8 rejection classes. Required:
Do not broaden this into a general repository JSON migration. P1 — the shared check re-reads mutable input
Executed reproducer: an enumerable Actual output: {"args_commitment":"c1","canonical_encoding":"hsp-jcs@v0",
"input_commitments":["a"],"numeric_profile":"cnp-0",
"requested_effects":[1],"target_substrate":"myc","verb":"apply"}
Required: replace check-then-reread with one normalization/snapshot operation that validates and returns owned scalar/array values; encoding must use only that snapshot. Add this exact changing-getter control on both vendored copies or in live parity. This is the boundary of the finding; no proxy/same-process sandbox project is requested. P1 — stored proposal and authority comparison are not one E2EThe current Myc E2E proves storage. Trinity tests the authority helper on a constructed descriptor. No test feeds the proposal Myc actually wrote into Trinity's authority comparison, despite campaign #26 requiring the stored proposal/grant and authority comparison in one path. Required live test when Myc is present:
The submodule-absent case may remain explicit SKIPPED; the existing CI non-vacuity gate must cover this live E2E too. Packaging/status closure
Return one delta-only packet with both new exact SHAs, PR links, the five executed attacks (duplicate, escaped duplicate, invalid UTF-8, changing getter, stored cross-substrate E2E), and green suites. No receipt yet. |
Three P1s from the joint gate round 1/2. All reproduced before fixing.
**Raw JSON erased forbidden evidence before validation.** `warrant intent` and
`warrant admit --intent` did `JSON.parse(await Deno.readTextFile(...))`, which
collapses duplicate member names last-wins and replaces invalid UTF-8 with
U+FFFD. `{"verb":"deny","verb":"apply",...}` emitted ccc26b8… at exit 0.
Both boundaries now take raw bytes through the vendored
`parseActionIntentBytes`: UTF-8 decoded fatally, duplicate top-level member names
detected before `JSON.parse` can collapse them — names decoded first, so
`"verb"` cannot smuggle a second `verb` past the check — then the closed
domain normalization.
These are the rejection classes §5.1.1 rule 4 exists for, and this project's own
conformance kit says they are "properties of the input bytes that a permissive
JSON parser resolves before you can see them". The authority boundary was using
a permissive parser.
**Check-then-reread.** `normalizeIntent` now reads every property exactly once
into owned copies, arrays included, and the encoder uses only that snapshot. A
getter answering the validator `["write"]` and the encoder `[1]` produced
canonical bytes containing a JSON number; it cannot now, on either side.
**The stored proposal and the authority gate were never one path.** MYC proved
storage; Trinity tested authority on a descriptor built in the test. The new live
E2E runs MYC's `propose --action-intent` into a temp root, reads what was written
to disk, computes the intent through Trinity, and passes the STORED descriptor to
`actionBoundAuthority` — asserting the current commitment authorizes and the
superseded one is `intent_mismatch`. It reads the grant where `readProposal`
reads it (`body.action_grant`, `commitment.value`), because a test that invents
its own shape passes while the real path finds nothing.
Verified by drift: renaming MYC's numeric_profile value turns both the byte
parity AND the stored-grant E2E red. Verified the submodule-absent path still
reports SKIPPED, and CI already fails the job if SKIPPED appears while submodules
are present.
**Status disposition.** `adoption-evidenced: true` in Part 00, Part 01 §5.1.3 and
Part 07 §18 — scoped to ONE named path, with the same entries saying the
proposal-body digest and every other `stable()` still use the old
stringification, so nothing may read it as "Trinity computes references under
hsp-jcs@v0". `interop-confirmed` stays false: both implementations are under one
maintainer, which is what that level exists to distinguish. A3 is untouched.
Part 07 §18 records the five executed attacks alongside the adoption, because a
state that records only the success teaches nothing.
Pairs with myc e02d7f98637c1ea7fcc793c7023e3e015bbecf7e.
./t check READY: 582 tests, 367 chords valid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An
|
Codex final-round acceptance of the implementation at 2db7056. I replayed the five bounded attacks and both targeted suites, then ran full Trinity (582) and standalone MYC (273) checks. One editorial acceptance defect remained: Part 00 simultaneously reported adoption-evidenced as false and true in several current-status passages. I corrected only those stale status lines and mechanically repinned the MYC submodule from the accepted PR head e02d7f9 to its tree-identical merge commit b1e94b0. New Trinity head: 87274a2. No implementation bytes changed after acceptance; interop-confirmed remains false. |
Trinity half of the joint ActionIntent adoption gate. Pairs with myc
ec6a754e9c7f85a45efbe98aefd7a3bee9a955ae(myc PR #1 rebuilt onmain@2fbcdbaf). Review together per issue #26; do not merge before the myc side.Trinity candidate:
a92addf46164b4d51d169f0557b23e62002e5815Adoption commit:
fdf76b52a26f16117efa6be1eb170bc1c1e2d55a(projections separate)Findings closed on the myc side (
ec6a754e)validateIntent(requested_effects:[1]got a digest)domainError, called by both. 12 direct-bypass controls, each requiring encoder and digest path to throw["write","write"]≡["write"]; a different set is still a different commitmentVector digest unchanged by all three.
Trinity half
Live parity replaces two matching constants.
src/action_intent_parity_test.tsimports MYC's module when the submodule is present and compares canonical TEXT, canonical BYTES, then digests, across 8 inputs chosen for plausible divergence: astral scalars (UTF-16 vs code-point ordering), duplicated effects, both permutations, empty arrays, escapes, BMP non-ASCII. Plus: both boundaries refuse the same out-of-domain values and neither reaches a digest.Counterexamples run, not asserted:
CI gates the vacuous case: with submodules present, a
SKIPPEDline fails the job.CLI validates runtime input fail-closed.
warrant intentandwarrant admit --intentparsed JSON and cast. Both now runvalidateIntentand report a reason instead of letting a bareRangeErrorreach the caller.Superseded digest →
intent_mismatchat the authority gate.Canonical bytes judged by the ratified verifier-only path (
probes/cnp-0-seed-v0/ts/reject.ts— shares no code with the encoder, cannot repair what it reads). Checking against our own encoder would only prove it agrees with itself. Discriminating: the same value in source member order is rejected.Profile identifiers are load-bearing — removing either from the hashed root moves the digest, tested.
Boundaries
stable()inx5E10is not migrated and now says why: it recomputes a commitment MYC already wrote with that stringification, so changing it alone rejects every existing proposaleffectVerdictHashuntouchedadoption-evidencedandinterop-confirmedstayfalsein this PRChecks