Skip to content

feat: let a call declare its own sensitivity class via _cmcp.data_class - #498

Merged
imran-siddique merged 1 commit into
agentrust-io:mainfrom
qubeena07:feat/per-call-sensitivity-classification
Aug 12, 2026
Merged

feat: let a call declare its own sensitivity class via _cmcp.data_class#498
imran-siddique merged 1 commit into
agentrust-io:mainfrom
qubeena07:feat/per-call-sensitivity-classification

Conversation

@qubeena07

Copy link
Copy Markdown
Contributor

Summary

Closes #479, the second piece, per call classification. Part one, configurable vocabulary, merged in #489.

One catalogued tool can serve many classes of data, a model call tool catalogued at pii might, on a specific call, actually carry confidential data, agentrust-io/demos#36 is the motivating example. The signed transcript could only ever show the tool's single catalogued value, and the session's own sensitivity tracking never rose past what the catalog alone said either, which is an enforcement gap for later calls in the session, not only a record keeping one.

A call may now declare a class for itself via _cmcp.data_class on the request, following the exact pattern _cmcp.workflow_id already uses.

Why no extra validation was needed

_max_sensitivity, already in session/state.py from part one, turns out to already be the validator. It returns whichever of two labels ranks higher and ties favour the catalog value, so an unrecognised or lower declared value is harmless by construction and a legitimately higher one raises the effective class, no separate check needed anywhere. Full reasoning is in the code comments at the computation site in proxy.py.

The two effects are computed separately on purpose

The declared value raises both the session's max_sensitivity (composing multiple tags the same way update_from_inspection already did, no change needed there) and, independently, that specific call's own row in the signed transcript. The transcript value is deliberately not read back from session state, since max_sensitivity is monotonic across the whole session, so by the time call 2 runs it may already reflect call 1's history, and call 2's transcript row needs to describe call 2's own class, not the session's accumulated one. Verified manually end to end, see test plan below.

Changes

  • src/cmcp_runtime/audit/chain.py: new AuditEntry.effective_data_class field, threaded through AuditChain.append.
  • src/cmcp_runtime/mcp/proxy.py: call_tool takes declared_data_class, computed once per call and used at both response classification sites.
  • src/cmcp_runtime/mcp/server.py: parses _cmcp.data_class the same way _cmcp.workflow_id already is parsed.
  • src/cmcp_runtime/session/manager.py: close_session's transcript loop prefers effective_data_class over the catalog default when present.
  • docs/tutorials/connecting-agent-frameworks.md, docs/spec/session-policy.md, CHANGELOG.md.

Test plan

  • pytest tests/unit/ passes locally, 1034 passed, 8 skipped
  • ruff check src/ tests/ clean
  • mypy src/cmcp_runtime/ src/cmcp_verify/ clean
  • bandit -r src/ clean
  • manual end to end check: a call declaring top_secret (composing with a custom vocabulary from part one) raises the session and shows top_secret in its own transcript row; a second call on the same tool with no declaration in the same session correctly shows the tool's own catalogued public in its own row, unaffected by the session's accumulated maximum
  • found and closed a real pre-existing gap while writing this: close_session's transcript building loop had no test coverage at all before this change, now has both the new-field case and the existing-fallback case

Part 2 of agentrust-io#479. One catalogued tool can serve many classes of data, a model
call tool catalogued at pii might, on a specific call, actually carry
confidential data, agentrust-io/demos#36 is the motivating example. The
signed transcript could only ever show the tool's single catalogued value,
and the session's own sensitivity tracking never rose past what the catalog
alone said either, an enforcement gap for later calls in the session, not
only a record keeping one.

A call may now declare a class for itself via _cmcp.data_class on the
request, following the exact pattern workflow_id already uses. It composes
with the vocabulary work from part one: a deployment adds a label in config,
a call declares it per call.

The declared value needs no separate validation, _max_sensitivity already is
the validator. It returns whichever of two labels ranks higher and ties
favour the catalog value, so an unrecognised or lower declared value is
harmless by construction, and a legitimately higher one raises both the
session's max_sensitivity and, independently, that specific call's own row
in the signed transcript, without inheriting whatever the session had
already accumulated from earlier calls.

AuditEntry gained one new field, effective_data_class, None on every call
that never declares one, so nothing changes for a caller that does not use
_cmcp.data_class. Also found and closed a real test gap while writing this:
close_session's transcript building loop had no coverage at all before this
change.

Signed-off-by: Dipika Ranabhat <qubeena7@gmail.com>
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@imran-siddique imran-siddique left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the code rather than the description, since the whole change rests on one claim.

The claim holds. _max_sensitivity is if order.get(b, 0) > order.get(a, 0): return b; return a, so an unrecognised label resolves to rank 0 and a tie returns a, the catalog value. A declaration therefore cannot lower the effective class, and a garbage value cannot raise it, both by construction rather than by a check that a later refactor could drop. You are right that no separate validation was needed, and the tests for the below-floor and unrecognised cases are the ones that matter here.

Computing the session effect and the per-call effect separately is also right. A call's transcript row should not inherit whatever earlier calls accumulated, or the row stops describing the call.

One non-blocking gap. The declaration never reaches Cedar. _build_cedar_context is untouched by this diff, so a call declaring confidential against a tool catalogued pii is still evaluated at pii. The effect is real but deferred: the session rises, so the next call is gated correctly, while the declaring call is not. That is not a security hole, since declaring can only raise and an attacker simply would not declare. It is a mismatch with the docs, which say the declared value raises the effective class "for this call", and a reader will take that to include the policy decision.

Merging as-is because the deferred behaviour is the enforcement gap #479 actually named, and the fix is additive. Filing the Cedar wiring as a follow-up.

@imran-siddique
imran-siddique merged commit 141f483 into agentrust-io:main Aug 12, 2026
12 of 13 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.

Sensitivity vocabulary is fixed, so a deployment cannot express its own classification scheme

3 participants