Skip to content

feat(link): zero-preinstall SSH ocx-child planting, --purge detach, and #remote UX - #8255

Open
lidge-jun wants to merge 8 commits into
feat/ocx-child-ephemeral-linkfrom
feat/ocx-child-plant-ux
Open

lidge-jun wants to merge 8 commits into
feat/ocx-child-ephemeral-linkfrom
feat/ocx-child-plant-ux

Conversation

@lidge-jun

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

Copy link
Copy Markdown
Owner

Summary

Stack (merge bottom-up):

# PR Layer Review focus
2 #8255 (feat/ocx-child-plant-ux) ← you are here Zero-preinstall SSH ocx-child planting, --purge removal, conflict diagnostics, CLI flags, & 11-locale #remote UX SSH stdin plant & rollback, managed-path --purge, #remote UI & i18n
1 #8237 (feat/ocx-child-ephemeral-link) Standalone Rust native/ocx-child runtime & Home discovery/relay base Rust ocx-child wire/journal conformance & base enrollment

Depends on #8237 (feat/ocx-child-ephemeral-link at c4d4d5d6311905bea62f065d7d04b9de7cdb5a86). Review this PR's diff only.

Connecting a disposable SSH devbox, container, or cloud VM through #remote or ocx link add previously required pre-installing ocx or manually copying ocx-child onto the Child beforehand (502 remote_ocx_missing), offered no one-click or --purge option to remove a planted binary on disconnect, and surfaced generic preflight errors when port 10100 or ~/.codex/config.toml was already owned externally.

This layer completes end-to-end lightweight Ephemeral Child onboarding and lifecycle UX:

  • Zero-preinstall SSH host inspection & atomic stdin planting: POST /api/link/probe and POST /api/link/confirm-host (allowMissing: true) inspect remote targetTriple (darwin-arm64, darwin-x64, linux-x64, linux-arm64, windows-x64) and classify runtimeKind (full, ephemeral, or missing). src/link/child-binary-source.ts verifies non-symlink ocx-child artifacts (OPENCODEX_CHILD_BIN_DIR, sidecar directories, host-matching local native/ocx-child release build, or SHA256SUMS-verified $OPENCODEX_HOME/cache/ocx-child/<version>/). When plant: true is authorized on POST /api/link/apply, Home streams the verified binary over SSH stdin into ~/.cache/opencodex/bin/ocx-child (0700 atomic write on POSIX) or %LOCALAPPDATA%\opencodex\bin\ocx-child.exe (inside OcxJob <= 8000 encoded chars on Windows, refusing symlinks and reparse points) — working even on air-gapped Children — and automatically purges a newly planted binary if enrollment fails.
  • Two-tier disconnect (fast detach vs --purge) & durable childKind: native/ocx-child (detach / disconnect --purge), DELETE /api/link/:id ({ purge: true }), and ocx link revoke --link-id <id> --purge delete the binary after restoring ~/.codex and stopping ocx-child serve only when the executable resides inside the managed plant path (refusing symlinks, ancestor symlinks/reparse points, and foreign/system paths; using rename-aside + detached cmd.exe cleanup on Windows), while default disconnect keeps the cached binary for instant re-attach. LinkRecord persists childKind ("full" | "ephemeral") and projects it on GET /api/link/status (alongside gui-session homeCodexSettingsDigest).
  • Actionable conflict diagnostics & CLI flags: Foreign listeners on childPort and external model_provider / user openai_base_url / model_catalog_json ownership in ~/.codex/config.toml map deterministically to 409 child_port_busy and 409 child_external_provider_conflict while leaving Child files untouched. ocx link add <alias> [--plant] prompts once on interactive TTY for bare hosts and requires explicit --plant alongside --expect-fingerprint in non-TTY automation.
  • #remote Dashboard UX across all 11 locales: Modularized #remote (RemoteLink.tsx, RemoteLinkSheet.tsx, RemoteLinkDialogs.tsx) and updated all 11 locale catalogs (en, ko, ja, zh, zh-TW, de, fr, pt, ru, tr, vi) with the Ready to plant lightweight agent confirmation card, Lightweight / Full install and Home settings synced / Sync needed badges on connected Child rows, specific conflict notices, and the Disconnect --purge checkbox + ~/.cache/opencodex/bin/ocx-child detach offline recovery command.

UI Screenshots (origin/pr-assets @ 4d57c948b78bc66f2fb06b01cd9eb947766f1a60)

1. Add Child — Ready to Plant Lightweight Agent (runtimeKind: "missing", plantable: true)

Ready to plant lightweight agent modal

2. Connected Child Computers — Mode (Lightweight / Full install) & Home Settings Sync Parity Badges

Connected children list with Lightweight and Sync parity badges

3. Ephemeral Child Disconnect — Zero-Daemon Fast Detach & Optional --purge Binary Deletion

Ephemeral disconnect dialog with --purge checkbox

Verification

  • cargo test --locked --manifest-path native/ocx-child/Cargo.toml — 9 passed (7 unit/conformance + 2 full relay/lifecycle/--purge/symlink/conflict integration tests), 0 failed.
  • cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml — exit 0 (930 KB optimized ocx-child binary).
  • bun run typecheck — exit 0 (2,321 files checked, 0 errors).
  • cd gui && bun run lint:i18n && bun run lint && bun test tests && bun run build + root bun run build:gui — exit 0 (3,738 GUI tests passed across all 371 files including 11-locale parity & remote-link.test.tsx).
  • bun test tests/clients/link-child-plant-foundations.test.ts tests/clients/link-ocx-child-e2e.test.ts tests/server/link-enrollment-ephemeral-child.test.ts tests/server/link-enrollment.test.ts tests/server/link-management-routes.test.ts tests/cli/cli-link-enrollment.test.ts tests/cli/cli-link-force.test.ts tests/cli/cli-link.test.ts — all passed (including real-binary bare-host SSH stdin plant + DELETE --purge E2E).
  • bun run test:changed — 35,729 passed, 121 skipped, 0 failed (306,106 expect() calls across 1,553 import-connected files; scoped to test:changed + explicit Rust/GUI/SSOT/hygiene/ratchet suites per AGENTS.md to avoid redundant full-suite contention across concurrent worktrees while leaving unmodified unrelated domains to batch CI).
  • bun run structure:check, bun run skill:surface:check, cd docs-site && bun run build (641 pages, 93,153 internal links), bun run privacy:scan, and bun test tests/ci-workflows/repo-hygiene.test.ts tests/ci-workflows/file-size-ratchet.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts — all exit 0.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner November 7, 2026 00:03
@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 2

  • ^dev$
  • ^preview$

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • 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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-07T00:13:00.201229Z 71352d3 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@lidge-jun lidge-jun added the enhancement New feature or request label Nov 7, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8adfb2224

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/link/remote-plant.ts Resolved
Comment thread native/ocx-child/src/cli.rs Resolved
@lidge-jun

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 71352d39e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions github-actions Bot removed the intake: hygiene-blocked Deterministic PR hygiene checks failed label Nov 7, 2026

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 75dc0635 against its stacked base feat/ocx-child-ephemeral-link @ c4d4d5d6. This review covers only this layer's diff. Focus was correctness of planting, rollback and purge, not a security scan. I did not review the GUI/i18n changes for UX, since that decision is the owner's.

Checked and looks right:

  • POSIX plant: umask 077, symlink refusal on every managed path component, a temp file in the 0700 dir, exact byte-count and (when a tool is available) SHA-256 checks before mv -f, then --version verification with removal on failure. Expected size and SHA travel as positional args, so the script text is never interpolated.
  • Windows plant: CreateNew temp, streaming SHA-256 with a size cap, reparse-point refusal on each component.
  • Apply rollback: failed() now also runs for the remote_port / child_preflight early returns. A fresh plant is purged through detach --purge, whose non-zero exit now preserves the binary and surfaces residual (bot thread fixed in 71352d39). child_port_busy / child_external_provider_conflict are raised before any Child file is written, so remoteMayBeConnected = false is correct there.
  • purge_managed_executable: only an exact-name file whose canonical parent equals the canonical managed dir qualifies, with symlink and reparse checks on the exe, its parent and each managed component. Foreign paths return SkippedUnmanagedPath (covered by the new Rust test).
  • probe / confirm-host / apply: allowMissing and plant are strictly typed. apply refuses a missing host without plant: true, and plant is ignored for full / ephemeral hosts.
  • CI on this head: the required shards are green, and Windows/macOS lanes were not selected.

Inline notes below. None is marked blocking. The most user-visible one is the docs claim, because packaged installs have no artifact to plant.

Non-blocking:

  • childKind joins the strict RECORD_KEYS set (src/link/store.ts). An older Home that reads links.json after a downgrade will reject the whole store. This is the same trade-off remotePlatform made in #7832, so it only needs a release-note mention.
  • resolveChildBinarySource: with OPENCODEX_HOME="" (set but empty), env.OPENCODEX_HOME?.trim() ?? … produces "", and the cache lookup becomes cwd-relative. The Rust side treats empty as unset, so || join(...) would match it.

I did not approve because remote binary planting and execution need the explicit security review described in MAINTAINERS.md.

### Ephemeral Child machines (`ocx-child`)

For disposable SSH devboxes, containers and lightweight cloud VMs where installing the full Bun OpenCodex runtime is unnecessary, place the standalone Rust `ocx-child` binary (`cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml`) in `~/.local/bin/ocx-child`, `~/.cache/opencodex/bin/ocx-child` (or `%LOCALAPPDATA%\opencodex\bin\ocx-child.exe` on Windows) or on `PATH`. Before enrollment, Home probes every full `ocx` candidate first and falls back to `ocx-child` when no runnable full `ocx` is present; once `$OPENCODEX_HOME/child/state.json` exists, remote commands keep targeting `ocx-child` until it detaches.
For disposable SSH devboxes, containers and lightweight cloud VMs where installing the full Bun OpenCodex runtime is unnecessary, Home can stream a SHA-256-verified standalone `ocx-child` binary (~0.9 MB, zero Node/Bun required) directly over SSH `stdin` into `~/.cache/opencodex/bin/ocx-child` (`%LOCALAPPDATA%\opencodex\bin\ocx-child.exe` on Windows) when you choose **Plant lightweight agent & connect** in `#remote` or run `ocx link add <alias> --plant --expect-fingerprint SHA256:…` (interactive TTY `ocx link add <alias>` prompts once before planting; air-gapped Children with zero outbound internet work out of the box). You can also pre-place `ocx-child` in `~/.local/bin/ocx-child`, `~/.cache/opencodex/bin/ocx-child`, or `PATH`. Before enrollment, Home probes every full `ocx` candidate first and falls back to `ocx-child` when no runnable full `ocx` is present; once `$OPENCODEX_HOME/child/state.json` exists, remote commands keep targeting `ocx-child` until it detaches.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As written, this describes behavior that a packaged (npm/desktop) Home cannot deliver yet. resolveChildBinarySource only finds an artifact in four places:

  • OPENCODEX_CHILD_BIN_DIR
  • native-bin/ sidecars, which nothing in this stack produces or adds to package.json files
  • a host-triple local cargo build, which only exists in a dev checkout
  • $OPENCODEX_HOME/cache/ocx-child/<version>/ with SHA256SUMS, which nothing populates

The devlog says release-pipeline changes were deliberately left out. For a normal install, the probe will return plantable: false / artifact_not_found, and the bare-host flow dead-ends. That contradicts "air-gapped Children … work out of the box" here and the prerequisite bullet at line 12.

This paragraph also dropped the only instruction for obtaining the binary (cargo build --release …). Suggest stating the artifact requirement explicitly: where to place a binary plus SHA256SUMS, and that cross-OS/arch planting needs a binary built for the target. Keep that until release artifacts ship.

Comment thread src/link/remote-plant.ts
"elif command -v shasum >/dev/null 2>&1; then",
" actual_sha=$(shasum -a 256 \"$tmp\" | awk '{print tolower($1)}')",
" if [ \"$actual_sha\" != \"$expected_sha\" ]; then printf 'Planted binary SHA-256 mismatch\\n' >&2; exit 1; fi",
"fi",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When neither sha256sum nor shasum exists on the Child (some minimal or distroless images), the SHA-256 check is skipped silently and only the byte count is verified before the binary is executed. SSH already protects the transport, so this is low severity. Still, the PR text and docs say "SHA-256-verified". Either fail closed when no hash tool exists, or print a stderr note so the result is not presented as hash-verified.

const runner = runnerFor(ctx);
const platform = record.remotePlatform ?? await requireRemotePlatform(runner, record.alias, knownHostsFile(ctx));
const result = await runner.run(buildExecArgv({ alias: record.alias, argv: remoteOcxArgv(["disconnect"], platform), knownHostsFile: knownHostsFile(ctx) }), { timeoutMs: 30_000 });
const disconnectArgs = purge && record.childKind === "ephemeral" ? ["disconnect", "--purge"] : ["disconnect"];

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

disconnect --purge goes through the generic candidate resolver. When child/state.json exists, ~/.local/bin/ocx-child is tried before the managed ~/.cache/opencodex/bin/ocx-child. If a user also has a copy in ~/.local/bin, that copy runs. is_managed_plant_executable returns false, the command exits 0 with purged:false, and Home reports success while the planted binary stays. The route does not pass --json, so it cannot detect this.

Purge is also silently dropped when record.childKind is absent, which is the case for any ephemeral link enrolled by the base layer before childKind existed.

Consider using remotePurgePlantedChildArgv for the purge case (it targets the managed path explicitly and already handles detach + removal). Alternatively, request --json and fail or report when purged is false.

let child_dir = opencodex_home.join("child");
Self {
lock_file: child_dir.join("lifecycle.lock"),
lock_file: opencodex_home.join("child.lock"),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving the lock to $OPENCODEX_HOME/child.lock (the correct fix for the inode race) has a side effect. FileLock::acquire_inner calls ensure_private_dir(parent), which now hardens $OPENCODEX_HOME itself, not just child/:

  • on POSIX it runs chmod 0700
  • on Windows it calls SetNamedSecurityInfoW with a protected owner-only DACL plus inheritable ACEs, which also propagates to existing children

This runs on every attach/sync/detach/restart, and before assert_no_full_ocx_conflict. On a machine that also has a full ocx install under the same home, the child binary now rewrites the ACL of the full install's directory tree. Consider creating the lock file without hardening its parent ($OPENCODEX_HOME is not owned by ocx-child), or keeping it in a dedicated subdirectory that is never removed.

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.

2 participants