Skip to content

Build two of §9.4's three rows, and close T480 and T502 - #191

Merged
rohanrkamath merged 2 commits into
mainfrom
fix/section-9-build-and-close
Sep 14, 2026
Merged

rohanrkamath merged 2 commits into
mainfrom
fix/section-9-build-and-close

Conversation

@arpanghoshal

@arpanghoshal arpanghoshal commented Sep 13, 2026

Copy link
Copy Markdown
Member

#190 merged the first commit of its branch; this is the second, pushed a few minutes later. It
is the whole of the build-and-close work. Pairs with ctrlrun-docs#25, which carries the API page
and the count.

#190 recorded three unshipped SPEC-v0.10 §9 names and left them. That was the wrong instinct.
A gap that can be closed is closed; only the one whose name was wrong and whose reason was
right stays as a row.

§9.4 row Now
hop=/task= on ctrlrun.adapter.needs_approval built
ctrlrun.hop/v1's key set (§6.2) fixed in §6.2, and now parsed by a test
ssl_context= on gateway.transport.request not built, deliberately. §9.4 is one row saying why

needs_approval, a behaviour fix, not a doc fix

(control, action, arguments, *, resource=None, task=None, hop=None), both threaded into
Control.evaluate.

Without them the predicate evaluated against the receiver's whole candidate set while execute
evaluates against the hop alone (§2.3, no fallback). It answered "a human is needed" for a call
execute then refuses: the framework surfaces an approval item, a human says yes, the call fails
anyway.

It was never an authority hole: Control.execute is the enforcement point and refuses either
way, so nothing wider ever ran. What it cost was the framework's own approval item and a receipt
nobody could explain. Said plainly, because "a frozen API row didn't ship" reads worse than it was.

T128c drives the predicate and execute over one hop and asserts they agree. The no-hop case is
its negative control
, so the fixture cannot be one where everything is denied anyway (mutation
pattern 3). Mutated back to the old call: fails.

ctrlrun.hop/v1, the table listed eight of eleven keys

schema, root_id and missing_parent_id were emitted for a whole milestone while §6.2 described a
smaller shape. Under-describing is the safe direction for a reader and the wrong one for a schema
somebody writes a consumer against. Not re-cut, because nothing it emits changed.

T506b parses the table out of SPEC-v0.10.md and compares it to the emitted document. Mutated: one
row deleted from the spec, T506b fails naming it.

T480, both halves

  • Parser: no identity field from params.metadata becomes a field of the request, asserted over
    the parsed object's attributes rather than by grepping for a string.
  • End to end: the receipt names the IdentityProvider's principal, and the payload's chosen name
    reaches no receipt and no event.

It held by construction before, so this is the regression guard for a property that was true and
untested. Mutated: agent_id read out of the bag, both halves fail.

T502, the audit changed the answer

Two of its four rows already existed under their own numbers (test_T461..., test_T462...).
That is §9.4's defect in another place: a table describing the tree, checked by a human reading both.
My earlier report called T502 uncovered; it was half covered, under other names.

  • T502a, the resumed observed receipt's spend. T439 covers observe, T447 covers resume,
    neither covers the two together.
  • T502b, the doubled ACTION_DENIED on a resumed observed leg. The only row with no test
    under any name, and absent-shaped for a reason worth keeping: every assertion near it checked the
    event appeared, and an event that appears twice appears.

Two of these tests were wrong first, and the mutation is what said so

T502a asserted the opposite of the contract. It read an empty ledger as meaning an observed
resumed receipt should report no spend. §4.2.1a is explicit: every number on an observed receipt is
a counterfactual, and without it nothing records what the action would have cost, so a budget
cannot be sized from an observed run
, the entire reason to run one. The code was right; I nearly
"fixed" it.

T502b was green against its own mutation. It filtered on budget_exhausted, and announce
guards the unmeasurable refusal, so the scenario never reached the guarded line. Rebuilt on a
negative amount; now fails when announce is ignored.

Full gate: all checks passed.

Not merging.

The first version of this branch recorded three unshipped names and left them. A gap that can be
closed is closed; only the one whose name was wrong and whose reason was right stays as a row.

**`hop=` and `task=` on `needs_approval` — built.** Without them the predicate evaluated against
the receiver's whole candidate set while `execute` evaluates against the hop alone (§2.3), so it
answered "a human is needed" for a call `execute` then refuses: the framework surfaces an approval
item, a human says yes, the call fails anyway. Never an authority hole — `Control.execute` is the
enforcement point and refuses either way — but it cost the framework its own approval item and left
a receipt nobody could explain. `T128c` drives predicate and `execute` over one hop and asserts they
agree, with the no-hop case as its negative control so the fixture is not one where everything is
denied. Mutated back to the old call: fails.

**`ctrlrun.hop/v1`'s key set — fixed in §6.2**, which listed eight of the eleven keys the document
carries. `T506b` parses the table out of the spec and compares it to the emitted document, so the
two cannot drift again. Mutated: one row deleted from the spec, T506b fails naming it.

**`ssl_context=` stays unbuilt, and §9.4 is now one row saying why.** Check 3 lives where the
gateway builds its forwarder, which is per-gateway where a parameter on the shared request helper
would be per-process; adding it now would be a second way to configure one pin.

**T480 — both halves.** The parser half asserts no identity field from `params.metadata` becomes a
field of the request, searching the parsed object rather than grepping for a string. The end-to-end
half asserts the receipt names the `IdentityProvider`'s principal and that the payload's name
reaches no receipt and no event. Mutated: `agent_id` read out of the bag, both fail.

**T502 — and the audit changed the answer.** Two of its four rows already existed under their own
numbers (`test_T461...`, `test_T462...`), which is §9.4's defect in another place: a table that
describes the tree, checked by a human reading both. `T502a` is the resumed *observed* receipt's
spend, which `T439` and `T447` cover separately and neither covers together. `T502b` is the doubled
`ACTION_DENIED`, the one row with no test under any name.

**T502a's first version asserted the opposite of the contract and was wrong.** It read an empty
ledger as meaning an observed resumed receipt should report no spend. §4.2.1a is explicit: every
number on an observed receipt is a counterfactual, and without it nothing records what the action
would have cost, so a budget cannot be sized from an observed run. The code was right.

**T502b's first version was green against its own mutation.** It filtered on `budget_exhausted`,
and `announce` guards the *unmeasurable* refusal, so the scenario never reached the guarded line.
Rebuilt on a negative amount. Mutated: `announce` ignored, T502b fails.

Full gate: all checks passed.

Signed-off-by: arpan <contact@arpanghoshal.com>
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 20 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b2b83a9b-4955-4af5-974f-88b7b8ed9c84

📥 Commits

Reviewing files that changed from the base of the PR and between aecebba and f35cc1e.

📒 Files selected for processing (8)
  • docs/SPEC-v0.10.md
  • src/ctrlrun/adapter.py
  • tests/test_adapter.py
  • tests/test_budget_holds.py
  • tests/test_gateway_server.py
  • tests/test_hop_surfaces.py
  • tests/test_mcp.py
  • tests/test_repository_signals.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rohanrkamath
rohanrkamath merged commit e7d1115 into main Sep 14, 2026
16 checks passed
@rohanrkamath
rohanrkamath deleted the fix/section-9-build-and-close branch September 14, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants