feat(contract): freeze the Deny-reason vocabulary (#28) - #39
Merged
Conversation
Keep `Decision::Deny { reason: String }` on the wire unchanged (the
byte-stable serde/CBOR/JSON round-trip stays frozen) and freeze the
*vocabulary* in code instead.
- deny_reasons module in deckard-contract: 30 documented static tag
consts + 4 typed dynamic-prefix builders (railgun_keys/signer_error/
sign_failed/broadcast_failed -> "prefix: detail"). No open
with_detail(prefix, ..) — an arbitrary string can't become a reason.
- Migrate every Decision::Deny / *::Denied / reply_error construction in
deckard-contract, deckard-signerd, deckard-mcp to the consts; the mcp
failure-catalog match arms + sidecar guards use const patterns so a
rename is a compile error.
- tests/deny_vocabulary.rs: a dependency-free, structure-aware source
scan (mask string/comment interiors, blank #[cfg(test)] blocks, then
brace-aware require every Deny reason to route through deny_reasons::)
+ a reflective module<->snapshot cross-check so a new const/pub fn
can't slip past. Fails under `cargo test --workspace` if a Deny is
built from a string outside the frozen set.
- Per-tag remediation table extended in docs/build/31-agent-quickstart.md;
from_deny_reason gains matching arms for the prefix tags + malformed_request.
unsupported_v1 is kept (reachable via Unshield/ContractCall). No wire-shape
change, no new dependencies.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #28.
What
Keep
Decision::Deny { reason: String }on the wire unchanged (the byte-stable serde/CBOR/JSON round-trip stays frozen — no enum migration, no new fields) and freeze the vocabulary in code instead. The intended tags existed only informally; nothing stopped a newDenysite inventing"overCap"or a typo, and an agent retrying against an unstable vocabulary flails.How
deny_reasonsmodule (crates/deckard-contract/src/deny_reasons.rs) — the single source of truth: 30 documented static tag consts + 4 typed dynamic-prefix builders (railgun_keys/signer_error/sign_failed/broadcast_failed→"prefix: detail"). There is deliberately no openwith_detail(prefix, …), so an arbitrary string can't become a reason prefix.Decision::Deny/ExecuteResult::Denied/SignOrderResult::Denied/reply_error()construction indeckard-contract,deckard-signerd, anddeckard-mcpnow references a const. The mcp failure-catalog match arms + sidecar guards use const patterns, so a tag rename is a compile error.crates/deckard-contract/tests/deny_vocabulary.rs, runs undercargo test --workspace, no new deps): a structure-aware source scan that masks string/comment interiors, blanks#[cfg(test)]blocks, then brace-aware requires every Deny reason to route throughdeny_reasons::— raw literals, foreign vars/consts, and forged prefixes all fail. Plus a reflective module↔snapshot cross-check so a new const/pub fncan't slip past, and self-tests proving each bypass class is caught.ReadStatus/ShieldStatusreasons (a different, free-form vocabulary) are never touched.docs/build/31-agent-quickstart.mdis extended to every tag (16 new rows);from_deny_reasongains matching arms for the prefix tags +malformed_requestso runtime remediation matches the docs.The full Deny surface turned out to be 34 tags (incl. swap #24's, already merged), not the issue's informal list of 7 — all enumerated from code.
Open question (resolved)
unsupported_v1is kept — it's reachable via Unshield / non-shaped ContractCall (daemon_e2easserts it).Verification
cargo fmt --all --checkcleanjust checkgreen (both feature configs)cargo test --workspace→ 211 passed, 2 ignoredNot in scope
No wire-shape change, no
SetPolicy/policy mutation, noAllow/NeedsApprovalsemantics change.