Repository navigation
item 6c: the gateway in the execution path - #9
Merged
Merged
Conversation
SPEC-v0.2 §6.1, §6.3, §6.5-§6.8, §6.10. Third of four stages for item 6: acceptance tests T19, T21, T22, T23, T25, and the end-to-end halves of T20 and T24 that 6b could only do as pure functions. The shape worth noticing is how §6.8 reaches the kernel. It is not a second outcome model: the executor translates the table into v0.1 §5.5's own vocabulary — return for COMMITTED, raise NotExecuted for FAILED, raise anything else for AMBIGUOUS — and Control then applies the rules it has always applied. The gateway does not get its own way of deciding what happened, and a lost response over HTTP blocks a retry for exactly the reason a lost response in-process does. §6.7's canonical forwarding is the other half of that: the request the gateway sends is built from the action's canonical arguments, never copied from the received body, so what was hashed, reserved and recorded is byte-for-byte what the upstream receives. A gateway relaying the original bytes would be binding an approval to one document and executing another. A fresh connection per intercepted call, because "the connection was never established" is the only claim in §6.8 that asserts non-execution and a pooled connection closed while idle fails on write, which is indistinguishable from a request that arrived. Two resolutions worth reading: An HTTP 401 is classified before the body is parsed. A peer may answer 401 with any payload it likes, including a well-formed JSON-RPC result; the status is the load-bearing part, because the resource server validated the token before dispatch and nothing reached the tool. §6.10 says a denied request must not be re-asked until it expires. The spec leaves open what happens when no approval exists and the effect key would refuse anyway; the fail-closed reading is to give that refusal now rather than trouble a human with a decision that could not be acted on. Marked `# SPEC:`. It only ever refuses more. CI installs the gateway extra. T30 still proves in a subprocess that `import ctrlrun` touches neither httpx nor ctrlrun.gateway, installed or not.
Mutation-testing §6.10 found it: the check that will not send a human a decision the effect key would refuse anyway catches an *approved* duplicate before Control ever raises, so the except-DuplicateEffect handler had no test. An allowed action goes all the way to the reservation, which is where v0.1 §5.4 refuses it, and that is the mapping under test.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third of four stages for item 6. SPEC-v0.2 §6.1, §6.3, §6.5–§6.8, §6.10 — acceptance tests T19, T21, T22, T23, T25, and the end-to-end halves of T20 and T24 that 6b could only do as pure functions.
An MCP client points here instead of at the tool server.
tools/callbecomes an Action; everything else is relayed. No agent changes.The upstream in these tests is a real HTTP server on a real socket, because the guarantees are about what happens between two processes: that the upstream is never called for a refused request, that what it receives is byte-for-byte what was hashed, and that a lost response blocks the retry.
§6.8 doesn't get its own outcome model
The executor translates the table into v0.1 §5.5's own vocabulary — return for
COMMITTED, raiseNotExecutedforFAILED, raise anything else forAMBIGUOUS— andControlthen applies the rules it has always applied. The gateway does not get a second way of deciding what happened, and a lost response over HTTP blocks a retry for exactly the reason a lost response in-process does.§6.7's canonical forwarding is the other half: the request sent upstream is built from the action's canonical arguments, never copied from the received body. A gateway relaying the original bytes would be binding an approval to one document and executing another.
A fresh connection per intercepted call, because "never established" is the only claim in §6.8 that asserts non-execution, and a pooled connection closed while idle fails on write — indistinguishable from a request that arrived.
Two decisions I'd want you to look at
A 401 is classified from the status, before the body is parsed. A peer may answer 401 with any payload it likes, including a well-formed JSON-RPC result — my first version parsed that as a committed tool call. The status is the load-bearing part: the resource server validated the token before dispatch, so nothing reached the tool. The test caught it.
§6.10 leaves a case open, and I took the fail-closed reading. The spec says a denied request must not be re-asked until it expires. It doesn't say what happens when no approval exists and the effect key would refuse anyway. Asking a human to approve an action that could not run if they said yes wears the same approver down for the same reason — so the refusal the reservation would give is given now, before anyone is troubled by it. Marked
# SPEC: §6.10. It only ever refuses more. This is also what makes the spec's stated expectation for T25's third call (-41004) come out right.Mutation table — 30 mutations, all red
FAILED↔AMBIGUOUS_meta,not_executed_on_errorOne started green, and it's a consequence of the decision above. The
except DuplicateEffecthandler had no test at all: my new pre-approval check catches an approved duplicate beforeControlever raises, so every T25 path went around it. An allowed action goes all the way to the reservation, which is where v0.1 §5.4 refuses it — that test now exists.Worth naming as its own pattern, distinct from the subsumed guards of the last four items: adding an earlier check can orphan a later handler, and the tests that covered the behaviour keep passing because the outcome is unchanged. Only the mutation shows the handler is now unreachable from any test.
Also
ctrlrun gatewayjoins the CLI, and prints at startup the actions with noeffect:template (§3.2) — a write with no effect key should be visible on the line that starts the process, not discovered in a receipt three weeks later..[dev,gateway]in both jobs. T30 still proves in a subprocess thatimport ctrlruntouches neitherhttpxnorctrlrun.gateway, installed or not.ANN401/N818are ignored forsrc/ctrlrun/gateway/*.pywith a written justification: the subject matter is untyped JSON off a socket, and the error names follow this codebase's convention (DuplicateEffect,AmbiguousEffect) rather than the suffix rule.Verification
1082 tests pass (1034 → 1082),
mypy --strict src/,ruff checkandruff format --checkclean.Remaining: 6d — elicitation/MRTR held reservations, legacy passthrough, SSE resumption stripping (T26, T30b).