Repository navigation
fix(intent_bridge,successor_observation): validate caller options and JWKs at entry and deep-copy observation (#617) - #618
Conversation
|
🟡 Contributor Check: MEDIUM
Automated check by AgenTrust Contributor Check. |
…n at merge The entry this pull request proposed is written on main at merge by the merging maintainer, as CONTRIBUTING step 5 now asks, so open pull requests stop conflicting on the one file they would all touch. Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
|
From here on, pull requests do not edit |
dinakarjs
left a comment
There was a problem hiding this comment.
Reviewed exact head 25ddaf9. Caller configuration is validated before untrusted bridge input is consumed, signed timestamp values are retained, and successor predicates operate on detached observation state. Local focused intent-bridge/successor tests: 139 passed; Ruff and strict mypy (16 source files) passed. Hosted CI/conformance/review-bar checks are successful. No blocking finding.
|
Thanks @lywinged! Dropped the |
There was a problem hiding this comment.
The _pubkey_from_jwk move is good, but the trusted kid shape check is still not at entry.
In both verify_bridge and verify_successor_artifact, isinstance(trusted_kid, str) and bool(trusted_kid) still happens after the untrusted artifact has already been parsed. A cryptographically valid JWK with missing / empty / non-string kid can therefore surface artifact errors first.
#617 explicitly asks for the trusted JWK and its kid shape to be validated before any artifact inspection. The new tests cover unusable key material, but not this remaining ordering case, kindly hoist the non-empty-string check on trusted_*_jwk["kid"] right after _pubkey_from_jwk, and add a couple of regression tests that pair a valid public key (no/empty/bad kid) with a malformed artifact so the config error wins.
lywinged
left a comment
There was a problem hiding this comment.
Request changes. Reviewed at 25ddaf9 on c5d49a5.
What I checked rather than read:
- The three defects in #617 reproduce on
mainthrough the public functions and are gone here. A JWK carryingdunder the wrongkidraisestrusted_authorizer_jwk is unusable,now=-1with a signeddenyraises thenowerror, and a predicate writing to its argument leavesafteralone. - CI's commands, run on 3.12 and 3.11, pass, the full suite at 4817 passed and 48 skipped on both. The review bar finds all 31 test cases this adds failing on behaviour at the merge base.
- One at a time, I removed or moved back the 16 checks the diff touches, and the copy. Against the tests that reach the two modules, every mutant fails a test except the two that delete an
or not trusted_kidclause. - For the observations I tried, the predicate gets a separate copy of the observation, unchanged when the caller later edits
after.
Two things.
- rajnisht7's review of 25 October says the
kidcheck still runs after the artifact is parsed, so a valid key with a missing, empty or non-stringkidsurfaces artifact errors first. It does: a correct key with"kid": 7,authorization_id="auth-7"and an artifact signed forauth-othergivesAuthorizationMismatchfromverify_successor_artifact, which the description rules out for a malformed JWK. Two more facts bear on moving it. Where it stands,or not trusted_kidcannot change a result for anystr, becauseauthorizer_key_idpassed_nonempty_stringon line 389 andobserver_key_idon line 174; deleting both clauses leaves the whole suite green on 3.12. And moving the check changes behaviour that two existing tests pin:test_bridge_naming_another_authorizer_key_id_is_rejectedwantskey_to_jwk(key), which has nokid, to raisedoes not identify the trusted key, and theauthorizer_key_idcase oftest_runtime_verifier_enforces_schema_scalar_constraintspasses an emptykidand expects the artifact's error. I would move it, with a message that names the key:
trusted_kid = trusted_authorizer_jwk.get("kid")
if not isinstance(trusted_kid, str) or not trusted_kid:
raise IntentBridgeError("trusted_authorizer_jwk kid must be a non-empty string")right after _pubkey_from_jwk, the later comparison reduced to !=, the same for trusted_observer_jwk, and those two tests updated; the whole suite then passes on 3.12. It also wants the tests rajnisht7 asked for, one per function, in which a valid key with a missing, empty or non-string kid meets a malformed artifact and must raise the kid error: with only the two tests updated, moving the observer-side check back still passes every bridge test.
copy.deepcopygives up before the digest does. Withafter["observation"] = json.loads('{"k":' * 600 + '{}' + '}' * 600)and its own digest,evaluate_successor_observationwith a predicate reading one key returnsestablishedonmainand raisesRecursionErrorhere, on 3.12 and 3.11. The fuzz harness,.clusterfuzzlite/fuzz_intent_bridge.py, states the property this breaks:evaluate_successor_observationdocumentsSuccessorObservationError, and "any other exception is that evidence crashing it". A copy that can go as deep as the digest would keepmain's answer; at the least, theRecursionErrorwants to becomeSuccessorObservationError. The copy inplatform_measurement.pythat the description points to sits inside a handler that turns any exception intoappraiser_raised; this one has none, andtests/test_untrusted_input_stays_on_the_documented_error.pyhas no case for this function yet.
Things I would not hold the merge for:
transcriptis a parameterverify_bridge's entry block does not check:transcript="garbage"givesAuthorizationDeniedwith a signeddenyandAuthorizationMismatchwith a signedallow, as onmain. #617 does not name it, though the description's promise covers it.
Generated by Claude Code
|
Thank you @rajnisht7 and @lywinged! Addressed both points in
|
a12c467 to
1479860
Compare
rajnisht7
left a comment
There was a problem hiding this comment.
_clone_observation only walks dict and list. A tuple falls through to a plain assignment, so nested mutable objects stay shared
the project’s own JCS path accepts tuples as arrays (_refuse_whole_floats_out_of_range walks (list, tuple), and rfc8785.dumps treats them identically to lists). digest_jcs therefore succeeds on an observation that contains them, binding and evaluation succeed, and a predicate can still mutate the caller’s original:
obs = {"tuple_array": ({"secret": "original"},)}
after = {"observation": obs, "observer": "observer-1", "observed_at": 160}
# digest_jcs(after) succeeds; evaluate_successor_observation(...) returns established
# after["observation"]["tuple_array"][0]["secret"] is now "MUTATED"copy.deepcopy isolates this correctly but the iterative clone does not. existing tests only cover pure dict/list trees, so they stay green, kindly treat tuple the same as list in the clone (or otherwise detach it) and add a regression that exercises a tuple containing a mutable dict/list.
… JWKs at entry and deep-copy observation (agentrust-io#617) Signed-off-by: Raghuvar Karthik Durga <177662415+drk-ram@users.noreply.github.com>
1479860 to
76cacd8
Compare
|
@rajnisht7 Rebased onto |
dinakarjs
left a comment
There was a problem hiding this comment.
Re-reviewed current head 76cacd8. This supersedes the limited caller-configuration/isolation assessment in my older 25ddaf9 review: subsequent review correctly found kid-validation ordering, deep-copy depth and mutable objects inside tuples that it had not established. The current source now checks trusted kid shape at entry and iteratively detaches dict/list/tuple observations.
In this head's fresh hash-locked Python 3.12.14 environment, tests/test_intent_bridge.py plus tests/test_successor_observation.py pass 149 cases normally and under -O, including the deep and tuple controls. Changed-file Ruff and strict mypy for the two changed modules pass. No remaining blocker found in these selected paths. I did not rerun the full TRACE suite for this review; normal current-head CI, other reviewers' disposition and maintainer policy remain prerequisites. No withdrawal/status-service or production acceptance claim. Prepared with OpenAI Codex.
|
The current head 76cacd8 validates authorizer and observer kid at entry and uses iterative _clone_observation instead of copy.deepcopy. The 600-level observation and both malformed-kid regressions are present; the bridge and successor suites pass all 149 tests. I corrected the stale description to match the current implementation. Please re-review the remaining changes request. |
What this changes
Closes #617. Current source head:
76cacd81a18c40597e7088aaae3c05059071d754.verify_bridgeandverify_successor_artifactvalidate the trusted JWK with_pubkey_from_jwk, then require itskidto be a non-empty string at entry, before parsing the artifact. Malformed key identifiers therefore produce a key-specificIntentBridgeError, rather than an artifact mismatch. The later identifier comparison only checks equality.verify_bridgevalidates the listed caller options (now,declaration,tool_call,pic_intent_digest, andpic_args_digest) before signature verification and denial handling.verify_successor_artifactvalidatestrusted_observerandauthorization_idat entry._bind_successor_observationchecksexpected_successor_digestbefore the observation envelope. This does not claim to add entry validation fortranscript.evaluate_successor_observationpasses a separate, recursively isolated JSON-compatible observation to the predicate through iterative_clone_observation, notcopy.deepcopy. It preserves nested dictionaries, lists, and accepted tuple arrays and handles the reviewed 600-level nesting case without hitting Python's recursive-copy limit. Predicate mutation cannot change the caller's observation.Wire schemas, published vectors, and integration documentation are unchanged. These are reference-library input validation and state-isolation fixes, not a wire-format change. Malformed caller options fail closed with the documented error family; concurrent mutation by another thread during digest computation is not covered by these regressions.
The latest review identified delayed
kidvalidation andcopy.deepcopyfailing on an otherwise digestible deep observation. Both are corrected in the current source. This description removes the stalecopy.deepcopyclaim and historical measurements that did not describe the current head.Type of change
Reference-library code fix; no normative specification or schema edit.
Spec section
None. Production changes are in
src/agentrust_trace/intent_bridge.pyandsrc/agentrust_trace/successor_observation.py.Tests for a fix
The existing regression suite covers unusable authorizer/observer keys, malformed caller options before artifact/signature/denial errors, expected-digest validation, and predicate state isolation. The latest-review regressions are:
tests/test_intent_bridge.py::test_verify_bridge_rejects_bad_trusted_authorizer_kid_at_entryandtest_verify_successor_artifact_rejects_bad_trusted_observer_kid_at_entry, each parameterized over missing, empty, integer, and list identifiers.tests/test_intent_bridge.py::test_evaluate_successor_observation_clones_600_deep_observation.tests/test_successor_observation.py::test_predicate_receives_deep_copy_and_cannot_mutate_after_observationandtest_predicate_cannot_mutate_nested_objects_inside_tuple_array.Rerun on the current head with Python 3.11.9 and this checkout's
srcandconformance/srcinPYTHONPATH:python -m pytest tests\test_intent_bridge.py tests\test_successor_observation.py -q --tb=short-> 149 passed.This rerun verifies the current tests; it is not a new claim of per-clause mutation coverage. Current-head hosted CI, Conformance CI, trust-record-verify, Review bar, and ClusterFuzzLite PR runs are successful. The changes-requested review and independent maintainer-approval requirement still need maintainer re-review.
Changelog entry
Checklist
git commit -s)<!-- CHANGED: #NNN: description -->in spec text