镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(ocx-child): carry Codex's Responses WebSocket over the Remote Link when the Home advertises it (closed gate) - #8653

Draft
lidge-jun wants to merge 24 commits into
devfrom
feat/link-websocket-ocx-child
Draft

lidge-jun wants to merge 24 commits into
devfrom
feat/link-websocket-ocx-child

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

The lightweight Rust Child (native/ocx-child) answers Codex's Responses WebSocket upgrade with a fixed 426. Codex then uses HTTP for the whole session and re-sends the full conversation through the link tunnel on every model request. The TypeScript Child already carries that WebSocket over the link's authenticated connection when the Home advertises it (#8614, #8619, #8620); disposable cloud machines run ocx-child, so they kept paying the full HTTP cost.

This change gives ocx-child the same path behind its own closed release gate:

  • Advertisement (link_ws_advert.rs): every relay-auth preflight of the serving process (relayed HTTP requests, the parity report, the /readyz probe) takes a sequence number before it is written. Its answer's x-opencodex-link-ws-state and x-opencodex-link-ws-proof count only as exactly one of each, with the proof an HMAC under the 32 bytes decoded from the fingerprint, equal to the Home's linkWebsocketStateProof, compared in constant time; both sides check the same vectors in tests/fixtures/link/link-ws-state-proof.json. An answer applies only to its own link id and only above the last number applied for it (8 links, per-link watermarks). Lifecycle commands run in their own process and teach the server nothing.
  • Upgrade (link_ws.rs): with the gate open and a verified on for the current link, a valid Codex handshake without Origin takes a slot (8 open or setting up, 4 setting up, 32 MiB of a 256 MiB budget), then makes one preflight and the upgrade on one connection inside one 8 s deadline, cancellable by stop. The upgrade proceeds only on that answer's own verified on. The Home leg carries Codex's headers minus its handshake, the hop-by-hop and Connection-nominated set, Origin, Host, framing, caller credentials and the relay's internal headers; the link key is added through the existing wiped request-head pattern. The Home's 101 counts only with Upgrade, Connection, the right accept value and no extension or subprotocol. Every failure before the local 101 is today's 426.
  • Relay: one RFC 6455 frame reader for both roles (link_ws_frame.rs) with the TypeScript Child's bounds (8 MiB from Codex, 4 MiB + 64 KiB from the Home, fragment cap, held bytes bounded). Each socket has two threads, and each frame write is one operation with an absolute deadline (data 600 s, control 30 s) registered before its write lock, plus a 300 s no-progress write timeout. One reaper thread enforces every deadline by shutting sockets down without taking a write lock. One idempotent terminal transition closes both legs within 5 s (close frames best effort): Codex's close without a status reaches the Home as an empty close, Codex's drop ends the Home connection without one, Home close codes are relayed, and binary closes both legs with 1003. Serve shutdown aborts setups, closes every socket with 1001 (best effort, each close given at most 1 s, all at once, so a stop ends well inside the 3 s that detach, restart and sync wait for the port) and waits until every slot is free. A panic in either of a socket's threads still ends both connections, unregisters the socket and frees its slot, and a relay worker or control thread that panics still returns its admission slot.
  • Review fixes (the slice's review found two P2s, both fixed test-first): the local 101 is written under Codex's write lock inside its own operation deadline and no frame ever reaches Codex before it; a stop that claims a socket between registration and the 101 leaves the 101 unwritten, and the worker answers today's 426 as best effort (the stop also shuts Codex's connection down, so Codex may see it close with nothing written instead); every setup write (the preflight when it has a whole-request deadline, and the upgrade head) gives each write call only the time left before the deadline (relay::write_all_before), so a Home that drains slowly cannot stretch setup past 8 s. Preflights without a deadline write as before.
  • Fallback and a turn in flight: ocx-child has no tunnel supervisor, so it has no reconnect 503 of its own (the existing child_busy/child_starting 503s apply to upgrades as to HTTP). If the tunnel or Home drops mid-turn, Codex gets 1011 and retries on its own; nothing is replayed by the Child, so no request is duplicated.

What keeps it inert: link_ws_advert::gate_open is false in every build without the link-ws-test-gate Cargo feature, whatever the profile; only a build with the feature reads OCX_CHILD_TEST_LINK_WS_READY. build:ocx-child (cargo build --release --locked) passes no features. With the gate closed, or before the Home's verified on, the upgrade gets today's 426 byte for byte with no network I/O. A later change opens the gate.

Differences from the TypeScript Child (on purpose, for this runtime):

  • no connecting 503: ocx-child has no tunnel supervisor (its existing child_busy/child_starting 503s apply to upgrades as to HTTP);
  • TCP backpressure: a slow peer blocks the writer, where the TypeScript Child sends 1013 to Codex and 1001 to the Home on overload;
  • write deadlines (600 s data, 30 s control, 300 s without progress) close a socket, where the TypeScript Child never closes on a timer;
  • Codex's drop cuts the Home connection without a close frame, where TypeScript sends 1011 (the TypeScript Child gets the same behaviour in a follow-up);
  • a close without a status reaches the Home as an empty close frame (same follow-up);
  • 8 links kept in the advertisement table against 256;
  • no Child-local opt-out, counters or traffic book.

Unchanged: HTTP relaying, its headers and limits, and the 426 body. Lifecycle commands behave as before; their preflights now write through write_all_before, with the same effective timeout. One shared-file edit outside the new modules: daemon.rs send_attested_shutdown is now pub for the shutdown test. ocx-child has no Child-local opt-out, counters or byte book for the link WebSocket, as it has none for HTTP.

Verification

Tests (each behaviour commit was test-first; the later test commits cover paths that already existed):

  • Framing: both roles, masking, control and continuation rules, length encodings, limits and UTF-8, events before a fault kept, held-byte bound.
  • Advertisement: the TypeScript proof vector, exactly one verified on, ordering per link, eviction keeping ordering, and the gate staying closed without the feature (run in the no-feature build).
  • Relay, in process (mock Home and Codex on loopback): frames both ways with the link key and a first frame coalesced with the head; refusal, a silent Home and a broken 101 get 426; 1003; close relays; caps and the slot held until the Home connection closed; the setup deadline; stop with 1001; pings per hop; write bounds; a Home close while the worker is blocked; stop between the 101 and the start; spawn failure; a failed local 101; the close-claim race against the worker's cleanup (mutation-checked: it fails when cleanup may retire a claimed close); a close behind a blocked data write returning at the close deadline; maximum messages crossing both ways; pings during large messages; Codex lost while a write to it is blocked; stop with writes blocked both ways on several sockets.
  • Real binary (native/ocx-child/tests/link_websocket.rs, feature build): today's 426 and no Home connection without the gate; frames both ways with it; a failed preflight teaching nothing and a later one teaching on with the parity loop stopped; a Home that switches off answered by the upgrade's own preflight with 426, the turn then completing on HTTP; /readyz teaching on; the authenticated shutdown closing both legs with 1001.
  • Review fixes: a stop claim before the local 101 leaves it unwritten and Codex gets no frame; through the real stop the 426 is best effort and nothing hangs; a setup write ends at the absolute deadline however slowly the Home drains.
  • Final-review fixes (T6-4-2): a stop with a Codex that does not read, under the default limits, ends in about 1 s (failed first at 5.0 s); a panic in the worker unregisters the socket and frees its slot, and a panic in the Home reader ends the socket and lets the worker return (both failed first: the socket stayed registered; the worker hung); a relay worker or control thread that panics returns its admission slot (failed first: the guards did not exist); the state-proof vectors are read by both the Rust and the TypeScript test from one file; the relay and advertisement unit tests share one lock; the stop test's hook fires only once the writer is past its closing check (checked by mutation: with the stop bound raised to 5 s the test fails at 5.0 s).
  • CI wiring: tests/clients/link-ocx-child-ws-gate.test.ts pins the feature wiring and runs both Cargo modes (skipped where cargo is missing, like the conformance test); it makes the run without the feature and tests/clients/link-ocx-child-ws-feature.test.ts the run with it, one Cargo run per file; both are listed in SERIAL_FULL_SUITE_FILES, so each runs alone under the batch runner's 120 s process deadline, with its Cargo run bounded at 100 s. (scripts/ci/test-durations.tsv is generated from CI logs and is left alone.)

Commands (exit codes) on f8cc664 (the T6-4-2 head 46ade1f; a merge of dev 0c0888b, which brought the ocx-child CI step from #8635; one test-only fix, below; a merge of dev 6f58f62, which brought #8612 into native/ocx-child; both merges were clean):

  • The ocx-child CI step, run exactly as it is written, from the repository root on the current stable (rustc 1.100.0, 2026-11-09): cargo fmt --manifest-path native/ocx-child/Cargo.toml --check exit 0; cargo clippy --locked --manifest-path native/ocx-child/Cargo.toml --all-targets -- -D warnings exit 0; cargo test --locked --manifest-path native/ocx-child/Cargo.toml exit 0 (54 lib tests, feat(link): recover a force-revoked ephemeral Child by its link id (ocx-child owner record /2, detach --expect-link, ocx link recover) #8612's 22 owner_link tests and all other suites green)
  • That step builds without features. The link-ws-test-gate feature build (the in-process relay tests' barriers and the real-binary link_websocket target) is exercised only by the Bun wrapper test tests/clients/link-ocx-child-ws-feature.test.ts, not by that step. Run here on stable: cargo clippy ... --all-targets --features link-ws-test-gate -- -D warnings exit 0; cargo test ... --features link-ws-test-gate exit 0 (53 lib tests, 4 real-binary link WebSocket tests)
  • On Rust 1.90.0 (the crate's rust-version): cargo test --locked exit 0, with --features link-ws-test-gate exit 0
  • cargo build --release --locked (no features, as build:ocx-child): exit 0 on 46ade1f; grep -c -a OCX_CHILD_TEST_LINK_WS_READY on the binary: 0, so the shipped build cannot open the gate
  • bun install --frozen-lockfile: exit 0; bun run typecheck: exit 0; bun run privacy:scan: exit 0; bun run structure:check: exit 0
  • bun scripts/test.ts --parallel=2 over tests/clients/link-ocx-child-ws-gate, link-ocx-child-ws-feature, link-ocx-child-conformance, link-ocx-child-e2e, link-ephemeral-child-discovery, link-child-plant-foundations, link-boundary, tests/ci-workflows/test-runner, ci-workflows/ci-scope-gaps, tests/test-layout, test-layout-tooling, ci-workflows/file-size-ratchet, ci-workflows/structure-ssot: exit 0 (248 pass, 4 skip, all Windows-only, 0 fail)

A test-only fix found while running that step (2ea43f0): one plain cargo test run of the link_ws unit tests took 112 s instead of about 5 s. A loop reproduced it in codex_lost_while_a_write_to_it_is_blocked_ends_both_legs (about one run in eleven, 107 s, then passing). A thread dump of a stuck run showed the relay already finished and the test waiting to join its own flood thread, which was blocked writing toward a Child socket the Child had shut down with its receive window full, so the write ended only when the kernel dropped the orphaned connection. The four flood tests now shut their own end of the flooding socket before joining the flood thread; afterwards 100 looped runs of those tests had none over 3 s.

Review: a gpt-6.1-terra reviewer (xhigh, read-only) reviewed the whole diff; round 1 found two P2 (stop vs the local 101; setup write deadline), both accepted and fixed with tests that failed first (a_stop_before_the_local_101_answers_426_and_writes_codex_no_close_frame: Codex read a close frame where the 101 or 426 belongs; a_setup_write_ends_at_the_absolute_deadline_however_the_home_drains: injected clock, one byte per write); round 2 on the fix: no findings. The coordinator's fresh final review of d9c6b31 approved with nits (one P2 reachable only with the gate open, and P3s); each is fixed in its own commit, test-first where there is behaviour, and a gpt-6.1-terra round on those commits found one P2 (the gate test's two Cargo runs inside one 120 s batch process) and two P3s (the stop test's hook could fire before the closing check; a panicking relay worker leaked its admission permit), all fixed; the next round found none. A last round on the flood-test fix found none either. Open P0-P2: 0. The design addendum had nine review rounds before implementation.

#8612 (owner record, detach --expect-link, recover) is now on dev and merged here without conflicts: it changed cli.rs, codex_home.rs, platform.rs and a new test, none of which this PR touches (this PR adds link_ws*.rs and changes Cargo.toml, lib.rs, relay.rs, daemon.rs). Both add entries to scripts/test-layout/layout-clients.json and tests/fixtures/test-layout-expected.json and paragraphs to structure/remote-link.md. This hand-over also edits native/ocx-child/src/daemon.rs only by the one visibility change already in T6-4-1.

No docs-site page changes (the gate is closed; user docs come with the change that opens it).

NOT RUN: the full bun run test suite (4-core VM rule: focused files only); macOS/Windows; an opened gate against a live Home; tests/ci-workflows/docs-spanish-localization.test.ts. With #8630 the Rust suites also run in PR CI.

Checklist

  • Tests in their domain folders with layout entries
  • structure/remote-link.md updated
  • Inert by default (closed gate)

…(not wired yet)

ocx-child answers Codex's Responses WebSocket upgrade with 426, so the
saving of the link WebSocket does not reach Children that run it. This
is the first piece of carrying it there: one frame reader for both legs
(masked frames from Codex, unmasked from the Home) with the same bounds
as the TypeScript Child's framer, and the writers. Nothing calls it yet.
…ed gate (not wired yet)

- link_ws_advert: the state proof (same HMAC as the Home), exactly one
  state and proof header, per-link ordering by preflight number with
  watermarks for evicted links, memory only.
- The gate is closed in every build without the link-ws-test-gate Cargo
  feature; only a test build with it reads OCX_CHILD_TEST_LINK_WS_READY.
… the Home advertises it (closed gate)

With ocx-child's gate open (only a link-ws-test-gate build can open it)
and the Home's verified on applied for the current link, the upgrade on
/v1/responses now opens the Home leg on one authenticated connection
and relays whole text messages both ways; every failure is today's 426.

Two threads per socket, every frame write a bounded operation under a
per-socket write lock, one reaper thread enforcing every deadline by
shutting both sockets down, an idempotent terminal transition with a
5 s close bound, and a registry whose stop aborts setups and closes
sockets with 1001. Slots are held until nothing can use the sockets.
…op before start, spawn failure

A failed spawn of the Home reader thread before the local 101 now leaves
Codex's connection to its owner, which answers today's 426, and only
shuts the Home connection down.
…x-child

# Conflicts:
#	scripts/test-layout/layout-clients.json
#	tests/fixtures/test-layout-expected.json
@coderabbitai

coderabbitai Bot commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Nov 13, 2026
…Cargo runs bounded inside the batch deadline
…x-child

# Conflicts:
#	scripts/test-layout/layout-clients.json
… a test never waits for the kernel to drop an orphaned connection

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant