Skip to content

Share the round trip, sign once, and return a Decision - #482

Closed
bar-capsule wants to merge 2 commits into
refimpl/adapters-06-negotiatedfrom
refimpl/adapters-07-round-trip
Closed

bar-capsule wants to merge 2 commits into
refimpl/adapters-06-negotiatedfrom
refimpl/adapters-07-round-trip

Conversation

@bar-capsule

@bar-capsule bar-capsule commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #481. Next: #483. The full stack is listed in #22.

What changed

Two commits: 90ce41ca shares the protocol code, and d2b5517e names the decision fields.

  • acs.client.round_trip signs 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_roundtrip and NAT's _exchange.
  • acs.core.request_envelope and turn_start_envelope build every unsigned envelope. The builders no longer sign, so dropping request_id_ref needs no re-sign.
  • open_turn, current_turn and close_turn keep 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_decision returns acs.core.Decision (disposition, reasoning, modifications, and known for the five dispositions) instead of a tuple. The Claude Code and Cursor translators take the Decision, and NAT drops its own KNOWN_DECISIONS. 90ce41ca's tuple and d2b5517e's Decision agree on all 2,892 inputs of a parity check, and the known dispositions are tested against response-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 denied turnStart blocks the prompt in Claude Code and Cursor without sending its userMessage.

Ariel's findings this answers

Where this differs from Ariel's proposal

  • There is no Envelope value. Envelopes stay dicts built by request_envelope, and round_trip signs each one before the exchange serializes and posts it, so nothing edits a request after signing.
  • Decision takes the narrow form. disposition is a str rather than a Literal, because an unusable disposition is carried to the audit record; modifications stays 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

  • Reference implementation or adapter

I tested this

At this slice's top (d2b5517e), with claude removed from PATH and ACS_LIVE_CLAUDE and ACS_SPEC_DIR unset: 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

  • Commits are signed off with git commit -s (required by the DCO)
  • Prose follows STYLE.md
  • No secrets, tokens, or internal URLs in the diff

Security

Every request is signed as the last step before it is sent.

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 drk-ram left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

@rocklambros

Copy link
Copy Markdown
Collaborator

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.

@rocklambros rocklambros closed this Nov 5, 2026
@github-project-automation github-project-automation Bot moved this from In progress to Done in ACS Project Tracker Nov 5, 2026
@bar-capsule
bar-capsule deleted the refimpl/adapters-07-round-trip branch November 8, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants