Skip to content

Write docs/SPEC-v0.7.md, the Execution boundary contract - #135

Merged
arpanghoshal merged 5 commits into
mainfrom
spec/v0.7
Sep 11, 2026
Merged

Write docs/SPEC-v0.7.md, the Execution boundary contract#135
arpanghoshal merged 5 commits into
mainfrom
spec/v0.7

Conversation

@arpanghoshal

@arpanghoshal arpanghoshal commented Sep 11, 2026

Copy link
Copy Markdown
Member

docs/SPEC-v0.7.md, the v0.7 "Execution boundary" contract, and a CHANGELOG [Unreleased] entry naming it. Documentation only: no src/ change, no version bump, nothing in ctrlrun-docs, no ROADMAP edit.

Summary

v0.7 asks whether the kernel's guarantees hold at the edges it does not control. Five items, each specified against the code rather than against the plan:

  • §2 ctrlrun.transport: the NotExecuted rule moves from gateway/outcome.py into core (moved, not copied; the gateway calls the same function object). NotExecuted only from the classifier's own connect() on a fresh connection that offered no request byte; everything after the first byte is the original exception. No HTTP status is ever NotExecuted; redirects are not followed; no parameter widens FAILED. The httpx variant stays in ctrlrun[gateway].
  • §3 clock skew: measured at open and on the expired-lease path, never on the happy path; midpoint against clock_timestamp() with half the round trip as the bound, so latency is never reported as skew. Observation only: lease evaluation is unchanged.
  • §4 the idempotency token: ctrlrun.idempotency_token(), derived from (effect_key, attempt) through canonical_bytes with a domain tag, rendered as a 36-character UUIDv8 (inside the verified limits of Stripe, Adyen, Square and PayPal). Pinned: ("refund:txn_1", 1) gives 382ee448-97da-8107-b674-8c253650d93f.
  • §5 max_attempts: decided on the attempt number the store assigned, after the reservation and before the executor, with a read-before-the-approval-gate fast path tested separately. The amendment text for SPEC-v0.1.md §5.4 is written out in §5.8.
  • §6 precondition fingerprints: the first sentence says it narrows a window and does not close one. Fetched strictly before the reservation, hashed, fail-closed; ApprovalMismatch with three distinct reasons; the approval is left granted; §6.7 states the residual window and T261b opens it on purpose.
  • §7 gives every v0.3 §4.3.1 row its answer to "does it recheck preconditions", including the nine that say no. §8 is T209 to T271 plus G12 to G16 in v0.4 §2.1's form. §12 is headings only.

Where each settled decision landed

Decision Section
D1 one rule, moved §2.1
D2 NotExecuted chained, or the original re-raised §2.2
D3 what counts as proven §2.3 (wording corrected, see below)
D4 no HTTP status is NotExecuted §2.4
D5 the httpx variant §2.5
D6 no parameter changes a classification §2.6
D7 lease evaluation unchanged §3.2
D8 only a store with its own clock §3.3
D9 latency is not skew §3.4
D10 measured at open, always §3.5
D11 the derivation §4.2
D12 one accessor, InvalidArgument outside an executor §4.3
D13 changes on renewal, stable within an attempt §4.4
D14 no receipt field §4.5
D15 a reconciliation handle §4.7 (§4.6 adds the cross-deployment warning)
D16 a policy key, per action §5.3
D17 absent means no ceiling §5.4
D18 decided on the assigned attempt number §5.2, §5.5; its store premise is item 3a, §5.6
D19 opt-in §6.2
D20 hashed, never raw §6.2, §6.10
D21 captured at request time §6.2
D22 rechecked before the reservation, approval left granted §6.2, §6.3
D23 every failure closed, never a skip §6.4, §6.5
D24 APPROVE only; not ALLOW, not resume §6.8, §7
D25 ctrlrun.receipt/v4 §6.11
D26 a general hook §6.9
D27 the catalogue moves once §8.9, §9.4

For the maintainer

Updated after the independent spec review (7 blocking, 9 non-blocking, all fixed in the spec; the review round is summarised below).

Decisions you may overrule:

  • A new build item, "Item 3a: attempt numbers never repeat" (§1, §5.6). Its own PR, stacked immediately before item 3, independently reviewed because it changes a store. It fixes two Postgres defects the review confirmed: a stale renewal can write an attempt number a second time (postgres.py:787-800; fix: AND attempt = <planned-from>), and a lost COMMIT's re-issue discards its result and returns the originally planned attempt (postgres.py:602-622, 660-663, 696-699; fix: return what the re-issue wrote). New rule: every reservation method MUST return the attempt number it actually wrote. Tests T246 and T246b open both windows with the test proxy. On SQLite the extra WHERE is an equivalent mutant and the spec says so. Both fixes are changes inside existing methods, not new methods.
  • Verify's network rule and G12 (§8.9 G12, T230). The rule becomes "verify opens no connection except to the store --store-url names and to loopback listeners it bound itself", because the old "nothing leaves the host" was already false under a remote --store-url. G12 drives ctrlrun.transport.HTTPConnection("127.0.0.1", port) directly, not urlopen (which follows HTTP_PROXY and would false-fail). T107's guard admits a connect only to a (127.0.0.1, port) pair the run itself bound through the patched socket class, refuses any other bind (so a 0.0.0.0 listener fails), matches the literal string rather than is_loopback, checks the host inside connect itself, and refuses localhost and ::1. The tradeoff: an AF_UNIX socket or a socketpair would keep v0.4 §3.7's words verbatim, but a socketpair is caller-supplied, which §2.3 says never yields NotExecuted, so G12 would lose its control; and AF_UNIX needs either a verify-only connect path or a test seam, which §2.6 forbids. It keeps the words and loses the evidence. Item 6 reconciles README.md:258's "with no network".

The four open questions:

  • O1, how skew reaches the EventSink (§3.6). The store retains its latest measurement as clock_skew: ClockSkew | None; Control reads it with getattr at the start of execute/resume and after an AmbiguousEffect reservation refusal, uses it only if it is a ctrlrun.state.ClockSkew (a third-party store must use that class; a look-alike is ignored), logs at WARNING once per store per kind of error rather than raising, and appends CLOCK_SKEW_DETECTED through its normal path. Rejected: a constructor callback and a logger bridge. Please judge one thing: the spec now calls clock_skew what it is in effect, an optional member of the store contract (read from any store, invited from third parties, graded by T214), listed in §9.2 as an optional store attribute. It is not on the StateStore protocol. Whether v0.6 §9.2's "no new store method" bar should cover it is your call. A wrapper that does not forward it silently drops skew reporting; decisions are unaffected.
  • O2, the threshold (§3.7). One second by default, settable as PostgresStateStore(clock_skew_threshold=...) within (0, DEFAULT_LEASE]. No value turns detection off.
  • O3, what G12 and G14 grade (§8.9). G12 as above, on separate keys for observable and control; N/A built through unselected() like G10's, never because of the environment. G14 requires an effect key a renewal can reach, with G5's N/A plus a new one where every candidate declares max_attempts: 1. Item 4 amends G5's selection the same way, because under max_attempts: 1 G5's control would have failed a correct kernel.
  • O4, the names (§9). ctrlrun.idempotency_token(), max_attempts (with ctrlrun.policy/v5), CLOCK_SKEW_DETECTED, approvals.precondition_fingerprint via migration 0005_precondition_fingerprint. §9.2 lists every other name.

Where the spec departs from the plan, each argued in it:

  1. Tests start at T209, not T182. SPEC-mcp-operator.md owns T182 to T193 and SPEC-scan.md owns T194 to T208.
  2. D18's store premise is false on Postgres today, hence item 3a above.
  3. "One human approval buys unlimited dispatches" is wrong in enforce mode: each renewal of an APPROVE action needs a new granted approval (not necessarily a human one: scripted providers, automated wait=True loops, the operator server and approvals granted ahead of a gateway all grant). Observe mode needs none. The unbounded case is the ALLOW action. The adapter path (needs_approvalControl.evaluate, which cannot see the ceiling) can still put a human in front of an attempt the fast path will refuse; §5.5 records the exception rather than teaching evaluate the ceiling.
  4. D25 needs the rehash rule changed (amends v0.6 §6.4), and the round-2 review changed how: hash what was stored. A receipt read from a store keeps the document it came from in a private field, and chain_hash() hashes that document, so an added key, a relabel and a removed or unknown schema are hash mismatches by construction, from_dict never raises inside receipts() (one tampered row cannot blind every reader), and no public name is added. Each schema's key set is its released writers': v1 19 keys, v2 21, v3 26, v4 28.
  5. max_attempts needs ctrlrun.policy/v5, a superset of v4; item 4 fixes the three equality gates that would refuse a v5 document using earlier keys (policy.py:912, 931, 1171). A standalone authority document labelled v5 is accepted on purpose.
  6. G16's N/A cannot be "no fingerprint", because providers are named in code; G16 is graded with verify's own provider and a printed note.
  7. D1: the gateway's 401/challenged-403 rule stays in outcome.py and is now stated as the product's one status-to-FAILED path, resting on the MCP peer's word; ctrlrun.gateway.transport.request does not apply it.
  8. D3's wording: the byte mark is set before the first byte is offered, never "counted per successful call"; T229b pins http.client's single write path on 3.11 to 3.14.
  9. The token has no deployment discriminator (§4.6): two stores sharing a provider account whose effect-key strings coincide for different effects get identical tokens, and the provider can replay one tenant's success to the other, a false COMMITTED. Decided: an effect key must name its effect uniquely across every store sharing a provider account, including the deployment wherever a placeholder is not unique at the provider; verify prints a note beneath G14 because the kernel cannot check it. Rejected: adding the environment (stable, but does not separate two stores in one environment) and a store id (none exists).
  10. Operators must stop every 0.6 process before any caller uses preconditions= (§6.11): a 0.6.1 process running when migration 0005 applies consumes fingerprinted approvals without a recheck, and the kernel cannot detect it.
  11. A fingerprinted approval denies the gateway and ACS path until it expires (§6.4): both keep presenting the newest granted approval and create no fresh request. Bounded by expires_at; fail-closed.

Nothing made me stop. No new StateStore method, no new error type, no change to lease evaluation. The nearest are item 3a (inside existing methods) and clock_skew (an optional attribute, flagged above).

One convention I did not follow. The pre-push hook refuses AI-tool references in commit metadata and in the tree, and the commit-msg hook strips co-author trailers, so the commits carry none and this description leaves the attribution line off, matching recent PRs.

Review round 1

# Finding Where it landed
1 Postgres stale renewal reuses an attempt number item 3a; §5.6; T246
2 Lost-COMMIT path returns a stale attempt item 3a; §5.5's MUST, §5.6; T246b
3 max_attempts: 1 false-fails G5, G14 §8.9 (G5 and G14 selection, G12 separate keys); T270
4 G15 only exercised the fast path §5.5 reconcile route; §8.9 G15; T245, T245b
5 Two holes in the rehash rule §6.11; T265; §10
6 False N/A reasons §8.9 G12, G15, G16 through unselected(); G1's inherited weakness noted
7 Verify's network rule §1.4 item 5; §8.9 G12; T230; tradeoff above
9 "New yes" holds in enforce mode, "granted" not "human", adapter exception §1.4 item 2, §5.2, §5.5, CHANGELOG
10 Policy v5 equality gates, standalone authority v5 §5.3; T244
11 clock_skew is an optional store attribute §1.1, §3.6, §9.2, §10; T216
12 D3 pinned per Python; the 401 rule stated plainly; §2.5's evidence claim §2.4, §2.5; T229b
13 bool refusal load-bearing; cross-deployment tokens; G14's control rationale §4.2, §4.6, §8.9 G14; T235
14 Rolling upgrades §6.11
15 §6.3 overclaims; T268's word list §6.3; T268
16 Gateway/ACS denial loop; G16 note and vacuous assertion; ARCHITECTURE citation §6.4, §7, §8.9 G16, §3.6; T257

Review round 2

# Finding Where it landed
1 The rehash mechanism would have raised out of receipts() and let an added key verify §6.11 rewritten to "hash what was stored"; T265; §10
2 T246b's insert variant opened the wrong window §5.6; T246b split into renewal and insert variants
3 The G5/G14 ceiling N/A sentence was false for a mixed document §8.9 G14 and the G5 amendment, with precedence; T270
4 §4.6 led with the harmless case §4.6 rewritten; note beneath G14
5 T247 claimed a window it cannot open T247
6 Assert the refusal's reason; T245b needs three calls; the fast path refuses only FAILED; what max_attempts: 1 leaves ungraded §5.5, T245, T245b, §8.9 G15 and the G5 amendment
7 The loopback guard admitted more than the rule §8.9 G12, T230, §1.4 item 5, §9.6
8 clock_skew type and error handling §3.6, T214, T216
9 T229b covered plaintext only T229b

Checks

  • PYTHON=.venv/bin/python ./scripts/check.sh with CTRLRUN_TEST_POSTGRES set: ruff format, ruff check, mypy strict, 2951 passed (rerun after the review fixes, unchanged). Without it: 2900 passed, 45 skipped, the baseline at 724ae6e unchanged.
  • Docs generators against this branch: capabilities, cli, schemas, api, cookbook, readiness, badges all report no drift, as expected for a documentation-only change.
  • No sentence in §6 or §7 says the recheck prevents, closes, guarantees or ensures anything; the matches that remain are disclaimers ("does not close one") or the verify catalogue's own name.

This needs the separate spec review before it merges, per the build notes.

Summary by CodeRabbit

  • Documentation
    • Added an Unreleased entry documenting the v0.7 “Execution boundary” contract.
    • Documented execution classification, clock-skew detection, idempotency tokens, retry ceilings, and precondition fingerprints.
    • Added details on test coverage, public guarantees, receipt rendering, approval behavior, and corrected implementation assumptions.

A delta over v0.1 to v0.6. It asks whether the kernel's guarantees hold at
the edges it does not control: an executor deciding FAILED, a store clock on
another host, and a world that moved after a human said yes. Five items: the
NotExecuted classifier promoted from gateway/outcome.py into core as
ctrlrun.transport; clock-skew detection; a provider idempotency token from
(effect_key, attempt); max_attempts, written as an amendment to v0.1 §5.4;
and precondition fingerprints, which narrow the decision-to-execution window
and do not close it.

Written against the code rather than the plan, and §1.4 records the nine
places they disagreed: T182 to T208 are taken, so tests start at T209; a
renewal of an approved action needs a new approval, so one yes buys one
dispatch; the Postgres renewal UPDATE can reuse an attempt number; a receipt
schema bump would break every chained 0.6 receipt unless a receipt renders
under its own schema; verify's network guard refuses loopback; injected
clocks will measure skew; max_attempts needs ctrlrun.policy/v5; the gateway
raises NotExecuted unchained; and the byte count must be taken before the
send, not after.

Documentation only. No src change, no version bump, nothing in ctrlrun-docs.
SPEC-v0.7 §1.4 item 7 pointed at policy.py:115-127, which starts inside the
top-level key map; the entry-key gate is 118-127.
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5b100842-aa90-48a1-8a1b-7963e0ce3f6b

📥 Commits

Reviewing files that changed from the base of the PR and between 34682d4 and 6444c90.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • docs/SPEC-v0.7.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The changelog expands the Unreleased Documentation entry for the v0.7 “Execution boundary” contract. It records contract details, approval-dispatch rules, implementation assumptions, and corrections identified during draft review. No code or public declarations change.

Changes

Execution boundary documentation

Layer / File(s) Summary
Document v0.7 contract and reviewed corrections
CHANGELOG.md
Updates the v0.7 contract details, approval-dispatch wording, attempt-number sequencing, receipt rendering, max_attempts policy, and corrections from the independent draft review.

Priority: ⬇️ Low

Estimated code review effort: 1 (Trivial) | ~3 minutes

Change: Other

Merge Risk: ⚪ Minimal · up to 6444c

The documentation-only change has no identified merge-blocking risk.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding the v0.7 Execution boundary specification at docs/SPEC-v0.7.md.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch spec/v0.7

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.

Item 3a, a new build item stacked before item 3: the Postgres renewal
compare-and-set is conditioned on the planned-from attempt, and a lost
COMMIT's re-issue returns the attempt it wrote; every reservation method
must return the number it actually wrote (§5.6, T246, T246b).

G15 reaches the post-reservation check through a reconcile, so it can no
longer pass with that check deleted. G5 and G14 skip an action whose
ceiling forbids a renewal, and G12 uses separate keys. The rehash rule
renders every schema under its own label, parses only the declared keys,
and decides unchained by seq alone. N/A reasons go through unselected().
Verify's network rule is restated as it is actually true, with G12 on a
loopback literal and HTTPConnection.

Smaller: enforce mode and granted, not human; the adapter exception to the
fast path; the policy v5 superset gates; clock_skew as an optional store
attribute; the gateway's 401 rule stated plainly; per-Python pin of the
write path; cross-deployment tokens; rolling upgrades; the gateway and ACS
denial bound; wording in §6.3 and T268's word list.
The rehash rule becomes "hash what was stored": a receipt read from a store
keeps its document in a private field and chain_hash() hashes it, so an
added key, a relabel and a removed or unknown schema are hash mismatches,
from_dict never raises inside receipts(), and no public name is added. Each
schema's released key set is stated.

T246b opens the insert path's real window, between the re-read and the
re-issue's planning read. The G5/G14 ceiling N/A sentence is scoped to
drivable actions, with precedence, and T270 runs the mixed document.

Smaller: §4.6 leads with different effects sharing a key; T247 claims no
window; refusals assert their reason and T245b uses three calls; the fast
path refuses only a FAILED record; verify's guard admits only ports the run
bound; clock_skew must be ctrlrun.state.ClockSkew and fails the store case
otherwise; T229b covers TLS.
The stored document a receipt keeps is not an init parameter and does not
survive replace(): the store read path sets it after its own replace, and
put_receipt hashes the dict it serializes, so a receipt altered in memory
the way G11 alters one is still content_altered, and a read-back receipt
written again reads back clean. T265 gains that case, and its mutation
sentence now names the one tamper that catches the mutation.

The loopback guard records getsockname() after bind and refuses AF_UNIX on
purpose. The G5/G14 ceiling sentence says "verify can select", covering the
authority axis, and T270 runs the ungranted case.
@arpanghoshal
arpanghoshal merged commit f14b178 into main Sep 11, 2026
14 checks passed
@arpanghoshal
arpanghoshal deleted the spec/v0.7 branch September 11, 2026 09:31
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.

1 participant