Repository navigation
refactor(cp): make openab-cp proto usable without the server - #1588
Conversation
Split openab-cp into an always-compiled `proto` module and a default `server` feature (config, registry, policy, router, events, WS server, binary), so runtime clients can depend on the wire contract with default-features = false without linking axum/tokio. Adds proto::MIN_RUNTIME_FRAME_BYTES and rejects CP configs whose max_frame_bytes is below it. Carried from #1528 (commit 9455b50). Also fixes #1528 round-3 finding 7 (bail! string had a literal run of spaces; test now asserts the exact message) and gates tests/ws_lifecycle.rs on `server` so `cargo test --no-default-features` runs the proto tests. Part 1 of 3 re-landing #1528; see openabdev/openab-pty#77. Verified on macmini (M4, rustc 1.99.1), clean copy of this tree: - cargo metadata --locked: ok; Cargo.lock unchanged vs origin/main - cargo build -p openab-cp --locked: ok - cargo test -p openab-cp --locked: 142 unit + 9 ws_lifecycle passed - cargo check -p openab-cp --no-default-features --locked: ok - cargo test -p openab-cp --no-default-features --locked: 16 proto passed - cargo tree -p openab-cp --no-default-features -e normal: no axum/tokio/hyper - cargo clippy -p openab-cp --all-targets -- -D warnings: clean (default and --no-default-features) - cargo fmt -p openab-cp --check: clean - Docker dummy-layer sim (manifests + empty lib.rs, no openab-cp main.rs): cargo metadata --locked and root cargo check --locked ok - CI parity: cargo check --workspace, clippy --workspace [--features unified] -D warnings, test -p openab-gateway --features acp (417), test -p openab-core --features acp-mcp acp_mcp:: (5) / acp::pool:: (24), test --features acp (42), build --features unified: all ok - cargo test --workspace: all pass except openab-core secrets::tests::resolve_exec_nonzero_exit, which spawns /bin/false; macOS has only /usr/bin/false. openab-core is untouched here; CI is Linux.
Review opus5-F1: the max_frame_bytes floor ran before the max_outbound_queue_bytes <= max_frame_bytes check, so the case queue=4096/frame=8192 was rejected by the floor and the strictly-below branch of the queue rule went untested. Replace it with queue=1048576/frame=2097152 and assert the expected error text for every rejection case instead of a bare is_err(). Review opus5-F2: proto.rs and cp.toml.example now say the runtime client lands in a follow-up PR instead of describing it as already bundled. Verified on macmini (M4), clean copy of this tree: - cargo fmt -p openab-cp --check: clean - cargo build -p openab-cp --locked: ok - cargo test -p openab-cp --locked: 142 unit + 9 ws_lifecycle passed - cargo check -p openab-cp --no-default-features --locked: ok - cargo test -p openab-cp --no-default-features --locked: 16 proto passed - cargo clippy -p openab-cp --all-targets -- -D warnings: clean (default and --no-default-features) - cargo clippy --workspace -- -D warnings, cargo check --workspace: ok - Cargo.lock unchanged vs origin/main
This comment has been minimized.
This comment has been minimized.
…lient PR Keep MIN_RUNTIME_FRAME_BYTES in proto as the shared constant, but drop the CpConfig::validate() rejection and its test: nothing in this PR consumes the floor, and the error text promised a frame-size negotiation the protocol does not have. The check lands with the runtime client in PR B.
…to split The no-server guarantee holds only when nothing else in the same build enables the server feature; cargo build --workspace does, through the openab-cp binary. The tokio-tungstenite dev-dependency is test-only and never reaches downstream crates.
This comment has been minimized.
This comment has been minimized.
--workspace always enables openab-cp's default server feature, so no CI step built the configuration runtime clients depend on. Add clippy and test for openab-cp with --no-default-features, and fail if a server dependency appears in its normal dependency graph (-e normal leaves out the test-only tokio-tungstenite dev-dependency).
- cargo tree's output is captured on its own, so a failing cargo tree aborts the step under set -e, and an output without the openab-cp root line fails explicitly instead of reading as "no leak" (opus5-F2). - The denylist is now the ten optional deps the `server` feature gates plus axum's hyper/tower transitives (opus5-F3). Verified on macmini (scratch copy, rust stable): - the extracted CI step script (clippy -D warnings, cargo test --no-default-features: 16 passed, tree check): exit 0 - pattern against the default-features tree matches all ten server deps - cargo tree on a bad package aborts the step before the grep
|
Important CHANGES REQUESTED What This PR Does
How It Works
Findings
Finding Details🟡 F1: The leak check is incomplete and can pass silentlyThe check rejects only The Requested change: check the dependency closure against an allowlist, and fail closed. For example:
The current manifest marks all ten server dependencies optional, so this is a CI false-negative gap, not a current runtime leak. 🟡 F2:
|
| Reviewer | Verdict | Findings |
|---|---|---|
| Reviewer A | CHANGES REQUESTED |
F1, F4, F5, F6 |
| Reviewer B | CHANGES REQUESTED |
F1, F2, F4, F5, F6 |
| Coordinator | CHANGES REQUESTED |
F1, F2, F3 |
There are no inline review comments, submitted reviews or external reviewer comments on this PR to address.
Baseline Check
- PR opened: 2026-10-30
- Reviewed head:
0916de60b02ad5bb82d37f4ece05f113f43300a2; basemain; merge-base017ed21b3d4f2befffa2fb47ab522ee041f97ba7(not stacked) - Diff: 5 files, +122/-16 (
crates/openab-cp/plus.github/workflows/ci.yml) - Main already has:
openab-cpas a single always-server crate - Net-new value: a server-free
protobuild for runtime clients, a CI gate for it, and a shared frame-ceiling constant - CI on this head: all checks pass, including
checkand its new proto-only step
What's Good (🟢)
- Small, reversible first step of the feat(cp): land Agent Control Plane v1 on main #1528 split, with no change for existing
openab-cpusers (default features unchanged) - Round-1 and round-2 feedback was addressed quickly and without changing runtime behavior
- Comments explain why each gate exists, including the feature-unification caveat
5. Three Reasons We Might Not Need This PR
- No in-tree consumer yet - nothing in the workspace builds
openab-cpwith--no-default-featuresuntil the runtime-client PR, so this could land together with it. - Workspace builds still unify features - the common
cargo build --workspacepath keeps the server graph, so the benefit only appears when the runtime package is built on its own. - A separate protocol crate may age better - it would give an unconditional dependency boundary and remove the need for the feature gate and the leak check.
|
/review |
…loor constant - ci.yml: read the server-only denylist from the optional deps in crates/openab-cp/Cargo.toml instead of a hand-written list. Fail when that list is empty, when cargo tree errors or prints nothing, and when openab-cp is missing from the tree. The step has no || true. - proto.rs: remove MIN_RUNTIME_FRAME_BYTES. Nothing consumes it in this PR (review NIT F2). The constant and its max_frame_bytes floor move to the runtime-client PR. Checks on macmini, scratch copy of this commit (--locked): - CI step script run verbatim: clippy -D warnings, cargo test --no-default-features (16 proto tests), leak check exit 0. - Leak check against a fake cargo: leaked futures-util -> exit 1; empty tree -> exit 1; failing cargo tree -> exit 101; clean tree -> exit 0. - Default: clippy --all-targets -D warnings clean; cargo test (141 unit, 9 integration) passed. - cargo fmt -p openab-cp --check clean.
|
/review |
1 similar comment
|
/review |
What problem does this solve?
The runtime half of the Agent Control Plane (#1528) needs the CP wire types (
openab_cp::proto) insideopenab-core, but todayopenab-cpalways pulls in axum, tokio and the whole WebSocket server. This PR is part 1 of 3 re-landing #1528 (auto-closed by the stale-PR bot on 2026-09-13, not rejected), split so each piece can be reviewed in one sitting. All three are needed by openabdev/openab-pty#77 (Phase 0: openab-pty hosts as CP workers, Connect as primary).Refs #1528, openabdev/openab-pty#77
Discord Discussion URL: https://discord.com/channels/1491295327620169908/1491365158868619404/1532377620241449040
Review Contract
Goal
Make
openab_cp::protousable withdefault-features = false, without axum/tokio/the server, while keeping the default build ofopenab-cp(library +openab-cpbinary) unchanged. The runtime frame floor (MIN_RUNTIME_FRAME_BYTES, with itsmax_frame_bytescheck) is not in this PR; it lands in PR B with the client that uses it.Non-goals
[control_plane]config: that is PR B.openab agentCLI: that is PR C.openab-cpyet.Accepted Residual Risks
openab-cp --no-default-featuresand checks its normal dependency graph (seeci.ymlunder Proposed Solution). No dependent crate uses the realdefault-features = falseedge until PR B, and PR B must add a proto-only check of that consumer.CpConfig::validate()has no frame floor in this PR, so amax_frame_bytesbelow 1 MiB (including 0) still starts. That is today's behaviour; the floor and its constant land in PR B. See "Plan change" below.Acceptance Criteria
cargo test -p openab-cppasses (unit tests +ws_lifecycle)CI / checkstep "openab-cp (proto only, no default features)" passes (the test build still compiles thetokio-tungstenitedev-dependency; the leak check covers the normal graph only): clippy-D warningsandcargo testwith--no-default-features, andcargo tree -e normallists none of the tenserverdependencies (read fromCargo.toml)cargo clippy -p openab-cp --all-targets -- -D warningsis clean, with and without default featuresCargo.lockis unchanged (--lockedbuilds pass)CI / checkjob is greenFollow-ups
feat(cp): runtime CP client and worker delegation serving (headless worker):[control_plane]config, client (register/heartbeat/reconnect, bounded writes), worker executor (PromptRunner),PromptExecutionseam,drop_idle_gaterebased onto fix(pool): keep the [[ws:]] workspace across hung eviction #1557, headless worker mode. Fixes feat(cp): land Agent Control Plane v1 on main #1528 round-6 F3 (client tick clamp), F10, F11 (Servecomment) and drops the round-1 ride-along edits.-D warnings) and testopenab-cpwith--no-default-features, plus the consumer crate on itsdefault-features = falseedge. PR B must not count the--workspacejobs as coverage: Cargo feature unification keepsserveron whenever theopenab-cpbinary is in the build. Requested in review round 2 F1 (comment); this PR'sci.ymlstep coversopenab-cpalone.MIN_RUNTIME_FRAME_BYTES = 1 MiBinproto.rsand themax_frame_bytes >= MIN_RUNTIME_FRAME_BYTEScheck inCpConfig::validate()with its client. Its release notes must call the floor a breaking config change, since existing configs below 1 MiB stop starting (review round 3 F2 option b, comment).feat(cp): primary delegation state, owner-only local API and openab agent CLI:primary.rs,local.rs, the primary command arm,openab agent spawn|list|status|cancel. Fixes round-6 F1 (the red CI test), F3 (independent prune timer), F4, F5, F6, F7 (including CP-side empty-selector rejection), F9, F11 and F14.At a Glance
Prior Art & Industry Research
Not applicable: this is a Cargo feature split of an existing internal crate with no runtime, agent or scheduling behaviour change. The pattern (wire types always on, server behind a default feature) is the standard Cargo approach for a crate that is both a library contract and a binary.
OpenClaw: Not applicable (no equivalent crate boundary).
Hermes Agent: Not applicable (no equivalent crate boundary).
Proposed Solution
crates/openab-cp/Cargo.toml: serde, serde_json and chrono stay unconditional. tokio, axum, futures-util, toml, tracing, tracing-subscriber, anyhow, parking_lot, clap and subtle become optional behindserver(default).[[bin]] openab-cpand[[test]] ws_lifecyclerequireserver.src/lib.rs:protois always compiled. The other modules are#[cfg(feature = "server")]. A doc block explains the split.src/proto.rs: the wire types are unchanged. The frame floor is not part of this PR..github/workflows/ci.yml: thecheckjob builds, lints and testsopenab-cp --no-default-featuresand fails if any dependency that theserverfeature enables (the optional deps inCargo.toml, read by script so the list cannot drift) reaches its normal dependency graph, or ifcargo treegives no usable output.--workspacealways enablesserverthrough the binary, so nothing else in CI covers this configuration.src/config.rstests: themax_outbound_queue_bytes <= max_frame_bytesrule has two cases, queue 1048576 with the default frame (equal) and queue 1048576 with frame 2097152 (strictly below), and every rejection case asserts its error text instead of a bareis_err().This is the code reviewed as part of #1528 (commit 9455b50, which round 6 F12 found clean), carried onto current main with no conflicts. The changes to it are the test tightening above, deferring the frame-floor validation to PR B, and doc wording that describes the runtime client as a follow-up PR rather than already bundled.
Plan change: the frame floor moves from A to B
The #1528 closure plan put the
CpConfig::validate()checkmax_frame_bytes >= MIN_RUNTIME_FRAME_BYTES, its test and thecp.toml.exampleparagraph in this PR. Review round 1 (comment) asked to drop it: nothing on main consumes the floor yet, it would stop existing configs below 1 MiB from starting, and its message described a frame-size negotiation the protocol does not have. The owner agreed, so A ships neither the constant nor the floor. PR B adds both together with the client that relies on them, with an ASCII message, an exact-message test, a>= 1lower bound and no claim of negotiation.Why this approach?
It keeps one crate as the single source of the wire contract and avoids a separate
openab-cp-protocrate before a second consumer exists. The default build is unchanged, socargo run -p openab-cpand the docs keep working. Landing it first gives B and C a small, already-reviewed base.Alternatives Considered
openab-cp-protocrate: a cleaner boundary, but it adds a crate and a path move with no consumer that needs it yet. It can be extracted later if openab-pty embeds the client.#1528 review findings addressed here
max_frame_bytesbail!string and its substring-only test. Moves to PR B together with the floor validation itself; fix it there (ASCII message, exact-message test, no claim of frame-size negotiation).tests/ws_lifecycle.rsis gated onserversocargo test --no-default-featuresruns the proto tests instead of failing to compile.Validation
Run on macOS (M4, rustc 1.99.1) in a clean copy of this head:
cargo build -p openab-cp --lockedcargo test -p openab-cp --locked: 142 unit + 9 integration passedcargo check -p openab-cp --no-default-features --locked;cargo test -p openab-cp --no-default-features --locked: 16 proto tests passedcargo tree -p openab-cp --no-default-features -e normal: no axum/tokio/hypercargo clippy -p openab-cp --all-targets -- -D warnings(default and--no-default-features);cargo fmt -p openab-cp --checkcleancargo check --workspace,cargo clippy --workspace [--features unified] -- -D warnings,cargo test -p openab-gateway --features acp(417),cargo test -p openab-core --features acp-mcp {acp_mcp::,acp::pool::}(5 / 24),cargo test --features acp(42),cargo build --features unifiedcargo test --workspace: all pass on macOS exceptopenab-coresecrets::tests::resolve_exec_nonzero_exit, which spawns/bin/false(macOS only has/usr/bin/false). This PR does not touchopenab-core; Linux CI covers it.lib.rsfiles and nocrates/openab-cp/src/main.rs→cargo metadata --lockedand rootcargo check --lockedsucceedgit diff origin/main -- Cargo.lockis empty