Repository navigation
Share the round trip, sign once, and return a Decision - #482
bar-capsule wants to merge 2 commits into
Conversation
acs.client.round_trip, acs.core.request_envelope and the turn helpers replace each adapter's copies. A request is signed as the last step before it is sent. Signed-off-by: Bar Kaduri <bar@capsule.security>
normalize_decision returns acs.core.Decision instead of a tuple. Behavior is unchanged. Signed-off-by: Bar Kaduri <bar@capsule.security>
drk-ram
left a comment
There was a problem hiding this comment.
Reviewed commit d2b5517e6b50dc892d3bd0f2f9aad5696a3ffd32 against its stated stacked base.
No blocking finding in this refactor.
acs/client/_exchange.py:184-193 makes signing the last request mutation before transport and preserves the method-specific timeout and posture resolution. Removing request_id_ref before that boundary avoids re-signing a changed envelope. Decision keeps unusable dispositions distinguishable from allow, and all three translators retain their restrictive substitutions; the denied-turn-start check also confirms that no user message follows the denial.
The focused command below passed on Windows/Python 3.11. The broader run had 99 passed and 4 failures, limited to Windows permission and refused-port timing expectations; it was not a full-suite pass. The PR's test and package checks are green.
Local validation (from adapters, Python 3.11):
python -m pytest tests/core/test_decision.py tests/core/test_envelope.py tests/client/test_round_trip.py tests/common/test_session_gate.py::SessionGateThroughHooks::test_a_denied_turn_start_blocks_the_prompt_and_sends_no_message tests/integrations/nat/test_dispositions_live.py -q --tb=short
Result: 24 passed, 3 warnings, 23 subtests passed in 8.88s
|
Closing this slice because PR #22 (the reference adapters for Claude Code, Cursor and NAT) merged into integration as 2630ec2, and that merge carries every slice in this stack. The top of the stack had the same files as PR #22's final commit, so nothing in this slice was left behind. Thanks, Bar, for cutting the adapters into pieces this size, because it made the review possible. |
Stacked on #481. Next: #483. The full stack is listed in #22.
What changed
Two commits:
90ce41cashares the protocol code, andd2b5517enames the decision fields.acs.client.round_tripsigns a request as the last step before sending, exchanges it within its method's negotiated timeout, and returns the decision or the posture's resolution. It replaces each adapter's_guardian_roundtripand NAT's_exchange.acs.core.request_envelopeandturn_start_envelopebuild every unsigned envelope. The builders no longer sign, so droppingrequest_id_refneeds no re-sign.open_turn,current_turnandclose_turnkeep a session's open turn in one place. A request that fails to sign is not sent, and the posture settles that step as an adapter error. With no secret configured, requests still go out unsigned after a one-time warning; removing that fallback is a separate follow-up PR.normalize_decisionreturnsacs.core.Decision(disposition,reasoning,modifications, andknownfor the five dispositions) instead of a tuple. The Claude Code and Cursor translators take theDecision, and NAT drops its ownKNOWN_DECISIONS.90ce41ca's tuple andd2b5517e'sDecisionagree on all 2,892 inputs of a parity check, and the known dispositions are tested againstresponse-envelope.json's enum. New tests cover three rules the change touches that had none: an unusable disposition on a NAT result redacts the output under fail-open, and a deniedturnStartblocks the prompt in Claude Code and Cursor without sending itsuserMessage.Ariel's findings this answers
acs.client.Decisionvalue instead of a tuple, in the narrow form: the value inacs.core, returned bynormalize_decisionand used by all three adapters. A frozenModificationsvalue and the per-hook table stay with the HookSpec follow-up.KNOWN_DECISIONSin NATWhere this differs from Ariel's proposal
Envelopevalue. Envelopes stay dicts built byrequest_envelope, andround_tripsigns each one before the exchange serializes and posts it, so nothing edits a request after signing.Decisiontakes the narrow form.dispositionis astrrather than aLiteral, because an unusable disposition is carried to the audit record;modificationsstays a dict, and the MODIFY composition check stays its own function.Which issue does this implement
Part of #134
Base branch
Stacked on #481, not on
integration. It lands through #22; see the stack in #22.Type of change
I tested this
At this slice's top (
d2b5517e), withclauderemoved from PATH andACS_LIVE_CLAUDEandACS_SPEC_DIRunset: 418 passed and 3 expected skips with the NAT extra, 368 passed and 3 expected skips without it. The skips are the two opt-in live Claude Code tests and the manual Cursor procedure. CI passes on this PR, including the installed-wheel check on Python 3.11 and 3.12.Checklist
git commit -s(required by the DCO)Security
Every request is signed as the last step before it is sent.