Skip to content

feat(cp): openab-cp control plane — registry, router, policy (PR 1/4) - #1469

Merged
thepagent merged 11 commits into
mainfrom
feat/openab-cp
Aug 14, 2026
Merged

feat(cp): openab-cp control plane — registry, router, policy (PR 1/4)#1469
thepagent merged 11 commits into
mainfrom
feat/openab-cp

Conversation

@chaodu-agent

@chaodu-agent chaodu-agent commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

What problem does this solve?

First implementation slice (PR 1 of 4) of the Agent Control Plane ADR (#1465): the standalone openab-cp binary — wire protocol, identity-bound registry, CP-authoritative policy engine, and delegation router. Agents delegate to each other over ACP/WS instead of round-tripping through Discord/Telegram.

Planned follow-up PRs: (2) OAB-side [control_plane] config + registration client + worker pool wiring, (3) MCP facade over UDS + openab agent <verb> CLI, (4) streaming + observer endpoint.

What's in this PR

  • crates/openab-cp (new workspace member, standalone binary per the openab-gateway precedent):
    • proto.rs — versioned JSON-RPC 2.0 envelopes; cp/register(+ack), cp/heartbeat, cp/delegate, cp/delegate_result, cp/cancel; distinct machine-actionable error codes (IDENTITY_MISMATCH, POLICY_DENIED, NO_TARGET, SATURATED, DEADLINE_EXCEEDED, TARGET_DISCONNECTED, DUPLICATE_DELEGATION, UNSUPPORTED_VERSION)
    • config.rs — CP-owned identity table (auth key → immutable namespace/name/type claims, ${ENV_VAR} expansion, constant-time key lookup, duplicate-key rejection), per-namespace policy overrides
    • registry.rs — instance registry with replica semantics (rolling deploys route new work to the newest healthy instance), lease/heartbeat expiry, saturation-aware selection distinguishing NO_TARGET from SATURATED
    • policy.rs — CP-authoritative checks: initiator role, depth, cycle (incl. self), namespace boundary, deadline sanity/cap/parent-budget
    • router.rs — in-flight table; CP-constructed chain (callers pass only parent_delegation_id; ancestry derives from authenticated identities — unforgeable); deadline sweep (synthesized timeout + best-effort cp/cancel); disconnect handling both directions; result size truncation; idempotency
    • server.rs — axum WS server; auth via Authorization: Bearer at upgrade (keys never in URLs); mandatory cp/register first frame verified against the key's bound identity
  • ADR amendments (docs/adr/agent-control-plane.md): new "v1 contract amendments" section freezing the docs(adr): agent control plane for direct inter-agent delegation #1465 review findings (identity binding, CP-authoritative policy, restart/saturation/timeout semantics, result cap, idempotency); §11 Q1 streaming resolved as committed fast-follow with the observer endpoint sketch.

Review Contract

Goal

Ship a correct, tested control-plane core (registry + router + policy + wire contract) that later PRs (runtime client, facade, streaming) build on without protocol changes, implementing the frozen ADR baseline including the #1465 review checklist items.

Non-goals

No OAB-runtime-side code: no [control_plane] config section, no registration client, no MCP facade/UDS/CLI, no streaming/observer endpoint (PRs 2–4). No CP HA (single instance; restart semantics are defined and implemented). No durable state, queueing, or delegation history. No Docker/Helm packaging.

Accepted Residual Risks

  • Wire contract is exercised by unit tests, not yet by a real OAB runtime client; PR 2 integration may surface adjustments. Mitigation: protocol_version field + UNSUPPORTED_VERSION rejection gives a compatibility gate.
  • Registered-runtime JSON-RPC responses to CP-forwarded frames are logged and dropped (correlation is by delegation_id, not rpc id). Acceptable for v1; revisit if per-frame acks become necessary.
  • cp/cancel to a serving runtime is best-effort; a runtime that ignores it burns tokens until its own deadline handling stops it. The propagated deadline is the hard upper bound.

Acceptance Criteria

  • Registration rejects: wrong first frame, unsupported protocol version, identity claim mismatch (namespace/name/type each), empty instance_id — all tested
  • Policy denies: worker initiation (default), depth > namespace max, cycles incl. self-delegation, cross-namespace, past/over-cap/over-parent deadlines — all tested
  • Router: happy-path roundtrip with CP-stamped chain; duplicate id rejection; saturation fast-fail; deadline sweep synthesizing timeout+cancel; worker/initiator disconnect handling; spoofed-completion rejection; late-result drop (CP restart case); oversized-result truncation — all tested
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test -p openab-cp (42/42), cargo check --workspace all pass
  • No mockall, no network/fs in unit tests, tests inline #[cfg(test)]

Follow-ups

  • PR 2: [control_plane] OAB config + outbound registration client (gateway.rs backoff shape) + worker pool wiring
  • PR 3: MCP facade (spawn_agent, check_delegation, list_agents, cancel_delegation) over UDS + CLI
  • PR 4: streaming frames + read-only /observe endpoint + openab-cp tail
  • Packaging (Dockerfile/Helm) once PR 2 makes the binary deployable end-to-end

Verified

On macmini at the squashed head (content-identical to ac9c023):

  • cargo fmt -p openab-cp -- --check — clean
  • cargo clippy -p openab-cp --all-targets -- -D warnings — clean
  • cargo test -p openab-cp — 42 passed, 0 failed
  • cargo check --workspace — passes with the new member

⚠️ Breaking Change — admission is required on cp/delegate_result and cp/cancel

The CP wire protocol gains a required admission token on two frames:
cp/delegate_result (round-8 revision) and cp/cancel (this revision, both
directions). Both are required fields, so this is a wire-breaking protocol
change: a runtime built against the pre-token contract has every result and
every cancel refused with INVALID_PARAMS once the CP is upgraded.

There is no backward-compatible spelling. An optional token would mean "apply
this frame to whatever holds the delegation_id right now", which is precisely
the misdelivery the token exists to close: delegation_id is client-supplied
and cancel-then-retry legitimately reuses it, so with a single replica the retry
re-admits the same id to the same worker. Without a token on the wire, a late
result for the cancelled admission is delivered to the initiator as the retry's
terminal frame and commits the retry — releasing capacity it still occupies —
and a stale or CP-synthesized cancel for the ended admission aborts the retry's
work at the worker.

Migration for runtime authors

  • Serving runtimes: copy admission from the forwarded cp/delegate into
    the matching cp/delegate_result. Match incoming cp/cancel on the token,
    not on delegation_id: a CP-synthesized cancel names the admission it ends
    and can arrive after a forward that reused the same id.
  • Initiators: read admission from the cp/delegate ack. Send it on
    cp/cancel to name the admission you mean to abort — a cancel naming a
    superseded admission is refused, which is what prevents a retried cancel from
    killing the re-admission that replaced its target. Correlate terminal frames
    on admission, not on delegation_id ("first terminal frame per admission
    wins").
  • Refusals are indistinguishable by design. An unknown id, another
    instance's live id, and your own id under a superseded admission all return
    the same byte-identical POLICY_DENIED. Treat a refusal as "not mine / not
    live" and reconcile against your own state; the CP deliberately reports
    nothing about whether an id is currently re-admitted.

No shipped clients are affected at this point in the stack — every serving
runtime in PRs 2–4 learns the token from the forwarded cp/delegate — so the
migration is mechanical. It is not, however, silent or optional.

Contract details: docs/adr/agent-control-plane.md §4 (v1 contract
amendments, "Admissions carry a protocol-visible token"); runtime-author
summary in docs/control-plane.md ("Client behavior to expect").

@github-actions

Copy link
Copy Markdown

👥 Top 3 recent committers of changed files

Login Last commit Commit File
chaodu-agent 2026-08-10 448b05fb docs/adr/agent-control-plane.md
brettchien 2026-08-04 3ace7de3 Cargo.toml
canyugs 2026-07-30 c5a75ac6 Cargo.toml

Updated for e7c29c8

@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

@chaodu-agent
chaodu-agent force-pushed the feat/openab-cp branch 2 times, most recently from 2f15fe8 to 81cd5f7 Compare August 11, 2026 03:40
… PR 1/4)

Implements the first slice of docs/adr/agent-control-plane.md: the
standalone openab-cp binary with wire protocol, identity-bound registry,
CP-authoritative policy engine, and delegation router.

Addresses round-1 review findings: CP-generated registration handles for
all ownership (F1), atomic delegation admission with insert-before-send
(F2), parent delegation bound to its serving instance (F3), JSON-RPC 2.0
envelope validation (F4), transport/prompt/queue resource bounds (F5),
lease-only heartbeats + least-loaded label scheduling (F6), and unrelated
artifacts removed from the diff (F7).
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

All round-1 findings addressed in e2853b4 (branch rewritten; the unrelated artifacts are gone from the diff entirely).

# Finding Resolution
F1 🔴 Client-selected instance_id as registry/ownership key ✅ Registrations now keyed by a CP-generated handle (registry.rs); all ownership checks (completion, cancel, parent linkage, teardown) compare handles. A colliding instance_id cannot replace or tear down another connection's entry — regression tests colliding_instance_id_cannot_replace_other_registration, result_from_wrong_handle_dropped_and_restored
F2 🔴 Check-then-act admission; send-before-insert ✅ Admission (duplicate check → parent lookup → selection → capacity reservation → in-flight insert) is one atomic sequence under an admission lock; the in-flight entry exists before the forward frame is sent, with rollback on send failure — tests inflight_exists_before_target_receives_frame, send_failure_rolls_back_reservation
F3 🔴 Any live parent_delegation_id accepted ✅ The caller must be the instance serving the parent (p.to_handle == from_handle); unknown and unauthorized parents return the same non-enumerating error — test chain_extends_through_parent_and_foreign_parent_rejected (foreign-parent negative case included)
F4 🟡 JSON-RPC 2.0 envelope not validated jsonrpc field added to the incoming type; require_request_envelope() enforces "2.0" + request id for all cp/* methods, returning INVALID_REQUEST (-32600) — tests request_envelope_validation, register_invalid_envelope_rejected
F5 🟡 No pre-allocation resource bounds ✅ WS transport enforces max_frame_bytes (default 1 MiB) via max_message_size/max_frame_size before parsing; max_prompt_bytes rejects oversized prompts; per-connection outbound queues are bounded (256) with disconnect-on-overflow (try_send everywhere)
F6 🟡 Heartbeat count merge; recency-over-load label scheduling ✅ Heartbeats refresh the lease only — CP-owned counts are authoritative (test heartbeat_does_not_mutate_session_count); label selection is least-loaded-first with recency tie-break, exact-name keeps the newest-replica rule (inverse recency/load test label_selection_least_loaded_first)
F7 🟡 Unrelated artifacts in diff ✅ Branch rewritten from main; diff now contains only crates/openab-cp, workspace Cargo.toml/Cargo.lock, the ADR amendment, and the Dockerfile stub-layer updates required for the new workspace member

ADR's "v1 contract amendments" section updated to record the handle-based ownership, envelope validation, and resource-bound semantics.

Verified at e2853b4 on the build host: cargo fmt --check OK, cargo clippy --all-targets -- -D warnings clean, cargo test -p openab-cp 48/48 (up from 42 — six new regression tests for F1/F2/F3/F4/F6), cargo check --workspace passes.

…ult cap

Round-2 review F4/F5: bearer keys never cross cleartext non-loopback TCP
without an explicit allow_insecure_bind acknowledgment (TLS proxy or
private network required), and result truncation counts the marker
against max_result_bytes.
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Round-2 response — note the round-2 review examined stale head 2842d264 (pre-rewrite); findings F1/F2/F3/F6/F7 were already resolved in the branch rewrite (e2853b4, see the previous response comment for the per-finding test evidence). The two findings that applied to current code are now fixed in 33ed835:

# Finding Resolution
F4 🔴 Bearer credentials over cleartext non-loopback TCP ✅ Default bind is now 127.0.0.1:9800; validate() rejects any non-loopback bind unless allow_insecure_bind = true is set explicitly, documented as requiring a TLS-terminating proxy (wss://) or a private network (tailnet) in front. cp.toml.example updated. Test: non_loopback_bind_requires_override (covers 0.0.0.0 rejection, override acceptance, and loopback variants incl. [::1])
F5 🟡 (marker overflow) Truncation marker pushed results past max_result_bytes ✅ The marker now counts against the cap (floor_char_boundary budget math); degenerate tiny caps also never exceed the limit. Test: oversized_result_truncated asserts result.len() <= cap for both normal and degenerate caps

The remainder of F5 (bounded outbound queues, max_frame_bytes WS transport limit, max_prompt_bytes rejection) was already in e2853b4.

Per the review contract's stopping rule, round 3 should verify these fixes against head 33ed835 and check for regressions only.

Verified at 33ed835 on the build host: fmt clean, clippy --all-targets -D warnings clean, 49/49 tests, cargo check --workspace passes.

@chaodu-obk

This comment has been minimized.

…mespace-scoped delegation ids, admission bounds

F1 — lease expiry left a zombie connection. The sweeper deregistered the
instance and failed its in-flight work but never told the connection task to
stop, and the task holds its own clone of the outbound sender, so `rx.recv()`
never ended. The socket stayed open bound to a registration that no longer
existed: every later frame (heartbeats included) hit `registry.get(handle) ->
None` and got no reply, and the client could not recover because registration
is first-frame-only. Each connection now creates a `watch` shutdown signal
(`registry::shutdown_signal`), subscribes to it before registering, and hands
a clone to the registry, which keeps it in a CP-internal `Entry` alongside the
public `Instance` (nothing routed or serialized carries it). `sweep_leases`
calls `signal_shutdown` before `deregister`, and the main loop `select!`s on
the signal, sends a Close frame, and tears down; the client then reconnects,
re-authenticates, and registers again. Proven end-to-end over a real loopback
socket by tests/ws_lifecycle.rs
`lease_expiry_closes_the_connection_and_permits_reregistration`, and at the
sweeper level by `sweep_leases_signals_the_connection_before_dropping_it` /
registry `signal_shutdown_reaches_the_owning_connection`.

F2 — remove-then-check race in `complete` and `cancel`. Both removed the
in-flight entry, released the lock, validated the caller's handle, and
reinserted on mismatch. A genuine result landing in that window saw an unknown
id and was silently dropped, the entry was briefly invisible to the deadline
sweep, and the wrong-handle frame's reinsert left the delegation to stall to
its deadline. Ownership is now validated under the SAME lock acquisition that
removes: `get()`, check the handle, `remove()` only on success, never reinsert.
Proven by `wrong_handle_result_never_hides_the_genuine_one` (both frame orders),
`genuine_result_survives_concurrent_non_owner_frames` (200 barrier-synchronised
races), and `refused_cancel_leaves_the_delegation_cancellable` for the cancel
path. The pre-existing test `result_from_wrong_handle_dropped_and_restored` was
renamed to `result_from_wrong_handle_dropped_and_entry_untouched`: its name
asserted the remove-and-reinsert behaviour this commit removes. Its body and
assertions are unchanged.

F3 — global delegation_id keyspace crossed namespaces. The in-flight table was
keyed on the client-supplied `delegation_id` alone, so one namespace's ids were
observable from another: a colliding id was denied with `DUPLICATE_DELEGATION`,
and `cp/cancel` distinguished "no such id" (`INVALID_PARAMS`) from "someone
else's live id" (`POLICY_DENIED`) — a cross-tenant existence oracle. The table
is now keyed by a composite `DelegationKey { namespace, delegation_id }`, and
parent-chain resolution is scoped to the caller's namespace too, so a foreign
parent id cannot be borrowed for its trusted chain and deadline budget. The
namespace always comes from the authenticated registration behind the handle,
never from the frame. `cp/cancel` returns one indistinguishable error — same
code and same message — for an unknown id and for not-the-initiator; only the
CP's own logs keep the distinction. Proven by
`same_delegation_id_in_two_namespaces_is_independent` (one `d-1` live in prod
and dev at once, each completing to its own initiator),
`parent_lookup_is_namespace_scoped`, and `cancel_refusals_are_byte_identical`
(the two error objects compared as serialized JSON).

F4 — unbounded authenticated pre-registration sockets. A valid key could open
unlimited sockets and park them in the registration loop forever, kept alive by
pings. Two bounds, both new config fields with serde defaults so absent fields
keep working (documented in cp.toml.example, rejected when zero by
`CpConfig::validate`): (a) `register_timeout_secs` (default 10) is a deadline on
the mandatory `cp/register` first frame, measured from the completed upgrade —
pings are skipped inside the timeout rather than extending it; (b)
`max_connections_per_identity` (default 8) is acquired in the upgrade handler,
before `on_upgrade`, so pre-registration sockets count too and an over-quota
peer is refused at the HTTP layer with 503. The slot is held by an RAII
`ConnPermit` whose `Drop` releases it, so no early return — including a failed
ack write, a rejected registration, or an upgrade that never completes — can
leak it. Proven by ws_lifecycle
`ping_only_pre_registration_socket_is_closed_at_the_deadline`,
`registration_after_the_deadline_is_not_accepted`,
`connection_quota_rejects_over_limit_and_recycles_on_disconnect`,
`pre_registration_sockets_count_against_the_quota`, and the unit tests
`conn_quota_bounds_and_recycles_slots` / `conn_quota_is_per_identity` /
`admission_bounds_default_and_are_validated`.

The end-to-end tests need a WebSocket client, added as a dev-dependency on
tokio-tungstenite 0.29 — the version axum 0.8.9 already resolves to, so
Cargo.lock gains only openab-cp's dependency edge and no new package version.

Audit follow-up: added genuine_cancel_survives_concurrent_non_owner_frames —
the cancel-side racing regression mirroring the complete-side one (200
barrier-synchronised rounds, non-initiator cancel racing the genuine
initiator cancel; genuine must always win, capacity released exactly once).
@chaodu-obk

This comment has been minimized.

…helper, ADR contract sync

F1  InFlight.namespace documented, not removed: the field is read by the
    observer event layer one PR up this stack (per-namespace cp/event fan-out
    in fail_instance/sweep_deadlines), and it is part of the entry's identity
    because delegation ids are only unique within their namespace. Removing
    it here would only force the stack to re-add it.

F2  Dropped the unused `uuid` dependency from crates/openab-cp/Cargo.toml —
    registration handles come from an AtomicU64, nothing generated a UUID.
    Cargo.lock regenerated (openab-cp's dep list loses `uuid`; the package
    itself stays, other crates still use it).

F3  Removed `codes::DEADLINE_EXCEEDED` instead of reserving it. Verified that
    admission already rejects an already-expired deadline: policy::check
    returns PolicyDenial::DeadlinePast, which router::delegate maps to
    POLICY_DENIED — so the reserved-for-admission rationale does not hold. A
    deadline that elapses in flight is not an error response either; the
    sweeper synthesizes a cp/delegate_result with status `timeout`. -32006 is
    left unassigned with a comment so the published numbering stays stable.

F4  Extracted the duplicated ownership-check-and-remove sequence from
    complete() and cancel() into one private helper, Router::claim(), returning
    Claim { Owned(InFlight), WrongOwner{..}, NotFound{..}, Unregistered }.
    The single-lock-acquisition property is preserved inside the helper: the
    owner comparison and the removal happen under one `inflight` lock, so a
    non-owner frame never makes the entry momentarily invisible. Callers keep
    their distinct logging and return shapes (None vs. byte-identical
    POLICY_DENIED). All pre-existing tests pass with zero non-comment changes
    inside the tests module — that is the behavior-preservation proof.

F5  Rewrote every `(review round-N FX)` comment tag across the crate into
    self-standing rationale, keeping the WHY prose and dropping the round
    index (src/**, plus tests/ws_lifecycle.rs, which a reviewer greps too).
    `grep -rniE 'review|round-[0-9]' crates/openab-cp/` is now empty.

F6  docs/adr/agent-control-plane.md §4 amended to the enforced contract:
    registration deadline (pings do not extend it; 1008 close), lease expiry
    actively closing the connection with reconnect + re-register as the only
    recovery, per-identity connection quota counted from the upgrade and
    RAII-released, and (namespace, delegation_id) scoping including the
    indistinguishable cancel refusals. The "CP restart semantics" section was
    corrected against the code rather than to the reviewer's phrasing: a
    restart is all leases expiring at once, but the CP cannot synthesize any
    failure because the table and the sockets die with the process —
    initiators reconcile against the deadline they already propagated.
    Synthesized failures are the live-CP path only, and they are
    **side-specific**: Router::fail_instance is a mutually exclusive branch —
    a dead serving instance sends `target_disconnected` to the initiator, a
    dead initiator sends a best-effort `cp/cancel` downstream (and releases
    the worker's reserved capacity), never both for one delegation. Only
    sweep_deadlines emits both frames, because there both peers are still
    connected. Two sentences that claimed lease expiry/disconnect of either
    side produces both outcomes were rewritten to match.

F7  CP-initiated closes now carry meaning: registration timeout and lease
    expiry both send WS close 1008 (policy violation) with reason
    "registration timeout" / "lease expired"; the over-quota upgrade's HTTP
    503 gets a body naming max_connections_per_identity. ws_lifecycle
    assertions extended (not weakened) to check code + reason and the 503
    body; verified they fail when the code/reason/body is mutated.

F13 handle_frame() now answers NOT_REGISTERED when registry.get(handle) is
    None — frames landing between a lease sweep and the connection close were
    dropped silently, indistinguishable from a hung CP. New unit test:
    heartbeat answered while registered, then the same frame on the swept
    handle returns error code NOT_REGISTERED correlated with the request id;
    a cp/delegate on a swept handle likewise never reaches the router.

F15 Added a module-level lock-hierarchy doc to router.rs: admission →
    inflight, never the reverse, with the registry lock called out as an
    independent lock never held together with `inflight`.

Verification (macmini, isolated worktree matching this branch so openab-core
resolves): 63 lib + 5 ws_lifecycle tests pass (baseline 62 + 5; +1 is the new
F13 test), cargo clippy -p openab-cp --all-targets -- -D warnings clean,
cargo fmt --check 0 diffs — all equal to or better than the b9850da baseline.
@chaodu-obk

This comment has been minimized.

@chaodu-obk

This comment has been minimized.

…ty bounds, panic-safe teardown

F1 (blocking) — admission identity is now protocol-visible. The round-6
generation stopped at the CP boundary: the wire carried only the reusable
delegation_id, so after cancel-then-retry re-admitted the same id to the same
worker, a late cp/delegate_result for the old admission A peeked the live B,
was delivered to the initiator as B's terminal frame, and committed B (peek
and commit both saw B, so B's own generation matched) — freeing capacity B
still occupied and leaving B's genuine result to be dropped as unknown.

- proto.rs: `admission` (AdmissionToken) added to DelegateAck (initiator
  learns it), DelegateForward (worker learns it), and DelegateResultParams as
  a REQUIRED field (worker MUST echo it; absent = INVALID_PARAMS, never a
  wildcard). CP-synthesized terminal frames build DelegateResultParams from
  the in-flight entry, so sweep `timeout` and `target_disconnected` carry the
  token of the admission they end.
- router.rs: peek_for_completion matches the echoed token against the live
  entry BEFORE the initiator-bound frame is built (delivery is the
  irreversible half). Mismatch → new Peek::StaleAdmission → Dropped, logged
  distinctly with both tokens, but answered with the same generic ack as any
  other drop: a distinguishable reply would tell the serving side whether an
  id is currently re-admitted (the oracle class round-3 F3 removed).
- Token design: the generation counter was ONE GLOBAL monotonic counter;
  putting that raw value on the wire would leak cross-namespace delegation
  volume. Commit matching only needs never-reuse per (namespace,
  delegation_id), so counters are now per namespace, held as the value guarded
  by the admission lock (BTreeMap<String, u64>) — minting already happened
  there, and this makes minting outside the admission sequence structurally
  impossible. Fail-closed exhaustion is preserved (a counter at the ceiling is
  never bumped, so it can never wrap and re-issue tokens) and is now also
  proven to be per-namespace: prod exhaustion does not refuse dev.
- Contract: ADR §4 amendment rewritten — the token's full round trip, the echo
  requirement, the stale-token drop, the namespace-scoping rationale — and
  "first terminal frame wins" is re-keyed per admission token (correlating on
  the reusable id lets a late frame for a superseded admission permanently
  mask the live one). docs/control-plane.md carries the same for runtime
  authors. cp/cancel deliberately does NOT carry the token yet; it lands with
  the runtime-client slice.

F2 — per-connection outbound BYTE budget. 256 entries is not a memory bound
when frame sizes are configured independently (~2 GiB at 8 MiB results).
registry.rs now owns OutboundBudget: bytes are reserved before enqueue,
released on dequeue and on teardown (FrameRx::drop drains and releases, so a
surviving sender clone never sees budget consumed by a dead connection).
FrameTx/FrameRx replace the bare mpsc alias; refusal semantics are unchanged
(SendRefused is still just "this peer cannot take the frame → treat as
disconnected"). Config `max_outbound_queue_bytes` (default 16 MiB, validated >
max_frame_bytes so one maximum-size frame always fits), documented in
cp.toml.example.

F3 — global in-flight bound. Config `max_inflight_delegations` (default 4096,
validated > 0), enforced at admission before any capacity is reserved,
refused with SATURATED naming the bound. DEVIATION with rationale: the bound
is the in-flight table's own length rather than a parallel counter, so no
removal path has to remember to decrement — commit, cancel, sweep,
fail_instance and the send-failure rollback all remove the entry and the count
follows by construction. A parallel counter is one refactor away from
drifting, and a drifted global bound wedges the whole CP. The regression walks
all five removal paths and re-admits after each.

F4 — default clamp for advertised capacity. Config
`default_max_delegated_sessions_cap` (default 16, validated > 0) applied via
CpConfig::effective_max_sessions to every identity with no
max_delegated_sessions_cap; per-identity value still overrides in either
direction. There is no longer an uncapped path, so a worker cannot advertise
its way out of saturation-based backpressure.

F5 — panic-safe teardown. Teardown ran only on handle_connection's normal
return path; a panic (reachable from expect("serializable") on production
paths) left the registry entry, the in-flight rows, and the capacity they
reserve on OTHER instances for the lease sweeper to reclaim up to
lease_expiry_secs later. A RegistrationGuard scoped to the registered lifetime
now owns teardown and is its ONLY caller here, so the normal and unwind paths
cannot diverge. Idempotency (relied on because the sweeper runs the same
deregister + fail_instance pair) is verified, not assumed: deregister is
handle-keyed and returns None for an absent entry, and fail_instance releases
capacity only for entries it actually removes.

Tests: 71 lib + 6 e2e → 82 lib + 9 e2e; clippy -D warnings clean, fmt 0.

Touched existing assertions, each an extension rather than a weakening:
- every DelegateResultParams literal and the result_of helper gained the now
  required `admission` field, sourced from the live entry via a new `token()`
  test helper or from the ack — mechanical consequence of the required field.
- happy_path_roundtrip additionally asserts the ack, the forwarded frame and
  the terminal frame all carry the same token (added coverage).
- peek_for_completion's test helper passes the live token (new parameter).
- generation_exhaustion_fails_closed_instead_of_wrapping →
  admission_token_exhaustion_fails_closed_instead_of_wrapping: same
  fail-closed property, now set on a per-namespace counter, plus a new
  assertion that another namespace is unaffected and that the refused
  admission never advances the counter.
- the stalled-initiator server test now exhausts the initiator's BYTE budget
  instead of a 1-entry channel — same contract, exercising the new bound.
- the e2e stalled-writer test echoes the token from each forward (required
  field) and asserts the forward carries one.

Audit follow-up: the stalled-writer e2e regression sets its byte budget above
the ~48 MiB it enqueues, so byte-refusal backpressure cannot preempt the
write-timeout path the test exists to prove (budget refusal has dedicated
regressions of its own).
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Round-8 response -- the critical finding and all four important findings addressed at d1b5104.

Fixed

# Resolution
F1 (critical) The admission identity is now protocol-visible. DelegateAck, DelegateForward, and DelegateResultParams carry an admission token; the echo is REQUIRED (absent = INVALID_PARAMS, never a wildcard); CP-synthesized terminals (timeout, target_disconnected) build from the entry so they carry the ending admission's token; complete()'s peek matches the echoed token BEFORE the initiator frame is built, so a late result for a superseded admission is dropped without touching the live entry. Token minting moved from one global counter to per-namespace counters (a global monotonic value on the wire would be a cross-namespace volume oracle of exactly the class round-3 F3 eliminated); the fail-closed exhaustion behavior is retained per namespace, with the boundary test. First-terminal-wins is re-keyed per admission token in the ADR and quickstart; cp/cancel token intentionally deferred to the runtime-client slice as the review suggested. Wire regressions: (a) A cancelled, B re-admitted same id + same worker, late result echoing A's token => dropped, B untouched with capacity intact, B completes normally with its own token; (b) A's and B's frames carry distinct tokens; plus real-socket e2e for the late-result drop and the missing-token rejection.
F2 Outbound queues are byte-bounded: FrameTx reserves bytes before enqueue and releases on dequeue AND on teardown (FrameRx::drop drains); new max_outbound_queue_bytes (default 16 MiB, validated > max_frame_bytes). Regressions: refusal fires on the byte budget well before entry-count exhaustion; teardown releases the whole reservation. The pre-existing stalled-writer e2e sets its budget above its payload so it still proves the WRITE-TIMEOUT path (budget refusal has its own dedicated regressions).
F3 Global max_inflight_delegations (default 4096, validated > 0) enforced at admission before capacity reservation => SATURATED naming the bound. The bound reads the in-flight table's own length, so it cannot drift from reality; the regression exercises release via all five removal paths (commit, cancel, sweep, fail_instance, rollback), re-admitting after each.
F4 New default_max_delegated_sessions_cap (default 16, validated > 0) clamps any identity without a per-identity cap; the per-identity knob overrides in either direction. Tests: u32::MAX advertised => effective 16; e2e ack reports the clamped value.
F5 Teardown now runs from a guard scoped to the registered lifetime, so normal return and panic unwind take the identical path; idempotency with the sweeper verified. Regressions: a panicking registered task still deregisters, clears in-flight rows, releases capacity, and queues downstream cp/cancel; double teardown does not double-release.

Stack propagation

The echo requirement lands with the stack: #1471's executor now carries forward.admission into all five terminal frame kinds, and its real-server integration suite passes against the token-enforcing CP -- the first live exercise of the new wire rule. #1470 rebased cleanly (114 lib + 9 e2e).

Verification

cargo test -p openab-cp at d1b5104: 82 lib + 9 e2e passed (baseline 71+6); clippy --all-targets -D warnings clean; rustfmt clean. Stack: #1470 at 114+9; #1471 full suite (37 control-plane lib + 7 integration + 25 bin) at clippy/fmt baselines.

On the accepted residual risks: agreed on all three; the key-length timing normalization is noted for the F1-adjacent follow-up. The docs cross-reference follow-ups are noted for a docs pass after the stack merges.

@chaodu-obk

This comment has been minimized.

…le fix, wire-break callout

F1 (blocking) — the admission token now covers cancellation, symmetrically with
the result path. Round 8 stopped at results, leaving `cp/cancel` keyed on the
reusable `delegation_id` alone, and that gap was reachable from two directions:

- CP-synthesized, no client error required. `sweep_deadlines` removes an expired
  entry under the in-flight lock and builds its best-effort `cp/cancel` AFTER
  releasing it. In that gap the initiator can legitimately re-admit the same id;
  with a single replica admission B routes to the SAME worker and its forward is
  enqueued first. The worker then sees `forward(B)` followed by an id-only cancel
  for A — different producers into one queue, so per-connection ordering
  guarantees say nothing — and B's work is aborted at the source while the CP's
  table still shows B live, so B can only resolve by deadline. Result-token
  validation cannot repair this: the work is already gone.
- Initiator-facing. An application-level retry of `cancel(A)` arriving after the
  re-admission matched B (`claim` compared key + `from_handle` only) and removed
  it: B's capacity was released while its work continued, and B's genuine result
  was later dropped as unknown with no synthesized terminal frame. An ordinary
  retry became a silent kill of the retry it was cleaning up after.

Changes:
- proto.rs: `admission: AdmissionToken` added to `CancelParams` as a REQUIRED
  field. Optional would be a "cancel whatever holds this id now" wildcard —
  exactly the misdelivery the token exists to close. `AdmissionToken`'s doc
  loses the "cp/cancel does not carry a token yet" deferral and documents the
  both-directions round trip instead.
- router.rs: `claim` takes the token and matches (from_handle, namespace +
  delegation_id, admission) — all three under the ONE lock acquisition that
  removes the entry. New `Claim::StaleAdmission` leaves the live entry and its
  capacity strictly untouched and is refused BYTE-IDENTICALLY with the
  unknown-id and wrong-owner refusals: a distinguishable reply would tell a
  caller whether the id it reused is currently re-admitted, the oracle class
  round-3 F3 removed.
- Every CP-synthesized cancel stamps the ended admission's generation. Grepped
  all 15 `CancelParams` construction sites: the two production ones are
  `fail_instance` (initiator-disconnect downstream cancels) and
  `sweep_deadlines`, and both build from the entry they removed. The
  stalled-initiator/backpressure teardown needs no separate change because it
  synthesizes nothing of its own — `server::teardown`, which the
  `RegistrationGuard` drives, and `sweep_leases` both route through
  `fail_instance`, and `grep methods::CANCEL src/` confirms those two plus the
  initiator forward are the only frame producers. The initiator forward passes
  the caller's params verbatim, which is correct: the token was just matched
  against the live entry.
- `Claim`'s doc comment no longer lets its single-phase framing imply the cancel
  path needs no admission identity. The atomicity claim is kept and scoped: it
  is about this operation's consistency, not about the frame having been
  composed against state that may since have moved on. Both properties are
  required; the two-phase completion path needs the token for the additional
  reason that it has a peek-to-commit window of its own.

Deterministic regressions (no barrier timing), both negative-controlled:
- `swept_admissions_cancel_frame_cannot_target_a_reused_id` — expire A via the
  sweep, admit same-id/same-worker B in the gap, then assert A's outbound cancel
  AND timeout frames carry A's token and not B's, and that B is still in flight
  with its capacity intact. Mis-stamping the sweep's token fails it.
- `retried_cancel_for_a_superseded_admission_never_removes_the_live_one` —
  the initiator retries cancel with A's token after B's re-admission: refused,
  byte-identical to the unknown-id refusal, B untouched (in flight, capacity
  held, live token unchanged, nothing forwarded downstream), and B then completes
  normally with its own token. Disabling the generation check fails it.
- `cancel_refusals_are_byte_identical` extended (not weakened) with the
  stale-token case and with post-refusal assertions that the live token, the
  in-flight count, and the capacity reservation are all unchanged. Disabling the
  generation check fails it too.

F2 — ADR §4's `cp/delegate` example showed a client-sent `chain`, a field
`DelegateParams` has never had; serde ignored it silently, which is why the
drift survived. The example now shows what an initiator actually sends
(`parent_delegation_id`, no chain, no admission) and the CP-forwarded frame is
documented separately with `chain` + `admission` + the authenticated `from`. A
`cp/cancel` example is added, since that frame is now wire-relevant.

BONUS: `adr_wire_examples_round_trip_through_the_serde_structs` makes the ADR
examples part of the compiled contract. `include_str!` pulls the markdown in at
compile time (no runtime fs, so the crate's test discipline holds), every
` ```json ` block must parse into the struct its `method` names, and the round
trip must reproduce the exact key set — so a field the example has and the
struct does not fails here. The initiator-sent arm asserts the absence of
`chain` and `admission` explicitly, with the message a future drift would need.
An unrecognized example shape panics rather than being skipped. Verified by
reintroducing the drift: the test fails with "the initiator-sent cp/delegate
example must not show a client-supplied `chain`". The `../../../docs` path does
couple the test to the repo layout; that is the trade, and the doc comment says
so — a fixture copied into the file would drift from the ADR the same way the
ADR drifted from the structs.

F3 — the required-field additions are wire-breaking. ADR §4's amendment block
and docs/control-plane.md now carry an explicit ⚠️ callout: `admission` is
required on `cp/delegate_result` (round 8) and on `cp/cancel` (here), there is
no backward-compatible optional spelling, and a runtime built against the
pre-token contract has EVERY result and EVERY cancel refused with
INVALID_PARAMS after the CP is upgraded. The runtime-author guidance covers both
roles and states that refusals are indistinguishable by design. The
release-note paragraph for the PR description is at
/tmp/1469-wire-break-note.md.

Verification (isolated worktree on the build host): 85 lib + 9 e2e pass
(baseline 82 + 9; +3 = two F1 regressions and the F2 drift test), clippy
--all-targets -D warnings clean, fmt 0 diffs. Assertion changes are extensions
only; the 13 test `CancelParams` literals gained the now-required field, sourced
from the ack or the live entry via the existing `token()` helper.
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Round-9 response -- the critical finding and both important findings addressed at fbc8f96.

Fixed

# Resolution
F1 (critical) The admission token now covers cancellation symmetrically. CancelParams.admission is REQUIRED; initiator cancels match (from_handle, key, admission) under the one lock acquisition that removes the entry, and a stale token is refused byte-identically with the unknown/wrong-owner refusals (extended serialization-equality test). Every CP-synthesized cancel -- deadline sweep and fail_instance (initiator disconnect; the stalled-initiator path routes through it, verified by grepping every frame producer) -- stamps the ended admission's generation from the entry it removed, so a cancel that races a same-id re-admission still names the admission that is over. Claim's doc no longer implies the cancel path needs no admission identity. Both decisive regressions: sweep-expire A -> re-admit B same id+worker -> A's cancel frame carries A's token and B stays in flight with capacity intact; initiator retry with A's token -> refused, B untouched, B completes with its own token. All four new/extended tests negative-controlled.
F2 The ADR cp/delegate example now shows what an initiator sends (parent_delegation_id, no chain/admission); the CP-forwarded frame is a separate block; a cp/cancel example was added. Bonus landed: adr_wire_examples_round_trip_through_the_serde_structs pulls the ADR in via include_str! and round-trips every JSON example through the structs -- the drift class this finding caught is now a compile-time-checked contract (negative control: reintroducing "chain" fails with the intended message).
F3 The wire-breaking contract is called out in the ADR amendment block and the quickstart, and the PR description gains a compatibility section: runtimes built against the pre-token contract have every result and cancel refused INVALID_PARAMS after upgrade.

Stack propagation

#1471's runtime is now token-complete on both directions: the executor echoes forward.admission in all five terminal frames, and the worker-side cancel handler matches (delegation_id, admission) -- a stale cancel naming a superseded admission is ignored instead of aborting the live delegation (the worker-side half of the misdelivery this round closed, with its own regression). The initiator-side integration test echoes the ack's admission through cp/cancel against the real CP.

Verification

cargo test -p openab-cp at fbc8f96: 85 lib + 9 e2e passed (baseline 82+9); clippy --all-targets -D warnings clean; rustfmt clean. Stack: #1470 at 117+9; #1471 at 38 control-plane lib + 7 integration + 25 bin, clippy/fmt at baselines.

@chaodu-obk

This comment has been minimized.

F1 🔴 Parent linkage was the third surface binding CP-authoritative state to
the reusable `delegation_id` without an admission token, after results
(round 8) and cancels (round 9). `delegate()` resolved a parent by
(namespace, parent_delegation_id) plus serving handle only, so once parent
admission A ended (cancel/completion/sweep) and the id was re-admitted as B
to the same worker, a residual child request composed against A satisfied
every check against B: the child inherited B's CP-constructed chain and B's
remaining deadline budget, and depth, cycle and parent-budget were evaluated
for the wrong admission. Unlike the earlier two this is a policy-envelope
hijack feeding authorization decisions, and a best-effort `cp/cancel` cannot
make the worker police it.

- proto.rs: `DelegateParams` gains `parent_admission: Option<AdmissionToken>`.
  The parent reference is a coupled pair — a parented request must carry both
  halves; an id without a token is INVALID_PARAMS, never a wildcard. A root
  delegation carries neither and is byte-unchanged on the wire. A token
  without an id is refused rather than ignored (documented choice), so a
  client that drops the id sees its bug instead of silently getting an
  unparented delegation.
- router.rs: the parent branch matches (namespace + id) + serving handle +
  generation under the same single in-flight lock acquisition it used before.
  Chain and deadline are still read from the `p` binding that acquisition
  validated — no re-lookup after validation. Stale admissions join the
  existing single refusal shape for unknown/unauthorized parents, so the
  reply is not an oracle for whether an id is currently re-admitted.
- Docs: ADR §4 redefines "currently serving that parent" as the specific
  forwarded admission (serving handle + admission), not handle + reusable id;
  the parented `cp/delegate` example gains `parent_admission`; the
  wire-breaking callout now covers parented delegations while stating roots
  are unaffected. docs/control-plane.md quickstart mirrors it. The ADR
  round-trip test additionally asserts the example shows both parent halves
  or neither, since key-set equality alone cannot catch a missing `None` half.

Tests (85 → 91 lib, 9 e2e unchanged):
- child_naming_a_cancelled_parent_admission_cannot_hijack_the_re_admission
- child_naming_a_swept_parent_admission_cannot_hijack_the_re_admission
  Both assert the byte-identical refusal and that B is untouched — chain,
  deadline, admission stamp, endpoints, capacity, and no child admitted.
- child_naming_the_live_parent_admission_extends_it_normally — positive
  control: extends B's chain and is clamped to B's budget. A 90s child is
  denied because it fits A's old 120s but not B's 60s, so the clamp is
  provably B's.
- parent_reference_requires_both_the_id_and_the_admission — both coupling
  directions, plus the root shape still admitted.
- parent_refusals_are_byte_identical — one parent id, three CP states
  (unknown, foreign handle, stale admission), identical error bytes.
- proto: parent_reference_is_a_pair_and_root_delegations_are_unchanged_on_the_wire

Existing parent tests were threaded with the token mechanically: each names
its parent's real admission so the refusal under test stays attributable to
its original cause (foreign serving handle, cross-namespace lookup, policy
cycle) rather than to a missing token.

Negative control: reverting only the generation comparison fails both ABA
regressions and the oracle test while the positive control still passes;
stripping `parent_admission` from the ADR example fails the round-trip test.

Scope: crates/openab-cp + the two CP docs. openab-core untouched (PR 1471
constructs root delegations only). No dependency changes.

Round-10 loop follow-up: the proto.rs module-level contract summary was still
pre-round-10 — its AdmissionToken usage list omitted parent_admission and the
chain bullet said callers supply only parent_delegation_id, contradicting
DelegateParams, the router enforcement, the ADR and the quickstart. The
summary now states that parent linkage is the coupled
parent_delegation_id + parent_admission pair (both required on a parented
request, INVALID_PARAMS otherwise, neither on a root), that AdmissionToken is
carried there, and that no surface accepts a bare reusable id as a reference
to an existing delegation.
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

Round-10 response -- the critical finding addressed at cadca62.

Fixed

# Resolution
F1 (critical) Parent linkage is now admission-exact -- the third and final reusable-id surface joins result (r8) and cancel (r9). DelegateParams gains parent_admission, presence-coupled with parent_delegation_id (id without token = INVALID_PARAMS naming the missing half; token without id likewise REJECTED -- silently treating it as a root would hide a client bug and mint a delegation with no ancestry). The parent branch matches id + serving handle + generation under the same single lock; stale admissions reuse the pre-existing refusal verbatim, so unknown/foreign/stale are byte-identical (three-state serialization-equality test -- no re-admission oracle). Chain and deadline derivation verified to read from the validated entry (no re-lookup existed; a comment now forbids introducing one).

The module-level contract summary now states the general invariant the three rounds converged on: every surface that references an existing delegation names it by the (id, admission) pair; a bare reusable id is never accepted as a reference. A future fourth surface has a stated rule to violate rather than an enumeration to fall outside of.

Regressions (all negative-controlled)

  • Cancel-path and sweep-path ABA: end parent A, re-admit same-id B on the same worker, submit a child naming A's admission -> refused; B's chain, deadline, generation, endpoints, and both workers' capacity asserted untouched; no child admitted.
  • Positive control sharpened beyond the request: parent admitted at 120s, ended, re-admitted at 60s -- a 90s child (fits A's budget, not B's) is DENIED, proving the clamp derives from the live admission rather than being merely consistent with it.
  • Reverting only the generation comparison fails both ABA regressions and the oracle test (88/3) while the positive control passes.
  • The ADR's parented example now carries parent_admission, and the include_str round-trip test gained an explicit pair-coupling assertion -- the original key-set check would NOT have caught a missing optional field (a gap found while wiring the requested enforcement, now closed).

Docs

ADR section 4: "currently serving that parent" redefined as serving handle + specific forwarded admission; wire-breaking callout extended to parented delegations (roots unchanged). Quickstart mirrors both.

Verification

cargo test -p openab-cp at cadca62: 91 lib + 9 e2e passed (baseline 85+9); clippy --all-targets -D warnings clean; rustfmt clean. Stack rebased and green: #1470 at 123+9; #1471 at 38+7+25 (its integration suite issues root delegations only, so the parented-reference contract required no client change); clippy/fmt at exact baselines.

…tract

The v1 facade tool table and CLI examples still referenced existing
delegations by bare reusable delegation_id, contradicting the invariant
established for the wire protocol (result r8, cancel r9, parent r10):
every surface that references an existing delegation names it by the
(id, admission) pair.

Async spawn_agent now returns an opaque delegation handle encapsulating
(delegation_id, admission); check_delegation, cancel_delegation, and
'openab agent status <handle>' take that handle, so a delayed local API
operation can never observe or abort a same-id re-admission. This is a
contract-only amendment directing PR 3; no shipped code changes.
@chaodu-obk

chaodu-obk Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Note

LGTM ✅ -- The round-10 critical fix is verified: parent linkage is admission-exact, completing the (id, admission) invariant across all three wire surfaces; the one gap found this round (the planned facade/CLI contract still referenced work by bare reusable id) was fixed at this head by extending the same invariant to the local API. Full CI is green (40 checks: 39 success, 1 skipped).

What This PR Does

This is PR 1/4 of the Agent Control Plane ADR (#1465): a standalone openab-cp binary providing the wire protocol, identity-bound registry, CP-authoritative policy engine, and delegation router, so agents can delegate to each other over ACP/WS instead of round-tripping through chat platforms.

How It Works

A runtime authenticates with a bearer key at the WebSocket upgrade, registers within a deadline, and delegates through CP-owned state. As of this head, every surface that references an existing delegation names it by the (delegation_id, admission) pair -- results (round 8), cancels (round 9), parent linkage (round 10) -- and the ADR's planned facade/CLI contract now returns an opaque delegation handle encapsulating that pair, so the invariant holds at the local API layer as well (this round).

Round-10 Fix Verification

Round-10 finding Status Evidence at this head
F1 🔴 parent linkage lacked admission token ✅ Fixed DelegateParams.parent_admission presence-coupled with parent_delegation_id; parent branch matches id + serving handle + generation under one in-flight lock; chain and deadline read from the validated entry (a comment forbids re-lookup); unknown/foreign/stale refusals byte-identical (three-state serialization-equality test); cancel-path and sweep-path ABA regressions negative-controlled; positive control proves the deadline clamp derives from the live admission (120s vs 60s asymmetric-budget test)

Findings

This round consolidates a full multi-angle team review (architecture x2, performance, correctness, testing, readability, safety, docs/UX, security/CI, operability, infrastructure) at this exact head.

# Severity Finding Location
1 🟢 Parent-linkage ABA closed with no re-admission oracle: half-pair requests are static INVALID_PARAMS (no CP state consulted); a validated parent supplies chain and deadline in the same lock acquisition; root delegations keep their prior wire shape router.rs:403-477, proto.rs:268-312
2 🟢 The generalized invariant is now stated and enforced end to end: module-level contract summary in proto.rs, ADR section 4 (wire), and -- fixed at this head -- ADR section 6 (facade/CLI local API): async spawn_agent returns an opaque handle encapsulating the pair; check_delegation, cancel_delegation, and CLI status <handle> consume it; callers never see or construct the two halves separately proto.rs:13-29, docs/adr/agent-control-plane.md sections 4 and 6
3 🟢 Test claims triangulated: source-level count of #[test]/#[tokio::test] attributes matches the claimed 91 lib + 9 e2e; CI executes the workspace suite and is green on this head; the ADR include_str round-trip drift test (with the new pair-coupling assertion) passes, including against this head's section-6 amendment all crates/openab-cp sources, CI check job
4 🟢 Security posture verified line by line: constant-time bearer key lookup (subtle::ct_eq, no early exit), non-loopback binds require an explicit override, no existence oracle on any reusable-id surface, policy errors are namespace-scoped, and the global capacity value disclosed in SATURATED is an intentional, documented operability tradeoff config.rs, router.rs
5 🟢 The wire-breaking change (required admission on cp/delegate_result, cp/cancel, and parented cp/delegate) is fail-closed (deserialization refuses, never a wildcard), documented with migration guidance at every layer, and correctly scoped: root delegations are unaffected and no shipped client exists yet ADR section 4, docs/control-plane.md
6 🟢 No performance or lock-hierarchy regression: the parent check adds only an Option pair comparison and generation equality inside the existing admission -> inflight order; no new locks, queues, or hot-path allocation router.rs
Finding Details

🟢 F1-F2: The invariant is complete and self-defending

Rounds 8-10 each moved the admission identity one layer further (result wire, cancel wire, parent linkage). This round audited for a fourth surface and found none on the wire: heartbeats reference CP-minted handles (never reused), registration references no delegation, and CP-internal paths read the in-flight table directly. The one remaining bare-id reference was in the ADR's planned facade/CLI contract (section 6) -- a delayed local check or cancel could have targeted a same-id re-admission, recreating the ABA one layer above the wire. Fixed at this head by a contract-only amendment: the facade returns an opaque admission-aware handle, which is restart-safe (no facade-side mapping state) and keeps the invariant statement uniform across layers. The module-level rule gives any future surface a stated invariant to violate rather than an enumeration to fall outside of.

🟢 F3: Verification pathway

Reviewer environments have no Rust toolchain, so the suite was not executed locally. The claims are accepted on two independent grounds: a source-level test-attribute count reproducing 91 lib + 9 e2e exactly, and green CI on this exact head (the check job runs the workspace suite, including the ADR drift test that pins wire examples to the serde structs).

🟢 F4-F6: Cross-lane verification highlights

  • Independent line-level checks confirmed all three surfaces match (id, admission) under their existing single-lock acquisitions, with no bare-id authorization lookup remaining (grep-audited).
  • ABA regressions are negative-controlled: reverting only the generation comparison fails both ABA tests and the oracle test while the positive control passes -- the tests bite.
  • Docs are synchronized at four layers (struct docs, example config, quickstart, ADR), including the "first terminal frame wins, per admission" client rule.
Baseline Check
  • PR opened: 2026-08-11
  • Declared base: main; merge base: 448b05fbcc17d1ebe52fdc8f78344018bd50b080
  • Reviewed head: 33e99d2be991efefb23581fe7bd7fca88a7e13d2 (round-10 fix cadca627 plus one doc-only commit extending the invariant to the facade/CLI contract)
  • Diff: 36 files, +8911/-78
  • Prior rounds 1-10 all CHANGES REQUESTED; every prior finding is resolved at this head
  • Net-new value: a standalone control-plane registry, policy engine, WebSocket server, protocol, and delegation router not present in the base
Non-blocking follow-ups (tracked)

5. Three Reasons We Might Not Need This PR

  1. Eleven review rounds for one slice -- the concurrency surface consumed substantial review budget. Counter-argument: the stacked PRs 2-4 already exercise this contract end to end, every invariant frozen here is one they depend on, and pre-merge is the cheapest moment these defects will ever be found.
  2. A new single point of failure -- every delegation flows through one in-memory process; a CP restart stalls in-flight work to its deadline. Counter-argument: restart semantics are defined, implemented, and tested; HA is an explicit v2 topic.
  3. A server with no shipped client -- the runtime client lands in PR 2. Counter-argument: feat(cp): observer/lobby — 3-phase design + Phase 1 protocol scaffold (PR 2/4) #1470/feat(cp): OAB runtime CP client — [control_plane] config, worker serving, headless mode (PR 3/4) #1471 are rebased and green against this exact contract, which is stronger evidence than most first slices carry.

Addressing External Reviewer Feedback

  • There are no inline review threads or submitted GitHub reviews on this PR; all feedback is delivered through these aggregate rounds.
  • The github-actions[bot] recent-committers comment is informational; no actionable concern.
  • The author's round-10 response was verified against the tree at this head: the parent-admission fix is present exactly as claimed, with its negative-controlled regressions.
  • Prior rounds 1-10 targeted this or older heads and are superseded by this exact-SHA review.

Round History

  • Rounds 1-9: CHANGES REQUESTED -- identity/atomicity/lifecycle hardening, admission token on result (r8) and cancel (r9) surfaces.
  • Round 10 (fbc8f967): CHANGES REQUESTED -- parent linkage was the third bare-id surface.
  • Round 11 (33e99d2b, this comment): parent fix verified; facade/CLI contract aligned with the invariant at this head -- LGTM.

@thepagent
thepagent merged commit 94354a7 into main Aug 14, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants