Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a5c605f
docs(devlog): add docs-first roadmap and PRDs for ocx-child ephemeral…
lidge-jun Nov 6, 2026
07f5007
feat(link): add ephemeral-child contract helpers and golden conforman…
lidge-jun Nov 6, 2026
072d491
feat(ocx-child): add standalone lightweight Rust Child link relay and…
lidge-jun Nov 6, 2026
a49f471
feat(link): integrate ephemeral ocx-child discovery, enrollment, and …
lidge-jun Nov 6, 2026
fec8761
docs(link): document ephemeral ocx-child in structure SSOT and docs-site
lidge-jun Nov 6, 2026
2cce483
fix(ocx-child): persist write-ahead journal, track multiline TOML, an…
lidge-jun Nov 6, 2026
f8a5ba8
fix(link): harden ocx-child Windows ACLs, strict wire/settings bounds…
lidge-jun Nov 6, 2026
bd7a68c
fix(link): preserve interrupted-sync digests, rollback failed reattac…
lidge-jun Nov 6, 2026
5e26d66
fix(link): fail fast on unreadable attach snapshots, preserve feature…
lidge-jun Nov 6, 2026
c4d4d5d
fix(link): verify restored credentials before relay restart and clari…
lidge-jun Nov 6, 2026
fb2f12a
docs(devlog): add wp0 roadmap and PRDs for ocx-child plant UX
lidge-jun Nov 6, 2026
f04f80a
feat(link): add ocx-child --purge, conflict codes, and SSH plant foun…
lidge-jun Nov 6, 2026
671fd2d
feat(server): add bare-host SSH plant, rollback, childKind status, an…
lidge-jun Nov 6, 2026
f62b488
feat(gui,cli): add #remote Ready-to-Plant, parity badges, and --purge…
lidge-jun Nov 6, 2026
c8adfb2
docs(link): document zero-preinstall SSH plant, --purge, and #remote UX
lidge-jun Nov 7, 2026
50e0c99
fix(link): populate PowerShell best-effort catch blocks for pr-hygiene
lidge-jun Nov 7, 2026
71352d3
fix(link): preserve recovery binary on failed purge rollback and stab…
lidge-jun Nov 7, 2026
75dc063
test(link): stage compiled ocx-child with SHA256SUMS for bare-host E2…
lidge-jun Nov 7, 2026
580898d
Merge remote-tracking branch 'origin/dev' into codex/maintainer-8237
lidge-jun Nov 7, 2026
e40ab37
style(ocx-child): remove trailing whitespace
lidge-jun Nov 7, 2026
28a6886
fix(ocx-child): preserve recovery and verify shutdown
lidge-jun Nov 7, 2026
1aa8880
fix(ocx-child): harden recovery identity and shutdown
lidge-jun Nov 7, 2026
3907546
fix(ocx-child): preserve local state during detach and sync
lidge-jun Nov 7, 2026
87e1501
Merge origin/dev into lane/8237-blockers
lidge-jun Nov 8, 2026
768a058
test(ocx-child): add env-gated test barrier and shared mock Home harness
lidge-jun Nov 8, 2026
fb6d5d5
fix(ocx-child): serialize and own the shared CODEX_HOME
lidge-jun Nov 8, 2026
1612d7a
style(ocx-child): format lifecycle assertion message
lidge-jun Nov 8, 2026
27712f5
fix(ocx-child): recheck home identity around every restoration step
lidge-jun Nov 8, 2026
62aab7a
fix(ocx-child): commit a daemon generation before it serves
lidge-jun Nov 8, 2026
02f8376
fix(ocx-child): let shutdown end in-flight parity work and a busy pool
lidge-jun Nov 8, 2026
86f199a
test(ocx-child): cover removal of an unchanged injected web_search on…
lidge-jun Nov 8, 2026
e459d86
docs(link): state the single-owner CODEX_HOME rule for ocx-child
lidge-jun Nov 8, 2026
d83dc2b
Merge origin/feat/ocx-child-ephemeral-link into feat/ocx-child-plant-ux
lidge-jun Nov 8, 2026
ac4be31
fix(link): refuse to plant ocx-child when the Child cannot verify its…
lidge-jun Nov 8, 2026
00c11d6
fix(ocx-child): let Home catalog slugs win in models_cache sync after…
lidge-jun Nov 8, 2026
3dfac1e
test(link): give the bare-host plant E2E a SHA-256 tool
lidge-jun Nov 8, 2026
815d62d
fix(link): purge through the managed planted ocx-child and report a p…
lidge-jun Nov 8, 2026
4422013
fix(ocx-child): commit a rollback-restarted relay after removing the …
lidge-jun Nov 8, 2026
8808f98
fix(ocx-child): bound each control-mode request head with a deadline
lidge-jun Nov 8, 2026
6c917b3
fix(ocx-child): cap the cancellable parity connect at one second
lidge-jun Nov 8, 2026
7d3e24c
fix(ocx-child): stop re-permissioning existing OPENCODEX_HOME when ta…
lidge-jun Nov 8, 2026
2cb3218
fix(link): treat an empty OPENCODEX_HOME as unset when looking up cac…
lidge-jun Nov 8, 2026
f9d1b54
fix(ocx-child): keep both catalogs in the backup across the cache write
lidge-jun Nov 8, 2026
62d9e2f
fix(ocx-child): remove the padding inside the relay restart hint
lidge-jun Nov 8, 2026
0f4b36d
fix(ocx-child): compile the test barrier out of release builds
lidge-jun Nov 8, 2026
daa9973
docs(link): say where a plantable ocx-child build must come from today
lidge-jun Nov 8, 2026
0d0f17d
fix(ocx-child): open lock files without following links
lidge-jun Nov 8, 2026
3e6dfec
fix(ocx-child): wipe the shutdown secret and token-bearing request heads
lidge-jun Nov 8, 2026
6ceb5ed
docs(link): state Child sync serialization and the restore window pre…
lidge-jun Nov 8, 2026
ff6f9e5
test(link): find the ocx-child binary under CARGO_TARGET_DIR
lidge-jun Nov 8, 2026
79e4a18
fix(ocx-child): stop printing the link key id after attach
lidge-jun Nov 8, 2026
53d58b9
Merge origin/feat/ocx-child-ephemeral-link into lane/8255-r2
lidge-jun Nov 8, 2026
916aac6
fix(link): keep an unrunnable planted ocx-child while any recovery re…
lidge-jun Nov 8, 2026
88424a8
fix(gui): explain a refused --purge on a full install in the #remote …
lidge-jun Nov 8, 2026
8056475
Merge remote-tracking branch 'origin/dev' into lane/8255-r3
lidge-jun Nov 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions devlog/_plan/261107_ocx_child_plant_ux/000_roadmap.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions devlog/_plan/261107_ocx_child_plant_ux/001_audit_and_status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Audit & Work-Phase Status Ledger (`261107_ocx_child_plant_ux`)

> Reader contract: Durable audit, disclosure, and work-phase status record for `feat/ocx-child-plant-ux` (stacked on PR #8237 `feat/ocx-child-ephemeral-link`).

`wp0` (docs-first roadmap cycle per `LOOP-DOCS-FIRST-01`) audited all five plan documents (`000_roadmap.md` and `010..040`) with zero Critical/High blockers (`VERDICT: PASS`) and folded all four Medium/Low refinements into `010..030` prior to implementation.

## 1. `wp0` Audit Record & Same-Family Disclosure (`REVIEW-DECORRELATE-01`)

- **Model inheritance disclosure**: Per explicit user instruction ("너 상속 으로 해서"), both architect (`01a1b2ed-664f-7180-99b1-fd1091864296`) and independent A-phase reviewer (`01a1b2f8-ffdf-77b0-a854-0141550b121d`) inherited the coordinator's model family (`google-antigravity/gemini-4-argon`); cross-family decorrelation was intentionally not used.
- **Reviewer `01a1b2f8-ffdf-77b0-a854-0141550b121d` verdict**: `VERDICT: PASS` (`blocking_issues: []`).
- **Dispositions of Medium/Low findings**:
1. `M1` (Accepted & folded into `020`): Update `tests/server/link-enrollment-ephemeral-child.test.ts:192` (and `structure/remote-link.md:191` in `wp4`) from `"child_restart_unsupported"` to `"child_port_busy"` when `before.code === 0` on an ephemeral Child.
2. `M2` (Accepted & folded into `010`): Prefix all three conflict branches in `native/ocx-child/src/toml_surgeon.rs:399,421,433` (including user-owned `model_catalog_json`) with `ocx-child-conflict:child_external_provider_conflict:`.
3. `L3` (Accepted & folded into `020`/`030`): Keep `childKind` omitted on `POST /api/link/issue` client-initiated records so existing exact-store assertions stay green, and accept both legacy `{ alias, fingerprint, ocxVersion }` and extended `allowMissing` confirmation responses in `src/cli/link-enrollment.ts`.
4. `L4` (Accepted & folded into `030`): Leave `src/cli/capabilities-base.ts` untouched so `skills/ocx/references/*` (outside `writeScope`) remains byte-identical and `bun run skill:surface:check` stays green.

## 2. Work-Phase Status Table

| Work Phase | Status | Summary & Verification Receipt |
|---|---|---|
| `wp0` | DONE | Docs-first roadmap (`000_roadmap.md`, `001_audit_and_status.md`, `010..040`) completed, audited (`VERDICT: PASS`), and verified with `privacy:scan` + `structure:check` (`exit 0`); zero production files modified |
| `wp1` | DONE | Rust `native/ocx-child` `--purge` & conflict prefixes + `src/link/` foundations (`9/9` Cargo tests, `51/51` Bun foundation & layout tests) |
| `wp2` | DONE | Home Management API `confirm-host`, `apply` plant/rollback, `status`, and `DELETE --purge` (`90/90` server, projection, and real-binary E2E tests) |
| `wp3` | DONE | CLI (`--plant`, `--purge`) & `#remote` GUI + 11 locales (`82/82` CLI link tests, `3738/3738` GUI tests, `lint:i18n`, `lint`, `build:gui`) |
| `wp4` | IN_PROGRESS | SoT docs (`structure/remote-link.md`, `docs-site/`), `origin/pr-assets` screenshots (`4d57c948b78bc66f2fb06b01cd9eb947766f1a60`), `35729/35729` `test:changed` pass, stacked PR & CI watch |

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Work-Phase 2 (`wp2`): Home Management API — Inspection, SSH Plant & Rollback, `childKind` Projection, and `DELETE --purge`

> Depends on: `000_roadmap.md`, `010_wp1_rust_purge_and_conflicts.md`. Consumed types: `LinkChildKind`, `ChildTargetTriple`, `RemoteRuntimeKind`, `ChildBinarySource`, `inspectRemoteChildRuntime`, `remotePlantChildArgv`, `remotePurgePlantedChildArgv`.

## 1. File Change Map

| Action | Path | Summary |
|---|---|---|
| MODIFY | `src/server/management/link-route-state.ts` | Extend `PendingLinkHost` and `ConfirmedLinkHost` with optional `runtimeKind`, `targetTriple`, `plantable`, `binarySizeBytes`, `plantUnavailableReason` |
| MODIFY | `src/server/management/link-enrollment.ts` | Extend `probe` and `confirmHost` with `allowMissing?: boolean`; extend `apply` with `plant?: boolean`, streaming verified `ocx-child` bytes over SSH `stdin`, rolling back newly planted binary on failure, persisting `childKind`, and mapping `child_port_busy` / `child_external_provider_conflict` to `409` |
| MODIFY | `src/link/status-projection.ts` | Project optional `childKind?: "full" | "ephemeral"` on `LinkStatusDto["links"][number]` |
| MODIFY | `src/server/management/link-routes.ts` | Extend `GET /api/link/status` (`homeCodexSettingsDigest` on `gui-session`) and `DELETE /api/link/:id` (`{ force?: boolean, purge?: boolean }`, passing `["disconnect", "--purge"]` when `purge === true && record.childKind === "ephemeral"`) |
| MODIFY | `tests/server/link-enrollment-ephemeral-child.test.ts` | Add server route tests for `confirm-host` (`allowMissing: true` across `missing`/`ephemeral`/`full`), `apply` (`plant: true` success, missing `plant` refusal, post-plant failure rollback, `child_port_busy`, `child_external_provider_conflict`), `status` (`childKind` + `homeCodexSettingsDigest`), and `DELETE` (`purge: true` vs default vs `force+purge` `400`) |
| MODIFY | `tests/clients/link-ocx-child-e2e.test.ts` | Add real-binary E2E case streaming compiled `ocx-child` into a bare Child `$HOME/.cache/opencodex/bin/ocx-child`, enrolling, relaying, and running `disconnect --purge` to verify zero leftover binary or `child/` directory |

## 2. Diff-Level Specifications

### 2.1 `src/server/management/link-route-state.ts`

```diff
--- a/src/server/management/link-route-state.ts
+++ b/src/server/management/link-route-state.ts
@@ -18,10 +18,16 @@ export interface PendingLinkHost {
probedAt: number;
sshLatencyMs?: number;
remotePlatform?: "posix" | "windows";
+ runtimeKind?: "full" | "ephemeral" | "missing";
+ targetTriple?: import("../../link/child-binary-source").ChildTargetTriple | null;
}

export interface ConfirmedLinkHost extends PendingLinkHost {
- ocxVersion: string;
+ ocxVersion: string | null;
+ plantable?: boolean;
+ binarySizeBytes?: number;
+ plantUnavailableReason?: import("../../link/child-binary-source").PlantUnavailableReason;
}
```

### 2.2 `src/server/management/link-enrollment.ts`

1. **`probe`**:
- Accept `{ alias }`, `{ alias, includeVersion }`, or `{ alias, includeVersion, allowMissing }` (`includeVersion` and `allowMissing` must be `boolean` when present).
- When `includeVersion === true && allowMissing !== true`: unchanged legacy behavior (`remoteOcxArgv(["--version"], remotePlatform)` -> `{ alias, fingerprint, keyType, ocxVersion }`).
- When `includeVersion === true && allowMissing === true`: run `inspectRemoteChildRuntime(runner, alias, path, remotePlatform)`.
- If `runtimeKind === "full"` or `"ephemeral"`: verify `MIN_REMOTE_OCX` (`>= 2.66.0`, else `409 remote_ocx_outdated`), store `runtimeKind` and `targetTriple` on `PendingLinkHost`, and return `{ alias, fingerprint, keyType, ocxVersion, runtimeKind, targetTriple, plantable: false }`.
- If `runtimeKind === "missing"`: resolve `resolveChildBinarySource(targetTriple)`, store on `PendingLinkHost`, and return `{ alias, fingerprint, keyType, ocxVersion: null, runtimeKind: "missing", targetTriple, plantable, ...(binarySizeBytes !== undefined ? { binarySizeBytes } : {}), ...(plantUnavailableReason ? { plantUnavailableReason } : {}) }`.

2. **`confirmHost`**:
- Accept `{ alias, fingerprint }` or `{ alias, fingerprint, allowMissing }`.
- When `allowMissing !== true`: preserve exact legacy `remoteOcxArgv(["--version"], remotePlatform)` call and exact `{ alias, fingerprint, ocxVersion }` JSON response!
- When `allowMissing === true`: run `inspectRemoteChildRuntime(runner, alias, path, remotePlatform)`:
- If `runtimeKind !== "missing"`: check `MIN_REMOTE_OCX`, pin host in `known_hosts`, store `ConfirmedLinkHost` with `runtimeKind`, `targetTriple`, `ocxVersion`, `plantable: false`, and return `{ alias, fingerprint, ocxVersion, runtimeKind, targetTriple, plantable: false }`.
- If `runtimeKind === "missing"`: resolve `resolveChildBinarySource(targetTriple)`, pin host in `known_hosts`, store `ConfirmedLinkHost` with `ocxVersion: null, runtimeKind: "missing", targetTriple, plantable, binarySizeBytes, plantUnavailableReason`, and return that object.

3. **`apply`**:
- Accept `alias`, optional `fingerprint`, and optional `plant?: boolean`.
- If `confirmed.runtimeKind === "missing"` and `body.plant !== true`: return `fail("remote_ocx_missing", "ocx was not found on the remote host; confirm planting ocx-child before applying.", 409)`.
- Track `let plantedFreshBinary = false`.
- When `body.plant === true && confirmed.runtimeKind === "missing"`:
- `phase = "remote_plant"`; re-resolve `resolveChildBinarySource(confirmed.targetTriple ?? null)` (reading verified `Uint8Array` bytes + `sha256` right before streaming). If `!source.ok`, return `fail("remote_plant_unavailable", "No verified ocx-child binary is available for this host architecture.", 409, source.reason)`.
- Run `runner.run(buildExecArgv({ alias, argv: remotePlantChildArgv(remotePlatform, source.artifact.sizeBytes, source.artifact.sha256), knownHostsFile: knownHosts }), { stdin: source.artifact.bytes, timeoutMs: Math.max(budget, 60_000) })`.
- If nonzero exit, return `fail("remote_plant_failed", "Could not plant ocx-child on the remote host.", 502, sshFailureHint(plantRes.stderr))`.
- Set `plantedFreshBinary = true`.
- Extend `failed(response)` helper so whenever `plantedFreshBinary === true`, after `compensateNewLink` revokes any issued key and stops the tunnel, Home runs best-effort `run(remotePurgePlantedChildArgv(remotePlatform))` (which invokes `ocx-child detach --purge` if state exists and removes the newly planted managed binary) so failed enrollments leave the bare host untouched!
- Conflict mapping in `apply`:
- During `child_preflight` for `ephemeralChild`: when `before.code === 0` (or a TCP listener probe answers on `childPort`), return `fail("child_port_busy", "Port " + childPort + " is already in use on the Child; no Child files were modified.", 409)` (before any key is issued!).
- During `remote_connect`: if `connected.code !== 0`, inspect `classifyChildAttachConflict(connected.stderr)`:
- `"child_port_busy"` -> compensate (and purge if freshly planted; `remoteMayBeConnected = false` because `ocx-child` exited before writing state/files!) and return `fail("child_port_busy", "The Child's target port is already in use; no Child files were modified.", 409, sshFailureHint(connected.stderr))`.
- `"child_external_provider_conflict"` -> compensate (`remoteMayBeConnected = false` because `detect_unowned_root_routing_conflict` runs before journal/config writes!) and return `fail("child_external_provider_conflict", "The Child's ~/.codex/config.toml is already managed by another model provider or custom base URL; no Child files were modified.", 409, sshFailureHint(connected.stderr))`.
- Persist `childKind: ephemeralChild ? "ephemeral" : "full"` on hub-initiated `apply` `record` in `links.json` (leaving `POST /api/link/issue` client-initiated records without `childKind` so existing exact-store fixtures in `tests/server/link-management-routes.test.ts` stay untouched).
- Update `tests/server/link-enrollment-ephemeral-child.test.ts:192` assertion from `"child_restart_unsupported"` to `"child_port_busy"` on ephemeral foreign-listener preflight.

### 2.3 `src/link/status-projection.ts` & `src/server/management/link-routes.ts`

```diff
--- a/src/link/status-projection.ts
+++ b/src/link/status-projection.ts
@@ -20,6 +20,7 @@ export interface LinkStatusDto {
since: string;
reason: string | null;
tunnelPort: number;
+ childKind?: "full" | "ephemeral";
/** Home only: what the link carried from this Child, and the compatibility verdict. */
peer?: LinkPeerStatus;
@@ -141,6 +142,7 @@ export function projectLinkStatus(
...stateDetails(record, byId.get(record.id), compensation),
tunnelPort: record.tunnelPort,
+ ...(record.childKind ? { childKind: record.childKind } : {}),
...parity(record.id),
}));
```

In `src/server/management/link-routes.ts`:
- In `remove`: validate `body` keys `<= ["force", "purge"]`, both optional booleans, and reject `force === true && purge === true` with `400 invalid_body`. When `!force && record.direction === "hub-initiated"`, pass `purge === true && record.childKind === "ephemeral" ? ["disconnect", "--purge"] : ["disconnect"]` to `remoteOcxArgv`.
- In `GET /api/link/status`: when `ctx.principal === "gui-session"`, include `homeCodexSettingsDigest: codexSettingsDigest(projectHomeCodexSettings(ctx.config, packageVersion("0.0.0")).settings)` alongside `joinAvailable` and `joinDenied`.

## 3. Conditional Activation Scenarios (`C-ACTIVATION-GROUNDING-01`)

- Bare host `apply` without `plant: true` -> `409 remote_ocx_missing`, zero SSH writes.
- Bare host `apply` with `plant: true` + simulated `child_relay_failed` -> key revoked, tunnel stopped, and `remotePurgePlantedChildArgv` invoked to remove planted binary.
- `ocx-child` `connect` returning `ocx-child-conflict:child_external_provider_conflict` -> `409 child_external_provider_conflict` without `residual: true` and with link compensated cleanly.

## 4. `wp2` P-Phase Continuity & Architect Reflection Amendments (`01a1b316-9e8c-7a23-845e-da43f6554f0e`)

- **Continuity from `wp1` D**: Resumed from `wp1` D conclusion ("Next cycle wp2 implements Home Management API enrollment/plant/purge routes").
- **Architect `01a1b316-9e8c-7a23-845e-da43f6554f0e` verdict**: `ALIGNED` on `W2-ARCH-01..W2-ARCH-03`.
- **Folded refinements**:
1. Add `resolveChildBinarySource?: typeof import("../../link/child-binary-source").resolveChildBinarySource` to `ManagementApiDeps` in `src/server/management/context.ts` so server route tests can deterministically inject verified/unavailable binary resolutions across targets.
2. In `apply` (`src/server/management/link-enrollment.ts`), route EVERY exit after `plantedFreshBinary = true` (including `remote_port` and `child_preflight` refusals such as `child_port_busy` and `child_curl_missing`) through `await failed(...)` so newly planted binaries are always purged on failure; keep `confirmedHosts` intact on pre-plant `409 remote_ocx_missing` / `remote_plant_unavailable`; and reset `remoteMayBeConnected = false` on classified pre-write attach conflicts (`child_port_busy`, `child_external_provider_conflict`).
3. Enforce strict key sets: `probe` requires `includeVersion === true` when `allowMissing` is supplied; `confirmHost` accepts `{ alias, fingerprint }` or `{ alias, fingerprint, allowMissing }`; `apply` accepts `{ alias, fingerprint?, plant? }` (all 4 exact subsets); `DELETE /api/link/:id` accepts empty/null body or `{ force?, purge? }` and rejects `force === true && purge === true` with `400 invalid_body`.

## 5. `wp2` A-Phase Audit Fold-Backs (`01a1b322-c1e1-7c41-a560-45f51e35dca3`, `GO-WITH-FIXES`)

1. **`W2-BLK-01` (Folded — Purge exception safety & `residual` reconciliation)**: Wrap `remotePurgePlantedChildArgv` inside `failed()` in a once-only `try/catch` (`plantedFreshBinary = false`). If rollback purge exits `0`, reset `remoteMayBeConnected = false` (no residual left on Child); if rollback purge returns nonzero or throws, set `remoteMayBeConnected = true` with recovery `"Run ~/.cache/opencodex/bin/ocx-child detach --purge on the Child before retrying."`.
2. **`W2-BLK-02` (Folded — Public DTO projection, boolean guards, & `context.ts` seam)**: Add `src/server/management/context.ts` (`resolveChildBinarySource` seam) to the File Change Map, project only explicit public DTO fields from `confirmHost` (never leaking `knownHostLine`, `probedAt`, `sshLatencyMs`, or `remotePlatform`), validate `typeof allowMissing === "boolean"` in `probe`/`confirmHost` and `typeof plant === "boolean"` in `apply` (`400 invalid_body`), and wire `ctx.deps.resolveChildBinarySource ?? resolveChildBinarySource` in all three handlers.

Loading
Loading