feat(spec): enforcement_mode gains "declared", for policy nothing evaluated - #143
Conversation
…luated enforce, advisory and silent all assert that something evaluated the policy. enforce acted on the result, advisory did not, silent acted with the operational logs suppressed. There was no value for the case where the policy is named and bound into the signed record and nothing evaluated it at all. That case is the common one rather than a corner. An agent framework has no policy engine, so a record built by observing a LangChain or LlamaIndex run has a policy the operator declares and no evaluation of it anywhere. Both framework adapters hit this independently today and both refused to default the field, documenting that advisory overstates a bare run. That is honest and it still leaves every framework-produced record marginally untrue, which is the thing worth fixing in the vocabulary rather than in prose. declared is deliberately the weakest value and is never a default. A producer that evaluates policy MUST NOT use it, a consumer MUST NOT read it as evidence that any rule was checked, and a verifier appraising for enforcement SHOULD treat it as it treats an absent enforcement claim. Additive to a closed enum in the model and in both JSON schema copies, so unknown values are still rejected: a test pins that adding a value did not turn the field into free text. Same one-directional consequence as origin, a verifier older than this release rejects a record carrying declared. 246 tests, ruff and mypy clean.
Upstream merged agentrust-io#148, the last large piece this fork was carrying, which the maintainer rebased and opened himself under this fork's authorship. With agentrust-io#122, agentrust-io#125, agentrust-io#126, agentrust-io#136 and agentrust-io#137 already merged, most of the thirty-three commits here described work that now lives upstream with better provenance than this fork can give it: a PR number and a maintainer's review. `git rebase upstream/main` was tried first and abandoned. It stopped on the oldest commit in the set, a schema-and-version alignment whose content upstream has since taken, superseded and released three times over. Replaying thirty-three commits against fifteen of upstream's resolves early commits into shapes that no longer mean anything. So: main reset to upstream/main, fork-only material re-applied. The old history is tagged `archive/pre-576507b` and pushed rather than discarded. What is held here, and why it is held: - agentrust-io#117 gap disclosure: design note, two normative drafts, 18 vectors, generator - agentrust-io#116 verifier compatibility: 8 vectors, generator, normative draft - the normative crosswalk, mapping every RFC 2119 statement to whom it binds - `docs/conformance-method.md`, `coverage-report/` (historical), DECISIONS.md - the independent signature path and the package-consistency test Each is an unaccepted proposal or a method write-up, not unfinished work. Four files needed a real merge, and not in the same direction. `models.py` and `__init__.py` are upstream's plus this fork's two profile constants, because upstream had moved on with `origin` (agentrust-io#135) and the `declared` enforcement mode (agentrust-io#143) and a wholesale copy would have dropped both. `sign.py` and `test_sign.py` are this fork's `accepted_profiles` version, which supersedes the minimal cutover check upstream took from agentrust-io#125 -- a supersession CLAUDE.md predicted when agentrust-io#125 was offered. All four merged cleanly three-way against a817621, the last commit the two histories agree on. The crosswalk guard earned its place in the same run: upstream added two normative statements this fork had never seen, and `test_normative_crosswalk.py` failed until both had rows. A source-derived inventory noticing its subject moved is the property that document exists to have. 432 passed, 1 skipped. ruff and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
Upstream merged agentrust-io#148, the last large piece this fork was carrying, which the maintainer rebased and opened himself under this fork's authorship. With agentrust-io#122, described work that now lives upstream with better provenance than this fork can give it: a PR number and a maintainer's review. `git rebase upstream/main` was tried first and abandoned. It stopped on the oldest commit in the set, a schema-and-version alignment whose content upstream has since taken, superseded and released three times over. Replaying thirty-three commits against fifteen of upstream's resolves early commits into shapes that no longer mean anything. So: main reset to upstream/main, fork-only material re-applied. The old history is tagged `archive/pre-576507b` and pushed rather than discarded. What is held here, and why it is held: - agentrust-io#117 gap disclosure: design note, two normative drafts, 18 vectors, generator - agentrust-io#116 verifier compatibility: 8 vectors, generator, normative draft - the normative crosswalk, mapping every RFC 2119 statement to whom it binds - `docs/conformance-method.md`, `coverage-report/` (historical), DECISIONS.md - the independent signature path and the package-consistency test Each is an unaccepted proposal or a method write-up, not unfinished work. Four files needed a real merge, and not in the same direction. `models.py` and `__init__.py` are upstream's plus this fork's two profile constants, because upstream had moved on with `origin` (agentrust-io#135) and the `declared` enforcement mode (agentrust-io#143) and a wholesale copy would have dropped both. `sign.py` and `test_sign.py` are this fork's `accepted_profiles` version, which supersedes the minimal cutover check upstream took from agentrust-io#125 -- a supersession CLAUDE.md predicted when agentrust-io#125 was offered. All four merged cleanly three-way against a817621, the last commit the two histories agree on. The crosswalk guard earned its place in the same run: upstream added two normative statements this fork had never seen, and `test_normative_crosswalk.py` failed until both had rows. A source-derived inventory noticing its subject moved is the property that document exists to have. 432 passed, 1 skipped. ruff and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
|
One narrow clarification on silent across the existing lineage. #19 raised the original concern around silent. #28 then clarified it as allowing the governed action while suppressing operational logs and preserving every would-have-denied decision in the audit chain. #143 later describes silent as having “acted” on the policy result. Current TRACE also describes it as “evaluated and enforced” in the schema, while §4.3 still specifies the audit/log behavior without explicitly stating the deny disposition. So one narrow question: policy decision = deny MUST the governed action be allowed or blocked? Current cMCP allows a Cedar-denied call through in SILENT and projects that mode as TRACE silent. Did #143 intentionally change the deny disposition established in #28, or is “acted” / “enforced” only meant to distinguish evaluated silent from declared? I’m not assuming either answer; I just want to pin the intended semantics. |
Both framework adapters hit this independently today, which is what makes it a vocabulary problem rather than an adapter problem.
The gap
enforce,advisoryandsilentall assert that something evaluated the policy —enforceacted on the result,advisorydid not,silentacted with the operational logs suppressed. There was no value for a policy that is named and bound into the signed record with nothing evaluating it.That is not a corner case. An agent framework has no policy engine, so a record built by observing a LangChain or LlamaIndex run has a policy the operator declares and no evaluation of it anywhere. With three values such a record has to claim an evaluation that never happened.
Both adapters refused to default the field and documented that
advisoryoverstates a bare run. Honest, and it still leaves every framework-produced record marginally untrue — which belongs in the vocabulary, not in two READMEs.declaredThe weakest value, and never a default:
It says only: this is the policy the deployment states it was operating under.
Compatibility
Additive to a closed enum in the model and in both JSON schema copies — a test pins that adding a value did not turn the field into free text, and
monitoris still rejected by both.Same one-directional consequence as
origin: a verifier older than this release rejects a record carryingdeclared. Producers should not emit it until their consumers are current.246 tests,
ruffandmypyclean.Follow-up once released: both framework adapters can default to
declaredinstead of refusing, which is the first time either of them will be able to fill that field truthfully without the caller thinking about it.🤖 Generated with Claude Code