Skip to content

feat(spec): enforcement_mode gains "declared", for policy nothing evaluated - #143

Merged
imran-siddique merged 1 commit into
mainfrom
feat/enforcement-declared
Aug 9, 2026
Merged

feat(spec): enforcement_mode gains "declared", for policy nothing evaluated#143
imran-siddique merged 1 commit into
mainfrom
feat/enforcement-declared

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Both framework adapters hit this independently today, which is what makes it a vocabulary problem rather than an adapter problem.

The gap

enforce, advisory and silent all assert that something evaluated the policyenforce acted on the result, advisory did not, silent acted 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 advisory overstates a bare run. Honest, and it still leaves every framework-produced record marginally untrue — which belongs in the vocabulary, not in two READMEs.

declared

The weakest value, and 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.
  • A verifier appraising for enforcement SHOULD treat it as it treats an absent enforcement claim.

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 monitor is still rejected by both.

Same one-directional consequence as origin: a verifier older than this release rejects a record carrying declared. Producers should not emit it until their consumers are current.

246 tests, ruff and mypy clean.

Follow-up once released: both framework adapters can default to declared instead 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

…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.
@imran-siddique
imran-siddique merged commit 1bd0877 into main Aug 9, 2026
8 checks passed
@imran-siddique
imran-siddique deleted the feat/enforcement-declared branch August 9, 2026 19:07
imran-siddique added a commit that referenced this pull request Aug 9, 2026
Ships enforcement_mode 'declared' (#143) and the content-marking binding (#144).

Cut now because the framework adapters cannot default enforcement_mode to
'declared' against an unreleased enum: they would have to keep refusing the
field, which is the overstatement the value exists to remove.
lywinged added a commit to lywinged/trace-spec that referenced this pull request Aug 10, 2026
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>
lywinged added a commit to lywinged/trace-spec that referenced this pull request Aug 24, 2026
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>
@solloek369-arch

Copy link
Copy Markdown
Contributor

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
enforcement_mode = silent

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.

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.

2 participants