feat: let a call declare its own sensitivity class via _cmcp.data_class - #498
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
imran-siddique
left a comment
There was a problem hiding this comment.
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.
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
Test plan