Skip to content

feat(x5820): ActionIntent commits over CNP-0-JCS canonical bytes - #4

Merged
s0fractal merged 2 commits into
mainfrom
spec/action-intent-v2
Aug 26, 2026
Merged

s0fractal merged 2 commits into
mainfrom
spec/action-intent-v2

Conversation

@s0fractal

Copy link
Copy Markdown
Owner

Myc half of the joint ActionIntent adoption gate, round 2/2. Replaces PR #1, which pointed at 468ae52 on an old base and was CONFLICTING.

Myc candidate: e02d7f98637c1ea7fcc793c7023e3e015bbecf7e
Trinity candidate: 2db70569551a186b106765e03d00bed2bf2c05ef — trinity#27

Base main@2fbcdbaf. Three ActionIntent files plus the strict-intake wiring in x5800_propose.ts.

Round-1 P1s closed

Raw JSON erased forbidden evidence. Both boundaries now take raw bytes: UTF-8 decoded fatally, duplicate top-level member names detected before JSON.parse collapses them, names decoded first so "verb" cannot smuggle a second verb.

Executed, at the CLI, asserting on disk:

duplicate verb        proposals_written=0  duplicate-member-name: "verb" appears more than once
escaped duplicate     proposals_written=0  duplicate-member-name: "verb" appears more than once
0xff inside verb      proposals_written=0  invalid-utf8: the input is not well-formed UTF-8

Check-then-reread. normalizeIntent reads every property exactly once into owned copies (arrays copied, not aliased); the encoder uses only that snapshot. Controlled for four getter schedules: no canonical text ever contains a non-string effect, and any digest produced equals a validated value's.

Cross-stored E2E lives on the Trinity side (it needs both) — see trinity#27.

Boundaries

No general JSON migration; the strict path is used by --action-intent only. No proxy sandbox. No other stable() touched. No status claim in this PR.

standalone  deno task check green
targeted    x5820 10/10, x5800 10/10

s0fractal and others added 2 commits August 26, 2026 23:54
Rebuilt on main@2fbcdbaf. Three files, no unrelated commits.

    {"args_commitment":"c1","canonical_encoding":"hsp-jcs@v0",
     "input_commitments":["a","b"],"numeric_profile":"cnp-0",
     "requested_effects":["receipt","write"],"target_substrate":"myc",
     "verb":"apply"}
    → ccc26b8b460fe2debf0ad069d55ec170a78b7b70861f1f54c03e401e4576c3be

RFC-0003 Part 01 §5.1, Tranche A3. Chosen because it is an authority gate:
actionBoundAuthority permits actuation only on an exact commitment match.

**One domain check, not two.** `domainError` is called by `validateIntent` AND
by the encoder. The previous version repeated only the surrogate check inside
the encoder while calling itself "refusing anything else" — it encoded
`requested_effects: [1]` as the JSON number 1 and returned a digest. Two guards
meant to agree, written twice, disagree.

**`requested_effects` is a set: deduplicated then ordered.** Sorting alone was
not set semantics; `["write","write"]` and `["write"]` request the same effect
and produced different commitments, letting a grant depend on how many times a
caller named an effect. Duplicates are canonicalised away rather than rejected —
a repeated effect is the same ask written twice, not a malformed ask.

**Unknown members are rejected, not dropped.** Silently discarding one lets two
callers commit to one digest believing they asked different things.

No dual-hash: d02d75ad… is not produced and a test asserts it. No commitment was
ever persisted.

Twelve direct-bypass controls, each requiring the encoder AND the digest path to
throw: wrong array member type in both arrays, invalid substrate, empty and
whitespace verb, malformed array field, extra member, missing member, non-object,
array, null, lone surrogate.

Bytes pinned, not only digests. BMP, non-BMP (a surrogate pair is one scalar and
is legal), lone-surrogate rejection in every string position. End-to-end on what
lands on disk: `propose --action-intent` writes the new commitment; an intent
with a lone surrogate exits 1 and writes nothing.

Standalone 261 passed / 0 failed; nested `deno task check` green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An
…fuse

Three P1s from the joint gate. All reproduced before fixing.

**Raw JSON erased forbidden evidence before validation.** Both CLI boundaries
did `JSON.parse(await Deno.readTextFile(...))`, which collapses duplicate member
names last-wins and replaces invalid UTF-8 with U+FFFD. So
`{"verb":"deny","verb":"apply",...}` wrote a proposal for an intent that also
said `deny`, and a `0xff` byte inside `verb` committed to a replacement
character nobody wrote.

RFC-0003 §5.1.1 rule 4 exists for exactly this, and the conformance kit's own
INTERFACE.md says three rejection categories "describe properties of the input
BYTES that a permissive JSON parser resolves before you can see them". I wrote
that sentence and then used a permissive parser at an authority boundary.

`parseActionIntentBytes` decodes UTF-8 fatally from bytes, scans the top-level
object for a repeated member name BEFORE `JSON.parse` can collapse it, and only
then normalizes. Names are decoded before comparison, so `"verb"` cannot
smuggle a second `verb` past the check.

    dup       proposals_written=0  duplicate-member-name: "verb" appears more than once
    dup-esc   proposals_written=0  duplicate-member-name: "verb" appears more than once
    bad-utf8  proposals_written=0  invalid-utf8: the input is not well-formed UTF-8

**The shared check re-read mutable input.** `domainError(intent)` validated the
properties and `canonicalIntentText` read them again, so an enumerable getter
answered the validator with `["write"]` and the encoder with `[1]` — producing
canonical bytes containing a JSON number, and a digest over them.

`normalizeIntent` is now one operation that reads every property EXACTLY ONCE
into a local and returns owned copies; arrays are copied rather than aliased;
encoding uses only that snapshot. Controlled for four getter schedules: no
canonical text ever contains a non-string effect, and any digest that is produced
equals the digest of a validated value.

Scope held: no general JSON migration, no proxy sandbox. The strict path is used
by `--action-intent` only.

Standalone and nested checks green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DRHMVB9rUYNggn4wLmc1An
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Adopt CNP-0-JCS commitments for ActionIntent authority grants

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Hash ActionIntents over versioned CNP-0-JCS canonical bytes.
• Reject ambiguous or malformed byte inputs before proposal creation.
• Pin canonical vectors and fail-closed authority behavior end to end.
Diagram

graph TD
  A["Intent bytes"] --> B["Strict intake"] --> C["Owned snapshot"] --> D["JCS bytes"] --> E["SHA-256 digest"] --> F["Proposal artifact"]
  B -->|"rejects ambiguity"| G["Fail closed"]
  C -->|"closed schema"| G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a duplicate-aware JSON parser
  • ➕ Avoids maintaining a custom lexical scanner
  • ➕ Can enforce duplicate rules throughout arbitrary nested JSON
  • ➖ Adds a dependency to a standalone authority path
  • ➖ Provides broader behavior than the closed ActionIntent schema requires
  • ➖ Still needs strict UTF-8 handling and domain normalization
2. Adopt a general CNP-0-JCS encoder
  • ➕ Centralizes canonicalization for future protocol migrations
  • ➕ Can reuse broader conformance vectors across domains
  • ➖ Expands this tranche beyond ActionIntent
  • ➖ Introduces unneeded numeric and generic JSON surface into an authority gate
  • ➖ Raises review and migration risk for unrelated stable encoders

Recommendation: Keep the scoped, domain-specific encoder and strict byte boundary for this adoption gate. The closed string-only schema, shared canonical-byte vector, and end-to-end persistence tests make the narrow implementation preferable now; a general parser or encoder should be introduced separately only when multiple domains need it and full conformance coverage is available.

Files changed (4) +812 / -89

Enhancement (1) +360 / -53
x5820_action_intent.tsImplement strict CNP-0-JCS ActionIntent commitments +360/-53

Implement strict CNP-0-JCS ActionIntent commitments

• Replaces ad hoc stable stringification with versioned CNP-0-JCS canonical UTF-8 bytes and SHA-256 hashing. Centralizes validation into a single owned normalization, closes the schema, canonicalizes requested effects as a set, rejects unpaired surrogates, and adds strict raw-byte parsing for duplicate names and malformed UTF-8.

src/x5820_action_intent.ts

Bug fix (1) +7 / -4
x5800_propose.tsRead ActionIntent input as strict raw bytes +7/-4

Read ActionIntent input as strict raw bytes

• The proposal CLI now reads ActionIntent files as bytes and delegates to strict parsing before computing an action grant. Invalid UTF-8, duplicate members, and other rejected inputs stop proposal creation instead of being normalized by permissive text decoding or JSON parsing.

src/x5800_propose.ts

Tests (2) +445 / -32
x5800_propose_test.tsVerify committed grants and fail-closed CLI intake +182/-1

Verify committed grants and fail-closed CLI intake

• Adds end-to-end assertions that the CNP-0-JCS commitment is persisted and the superseded digest is absent. Exercises unpaired surrogates, literal and escaped duplicate names, and invalid UTF-8 while confirming no proposal is written.

src/x5800_propose_test.ts

x5820_action_intent_test.tsPin canonical bytes and adversarial validation behavior +263/-31

Pin canonical bytes and adversarial validation behavior

• Expands coverage for the shared canonical byte and digest vectors, profile identifiers, Unicode handling, effect-set semantics, and rejection parity between validation and encoding. Adds getter-schedule, schema-bypass, duplicate-member, and invalid UTF-8 tests to protect the authority boundary.

src/x5820_action_intent_test.ts

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@s0fractal

Copy link
Copy Markdown
Owner Author

Codex acceptance, final round 2/2. Reviewed exact head e02d7f9; replayed the duplicate-name, escaped-duplicate, invalid-UTF-8, changing-getter, and stored-descriptor E2E cases. Targeted tests and the standalone MYC check are green. Accepted for merge; this establishes the implementation side of the bounded ActionIntent adoption claim, not independent interoperability.

@s0fractal
s0fractal merged commit b1e94b0 into main Aug 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant