Use the models you choose with Codex, Claude Code, Claude Desktop and Grok Build,
and connect OpenCode, Pi, Qoder, WorkBuddy, Kilo, Cline, T3 Code and more through managed configs.
See every supported tool →
Part of GOAT: run your coding agents on any model, on any machine → trygoat.ai
```bash -npm install -g @bitkyc08/opencodex +npm install -g --allow-scripts=bun @bitkyc08/opencodex ocx start ``` @@ -48,7 +49,7 @@ Pick a provider and go — same workflow, different brain.
+
+
+
`, `undone`, `child_marked_moved`, `child_removed`, `child_resumed`.
+
+No line carries a token, key, refresh token, full email or `payloadDigest`. `bun run privacy:scan` gains a
+fixture for the format.
+
+The Home will expose the audit through `GET /api/link/account-moves` on its dashboard. The Child will expose
+its own.
+
+## 8. Failure and rollback
+
+Each item commits on its own. A batch is a convenience for the person, not a transaction.
+
+| Failure | Result |
+| --- | --- |
+| Link drops before the offer reaches the Home | Child keeps the offer local and resends with the same `offerId`; the Home deduplicates by offer id. |
+| Link drops after approval, before upload | Item stays `Approved` until its window ends; the Child uploads when the link returns. |
+| Link drops during upload | The Child does not know the outcome. It re-polls; the Home's item state says `Committed` or still `Approved`. A second upload of a committed item answers `already_committed` and changes nothing. |
+| Home crashes during commit | The store writers are atomic, and commit order is store write, audit, state. On restart, an `Approved` item whose identity already exists with this item's provenance is promoted to `Committed`; anything else stays `Approved` and can be retried. |
+| Validation, dedupe or provider-missing failure | Item goes to `Failed` with a closed code; the Child's copy is unchanged and not marked moved. |
+| Link revoked or key rotated mid-offer | Every open offer on that link expires; uploads are refused. Committed items stay. |
+| Child never learns about a commit | The Child's copy stays active-capable rather than dormant; the next poll or the Home audit view corrects it. This is the safe direction because nothing is lost. |
+
+Rollback after commit is **Undo** on the Home row, available while the account still carries this move's
+provenance. It removes the Home copy through the normal account deletion path and audits `undone`. Because the
+Child kept its copy, undo loses nothing. The Child sees `undone` on its next poll and clears the `movedTo`
+marker, so its copy becomes usable again (subject to section 5: if the Home already refreshed a rotating token,
+the Child's copy may need a new sign-in, and the row says so).
+
+## 9. GOAT hub switch (H7)
+
+H7 moves the hub from the current Home (A) to a new Home (B), keeping A as the rollback until B is proven.
+OCX-4 supplies its `PoolMoving` step. Two account sources exist:
+
+1. Accounts on Children. When a Child relinks to B in `Relinking`, it can offer its local accounts to B with
+ this flow, exactly as any Child would.
+2. Accounts on A itself, which is the main pool. A is a Home, so it has no client link to B. H7 needs A to
+ offer to B over a link where A is the HTTP client. This design proposes an account-source link: a
+ Child-initiated link from A to B that enrolls through the normal probe, host confirmation and apply path,
+ uses relay auth, admits only the `/v1/link-accounts` table, and does not switch A's own client traffic or
+ connection state. A keeps serving its Children throughout (decision D10).
+
+H7 drives the steps through OpenCodex surfaces and never handles a credential:
+
+- On A (or a Child): `ocx link offer-accounts --link-id [--provider ...]` creates the offer. GOAT may run
+ it through its existing runner; the command prints offer and item ids only.
+- On B: the person approves each account on B's dashboard. GOAT shows a decision item that links there; it does
+ not approve on the person's behalf, and the approval route refuses every non-paired session.
+- GOAT reads progress from `GET /api/link/account-offers` and `GET /api/link/account-moves` (metadata only)
+ to decide when `PoolMoving` is done.
+- `RolledBack`: GOAT asks the person to Undo on B, or leaves B's copies in place; A still holds every account,
+ dormant only if A marked them moved. On rollback A's markers are cleared through the same undo signal.
+- `Committed`: A's copies stay on A, marked moved. The person removes them from A's dashboard when satisfied,
+ as §1.8 of the GOAT design already says.
+
+The `/readyz` feature `link-account-move/1` ([Runtime](../../../structure/runtime.md), `src/remote/features.ts`)
+will let GOAT and the Child check support on both ends before offering.
+
+## 10. Implementation outline (for the follow-up PRs)
+
+1. Decision record under Remote Link that extends ADR-6032: credentials may cross the link only as a sealed
+ account-move payload on the account-move table, never as forwarded request headers.
+2. `src/link/account-move-routes.ts` (table, admission), Home offer book and commit, Child offer client and
+ `movedTo` marker, sealing helper, audit writer.
+3. Home dashboard offer panel and Child "Moved to Home" list (GUI PR with screenshots).
+4. CLI: `ocx link offer-accounts`, `ocx link account-offers` (Home, read-only), `ocx link moved-accounts` (Child).
+5. Account-source link kind for H7 (only if D10 is accepted).
+6. `structure/remote-link.md` section, `structure/quota-pool.md` for transferred hints, docs-site Remote Link
+ guide.
+
+Tests: a real two-listener link fixture with isolated homes covering offer, approval refusal for loopback and
+Tailscale sessions, digest mismatch, replay of a committed upload, link revocation mid-offer, crash recovery
+between store write and state, dedupe, undo, the Child dormant marker, and no secret in any audit or log line.
+
+## 11. Decisions for the owner
+
+| ID | Question | Proposal |
+| --- | --- | --- |
+| D1 | What happens to the Child's copy after commit | Keep it, dormant (no selection, no refresh) until the person removes or explicitly resumes it |
+| D2 | Seal payloads on top of SSH and relay auth | Yes, per-offer X25519 key held in Home memory |
+| D3 | Allow "approve all" for an offer | No in v1; one decision per account |
+| D4 | Can the Home CLI approve | No; dashboard paired session only. CLI lists offers |
+| D5 | Same identity already on the Home | Refuse as `already_on_home`; later option to replace only when the Home copy needs reauthentication |
+| D6 | Imported account becomes active | Never automatically; validation-pending or inactive |
+| D7 | Include API keys (and keychain-held keys) in v1 | Yes for key pools; keychain keys only when the Child can read them without prompting |
+| D8 | Include the Codex main account | No; it belongs to Codex on that machine. Revisit only with a Codex-owned export |
+| D9 | Carry quota windows | Only as hints with future resets, never as exhaustion; durable pauses carried as pauses |
+| D10 | H7 source for the old Home's own pool | Account-source link (offer-only, no traffic switch). Alternatives: make A a full Child of B (breaks A serving its Children) or re-sign every account on B (no move at all) |
+| D11 | Offer and upload windows | Offer lifetime 24 h, upload window 10 min after approval |
+| D12 | Who may start an offer on the Child | Child paired dashboard session or Child CLI with its admin token |
+
+The security analysis is in [010_security.md](010_security.md).
+
diff --git a/devlog/_plan/261022_child_home_account_move/010_security.md b/devlog/_plan/261022_child_home_account_move/010_security.md
new file mode 100644
index 00000000000..b22f7144f00
--- /dev/null
+++ b/devlog/_plan/261022_child_home_account_move/010_security.md
@@ -0,0 +1,81 @@
+# OCX-4 security note: Child-to-Home account move
+
+This is a threat model for a design that is not built. It describes no existing weakness.
+
+## Assets and trust today
+
+The assets are provider credentials: OAuth refresh and access tokens, Codex pool tokens and API keys. Today each
+stays on the machine that signed in, and ADR-6032 keeps them off the link. After this change one credential
+can exist on two machines, and the link carries it once, sealed.
+
+The Home already sees everything a Child sends upstream, because it relays every request. A compromised Home
+can already read a linked Child's prompts. Giving it the Child's credentials adds the ability to use those
+accounts directly. That is the purpose of the feature and the reason consent sits on the Home, per account.
+
+## Threats
+
+### A compromised Child
+
+A Child under an attacker's control holds the link key and can pass relay auth. What it can try:
+
+- **Plant an attacker-owned account in the Home pool.** If the Home routes the person's traffic through it,
+ the attacker's upstream account receives the person's prompts. Defenses: nothing moves without a per-account
+ approval on a paired Home session; the row shows the masked identity so the person can recognize their own
+ account; imports never become active automatically (D6); the audit names the source link. Residual risk: a
+ person who approves an identity they do not recognize. The UI should say plainly that approving lets the
+ Home send requests through this account.
+- **Redirect a credential.** A key paired with a malicious base URL would leak every request. Defense: only
+ credentials move; the item binds to an existing Home provider and that provider's endpoint, and the payload
+ parser rejects endpoint, header and model fields.
+- **Overwrite a Home account.** Defense: dedupe refuses an existing identity and never overwrites (D5).
+- **Exhaust the Home.** Bounded open offers per link, bounded items per offer, bounded payload size, strict
+ parsing, and a rate limit on offer creation. Offers carry no secrets, so storing them costs little.
+- **Approve its own offer.** The approval route is a Home dashboard route; the hub-link listener never admits
+ it, and a loopback-bootstrapped session on the Home cannot approve.
+- **Lie in quota hints.** Hints only order selection and expire; they never mark an account exhausted or
+ paused on the Home. A durable pause arriving from the Child makes the account paused, which is a
+ denial-of-service on that one account only and is visible on the row before approval.
+
+### Replay
+
+- A captured relay request cannot be replayed on another socket: relay auth binds the request to one proven
+ socket with a one-use nonce.
+- A repeated upload for a committed item answers `already_committed` and changes nothing; item state moves
+ forward only.
+- An upload must match the `payloadDigest` the person approved and arrive inside the upload window, on the
+ same link id and key. Rotation or revocation expires open offers.
+- The sealing key is per offer and dropped when the offer closes, so an old sealed payload cannot be opened
+ later even if replayed into a new offer.
+
+### Partial transfer
+
+Each item commits on its own, through an atomic store writer, under the config mutation lock. A crash leaves
+the item either uncommitted (retryable) or committed with provenance that restart reconciliation recognizes.
+The Child's copy is untouched until it learns of the commit, so the failure direction is "the account exists in
+both places", never "the account exists nowhere".
+
+### Interception on a machine
+
+SSH encrypts between machines. Relay auth proves the port's owner but, as the
+[Remote Link doc](../../../structure/remote-link.md#connection-bound-relay-authentication) states, is not
+encryption against an active local intermediary on the loopback port. Sealing payloads to a per-offer Home key
+(D2) closes that gap for credentials: an intermediary sees only ciphertext bound to the offer and item.
+
+### Refresh-token reuse
+
+Two live copies of a rotating refresh token sign each other out. Making the Child's copy dormant at commit
+avoids this in normal use; explicit resume on a standalone Child warns about it (D1).
+
+### Leakage through records
+
+Audit and offer files carry masked identities and digests only, with private permissions. Digests of OAuth
+identities are over account ids or subjects. For a bare API key the identity digest is over the key, which is
+high entropy, so the digest does not make the key cheaper to recover. No log line carries a payload, a token
+or a full email. `privacy:scan` covers the new formats.
+
+## What stays out of reach
+
+- SSH exec, `scp`, export files and clipboard paths are never used.
+- GOAT drives offers and reads progress but never receives a credential and cannot approve.
+- A Home never pulls from a Child; there is no route by which a Home asks a Child for credentials.
+
diff --git a/devlog/_plan/261107_auto_update_parity/000_plan.md b/devlog/_plan/261107_auto_update_parity/000_plan.md
new file mode 100644
index 00000000000..5fb9365c8af
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/000_plan.md
@@ -0,0 +1,266 @@
+# Automatic update parity — roadmap draft
+
+OpenCodex already checks for desktop updates and has a guarded package update
+orchestrator. This unit extends those owners into a user-controlled automatic
+update experience, with the existing sidebar download control as its entry
+point. The [inventory](001_inventory.md) separates observed behavior from gaps;
+the [gap ledger](002_gap_ledger.md) assigns completion evidence to each one.
+The [contract map](003_contracts.md) tracks proposed fields and their owners.
+The roadmap's architecture reflection and independent plan audit are complete
+for the snapshot recorded below. Each implementation unit remains a proposal
+until its own current-base plan audit, tests and applicable independent security
+review have passed; no update behavior is claimed to be implemented.
+
+## Loop specification
+
+- **Archetype and trigger:** satisfy-spec, scoped HOTL; the requested GOAT
+ parity work began on 2026-11-07.
+- **Work class:** C4 for updater, signing, installation, release and authority
+ changes; the docs-only roadmap is independently audited before code. GOAT's
+ built-in discipline was read from the pinned reference revision at
+ `packages/provider-core/src/server/builtinSkills/goatDiscipline.ts:7`.
+- **Goal:** signed, user-controllable updates for desktop and eligible linked
+ installations; useful progress, deferral and failure states in the sidebar.
+- **Non-goals:** modify the user's installed app, services or running proxy;
+ merge pull requests; publish releases; broaden workflow triggers or
+ permissions; add optional-subsystem imports to core paths; add telemetry.
+- **Verifier:** per-unit failing-first behavior tests, isolated homes and mock
+ feeds, typecheck and privacy scan, ownership and locale checks, UI screenshots,
+ exact-head required CI, and independent security review where applicable.
+ Commands and the source each observes belong in each completed decade plan.
+- **Stop:** report each PR ready for coordinator integration only after its
+ exact-head checks and required reviews pass. Continue until every ledger gap
+ is closed or a concrete blocker is reported.
+- **Memory:** this public unit and the bound Codexclaw goalplan; security
+ working notes belong in `.tmp/`, never in this directory.
+- **Outcomes:** DONE requires the complete audited scope and evidence;
+ BLOCKED or NEEDS_HUMAN identify missing authority, supply or capability;
+ BUDGET_EXHAUSTED applies only to an actual bound.
+- **Escalation and bounds:** report decisions outside the granted scope to the
+ coordinator. No token or wall-time bound stated; host limits apply. Keep load
+ moderate: no full root test suite. User authorized commits, pushes, PRs and
+ platform proof when needed, but not merge or release publication. A PR-branch
+ `lane=all` requires an explicit maintainer request on that PR; run it once on
+ the exact intended head, never redispatch because the head moved or rerun to
+ green, and cancel owned superseded runs. Ordinary full dispatches are batch
+ checks on `dev`, as required by `AGENTS.md`.
+
+## Draft dependency map
+
+1. **Signed supply and channel contracts.** Determine and implement a
+ verifiable artifact source for every automatic install surface. Extend
+ release metadata only when it can preserve existing signing and CI policy;
+ missing, invalid, or unverifiable signatures must refuse the automatic path.
+ Desktop stable/preview/nightly labels must correspond to feeds that actually
+ exist. npm provenance is a candidate for investigation, not yet a verified
+ consumer contract. Existing explicit package commands keep their current
+ behavior until a verified-supply migration ships. That migration should
+ retain their interfaces but may refuse operations that previously succeeded;
+ document the impact and supported alternatives for each manager.
+2. **Automatic activation admission.** Extend the existing automatic restart
+ fence and handoff (`src/server/management/system-restart.ts:385`) with an
+ identity-bound, expiring update reservation that proves no work remains and
+ transfers atomically into shutdown. Release its temporary fence before shutdown waits
+ for temporary drains. A plain sample of activity is insufficient: the
+ existing terminal drain aborts turns after its deadline
+ (`src/server/lifecycle.ts:520`), and an unreleased temporary fence can make
+ shutdown wait on itself (`src/server/lifecycle.ts:131`).
+3. **Desktop staged update engine.** Extend the Tauri owner to check, download
+ with progress, verify, retain a staged update, and apply through the existing
+ restart/drain and recovery path. Persist user controls and channel state;
+ account for stale checks, concurrent commands, unavailable feeds and failed
+ or interrupted installs. Signature verification stays mandatory. The exact
+ default is an audit decision; it must be justified against GOAT's defaults
+ and the user's control requirement.
+4. **npm preparation and activation.** Extract verified npm preparation
+ from the existing transactional swap. Automatic paths
+ must verify every artifact they execute, including installer dependencies;
+ unsupported owners remain visible and never silently fall back.
+5. **Sidebar and update panel.** Extend the existing session-scoped snapshot
+ and sidebar download button to express current, available, downloading,
+ ready and failed states; open a panel with channel, automatic download and
+ installation controls, release notes, restart and Later. Preserve the native
+ update-page command boundary and browser package-update behavior. Resolve the
+ footer version truncation and validate accessibility, locales and geometry.
+6. **Linked-machine orchestration.** Reuse Home–Child link identity, pinned
+ sync and the orchestrated plan/apply path where their contracts fit, while
+ adding signed-target verification, device-side controls, compatibility and
+ idle admission, per-machine progress, and failure/recovery reporting. Treat
+ unavailable identity, incompatibility, unverified signatures and offline
+ machines as explicit non-success states. Do not equate catalog sync with an
+ installation update.
+7. **Standalone installation support.** Add a signed, owner-bound standalone
+ adapter and platform-specific recovery after the common supply and admission
+ contracts exist; do not treat the current standalone refusal as closure.
+8. **Child-initiated link transport.** Add a separately authorized Child-pull
+ update exchange for links without Home-to-Child SSH; keep the existing
+ management relay read-only and preserve destination consent.
+9. **pnpm-owned installations.** Implement and prove the selected global
+ owner's offline signed preparation, manager-compatible transition and rollback
+ in `090`; unsupported layouts remain open until proven.
+10. **Bun-owned installations.** Implement and prove signed offline
+ preparation, global metadata/bin ownership, activation and rollback in
+ `100`; do not treat an ordinary global add as a staged transaction.
+
+The later units extend the preceding integration contracts: `070` adds a
+standalone owner to the linked update coordinator, `080` carries those owners
+over the Child-initiated transport, `090` adds a pnpm adapter to both link
+directions, and `100` reuses the resulting manager-stage interface for Bun.
+Each still has its own local behavior and independently reviewable PR. This
+dependency chain preserves the full goal even when an earlier owner is not yet
+eligible for automatic activation.
+
+Each unit will receive a decade-numbered document with its exact file map,
+before/after behavior, public contracts, acceptance scenarios and fresh
+verifiers. Each produces a separate PR against `dev`; dependency and base
+strategy will be resolved in the audited plan. Any updater, package installation,
+release, signing or workflow PR requires a separate independent security PASS.
+The owner requested no full root suite; record focused coverage and the CI
+coverage left outstanding rather than describing an unrun suite as passing.
+
+## Delivery and base strategy
+
+Each unit is a separate PR whose base is `dev`, as requested. Do not create a
+stack whose child targets another PR. After the docs-only roadmap PR and each
+implementation PR reaches exact-head readiness, report its URL, head, checks and
+reviews to the coordinator. The coordinator alone decides whether and when to
+merge. Once the prerequisite is actually on `origin/dev`, fetch and verify that
+head before starting the next unit on a fresh branch from it; revalidate that
+unit's prewritten plan against the resulting code. If a prerequisite is not yet
+integrated, watch the PR through GOAT and wait for the coordinator rather than
+manufacturing a downstream base or merging it here. A PR description uses the
+repository's Summary, Verification and Checklist template. GUI PRs link a
+reviewable screenshot hosted through `pr-assets`, never committed to the PR
+branch. Add `ci:full` when subprocesses, files read as data or generated
+outputs require coverage the changed-test import graph cannot discover
+(`MAINTAINERS.md:65`). Required CI must complete on the current head.
+
+For user-facing changes, compare the canonical English installation,
+desktop-app and remote-link pages and their existing `fr`, `ja`, `ko`, `ru`,
+`tr`, `zh-cn` and `zh-tw` translations; update every affected translation so
+it does not contradict the English source. For any docs-site edit, run its
+required `cd docs-site && bun install --frozen-lockfile && bun run build`
+(`docs-site/AGENTS.md`). GUI has eleven locales and its own locale parity
+checks. Review mapped `structure/` owners on each unit; `structure:check`
+does not itself establish that the prose accurately describes new behavior.
+
+## Consultation and audit
+
+Architect task: `auto-update-parity-architect-01`, read-only, Codex
+`gpt-6.1-sol` with xhigh reasoning. Proposal received 2026-11-07 at the pinned
+revisions in `001_inventory.md`; the following dispositions are provisional until
+the same architect reflects on the concrete plan and the independent audit passes:
+
+| Decision | Main disposition |
+| --- | --- |
+| AU-001 installer ownership | Accept: retain distinct native and package owners and their existing mutation gates. |
+| AU-002 controls | Accept: separate check, download and installation policy and revalidate before effects; preserve manual activation as the proposed default. |
+| AU-003 candidate | Accept with amendment: exact candidate and provenance must satisfy the user's mandatory signature requirement, not just registry SRI. |
+| AU-004 preparation | Accept conditionally: staged handles stay owner-held; npm, pnpm and Bun support must be proven individually, not assumed. |
+| AU-005 admission | Accept: add a separate reservation unit and explicitly prove the shutdown handoff cannot race admission or wait on its own temporary lease. |
+| AU-006 recovery | Accept: distinguish installation from a healthy serving target, preserve Stop/Resume intent and suppress repeat failed targets. Assess GOAT probation parity explicitly rather than treating it as automatic proof. |
+| AU-007 Child authority | Accept with amendment: diagnostics do not finish the requested goal; the roadmap must include separately authorized and security-reviewed remote binary updates, not defer them out of scope. Preserve `ocx.sync.fanout/1`. |
+| AU-008 channel and rollout | Accept: derive supported feeds from actual signed publication and never invent a nightly or preview channel. Document rollout and policy defaults. |
+
+The same architect's reflection (`auto-update-parity-architect-reflection-01`)
+returned **MISALIGNED**: supply/trust, activity fencing and stop-to-installer
+handoff, manager-specific staging, remote authority and resumption, policy and
+recovery, sidebar wire/cadence, and exact PR/test mapping were incomplete. The
+unit docs subsequently addressed these findings as described below. In particular, the existing restart
+route's handoff is not by itself an installer-stop handoff, and peer observations
+are not authorization.
+
+The user's authorization for platform proof operates within the unchanged
+`AGENTS.md` CI policy stated above. Subsequent architect reflection and the
+independent roadmap audit are recorded below.
+
+The third reflection (`auto-update-parity-architect-reflection-03`) returned
+**MISALIGNED** against the 14-file snapshot: executable manager transitions,
+update-stop authority and complete activity fencing, platform recovery actor,
+signing/rotation, destination authority and Child-pull transport, future-target
+compatibility, and nightly source/CI binding and shared-contract ownership needed
+decisions and evidence at that point. The manager units now explicitly require isolated
+pre-audit research; `003` specifies a candidate capability descriptor and
+`010` owns shared parser/test-vector delivery before `020`. These amendments
+were proposals awaiting the later reflections and independent audit below.
+
+The fourth reflection (`auto-update-parity-architect-reflection-04`) reviewed
+snapshot `1498639d17aef1013f99e406fc8ae574588a8f3da558ca6b7a8c5c3456e54cc8`
+and returned **MISALIGNED**. Its six material findings concerned bounded native
+acquisition; retaining a verified stage and grouped notes; cross-process
+admission/readback; platform guardian and remote Desktop handoff;
+simultaneous Home/Child compatibility; and manager activation/recovery. The
+main agent accepts these findings and has amended `003`, `010`, `020`, `030`,
+`050`, `060`, `090` and `100` with candidate contracts and evidence from the
+two completed isolated manager investigations. These amendments remain
+unverified proposals at that point, not a security PASS. Subsequent architect
+reflections and the independent roadmap audit are recorded below.
+
+Reflection 05 reviewed snapshot
+`21bdadc2bc897481027b534a43f48de8704a97b1fb2eb11250c5edf04daa132f`
+and returned **MISALIGNED**. The main agent accepted its six remaining
+findings: installed channel provenance and browser policy, locale Back
+navigation, quit/stopped-runtime health semantics, Windows app/installer
+handoff, cross-runtime registry interoperability, and manager-generated output
+authority. `003`, `010`, `020`, `030`, `040`, `050`, `090` and `100` now
+describe proposed resolutions and falsifiable tests. The installed SQLite
+dependency and Node/Bun broker are proposed new reviewed boundaries, not
+existing capabilities.
+
+Reflection 06 reviewed snapshot
+`cd9d7c20127a19fc08452c3203f9320ccdbe3d17a81d50aee942fa18d5145b5b`
+and found the six preceding design amendments addressed, while requiring the
+CI-policy wording above and the native-to-badge channel transport in `003`
+and `050`. The main agent accepted both findings. The subsequent audit is
+recorded below; implementation proofs, Rust 1.90 and Node 18 interop and the
+platform matrix remain gates.
+
+The independent roadmap audit of that snapshot returned **GO-WITH-FIXES** with
+two medium blockers: the desktop badge's old non-null channel DTO lacked an
+unknown-provenance migration, and linked-update dashboard actions lacked an
+end-to-end management and GUI contract. The main agent accepted both findings;
+`003`, `050`, `060` and `080` now propose separate versioned badge and linked
+dashboard DTOs, precise legacy/old-server behavior, authorized actions and
+future regression tests. The same independent reviewer re-audited these
+amendments as recorded below.
+
+The independent follow-up accepted the badge DTO correction and identified a
+remaining Retry A→B request-correlation blocker plus a non-blocking warm-cache
+failure presentation issue. The same architect independently identified the
+Retry gap. The main agent accepted both; `060` and `080` now specify durable
+request-ID readback and replay behavior, while `050` requires stale badge data
+to be visibly unknown after a failed latest observation. The subsequent
+architect reflection and independent re-audit are recorded below.
+
+## Roadmap audit disposition
+
+The same architect's reflection 09 returned **ALIGNED** and independent audit
+round 03 returned **PASS** for the 14-file snapshot
+`251f5fc6aebd2c28d3c88096caa3033266587c55db3b56f613180b9275350cf9`.
+The review closed the badge DTO, linked dashboard action/readback and stale
+cache findings; its final round found no new high or critical blocker. The
+reviewer checked staged whitespace, the privacy scan, citation ranges and
+the changed consumer paths. This disposition records a plan audit only: no
+proposed update implementation, artifact signature, platform install,
+security review or exact-head PR CI is claimed to have passed.
+
+Before a PR is marked ready, fetch the current `origin/dev` and revalidate the
+inventory and implementation plans on its exact resulting base. The last
+audited fetched base was `35d5010f502a8efd973d75c9e7a1b44eecfde777`; its
+WebSocket turn admission attaches a workflow lease and its binary GUI switch
+uses `role="switch"` and `aria-checked`. Later upstream changes need a new
+impact assessment. Every implementation unit remains subject to its own
+failing-first tests, required checks and independent security review where
+applicable; this roadmap audit is not permission to skip those gates.
+
+The independent roadmap reviewer must receive the original goal and hard
+constraints, the exact plan revision, and the pinned source revisions. The
+review must challenge the full file and field chains, reachable negative cases,
+cross-version compatibility, source-to-artifact verification, destination
+authority, recovery and PR dependency order. It must verify existing commands
+and identify candidate commands that do not yet exist or do not observe the
+proposed change. Record each finding's acceptance or evidence-backed rebuttal,
+amend the plan, and re-review with the same reviewer until blockers are closed;
+an architect reflection is not the independent audit. Security-sensitive
+working notes remain in `.tmp/`. The review packet will attach the code-review
+and search skills and require a normalized verdict.
diff --git a/devlog/_plan/261107_auto_update_parity/001_inventory.md b/devlog/_plan/261107_auto_update_parity/001_inventory.md
new file mode 100644
index 00000000000..68b564a2088
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/001_inventory.md
@@ -0,0 +1,89 @@
+# Update parity inventory
+
+Baseline: OpenCodex `803d6c0b4dbb4823a9379e485e417f3f9e3d976c`; GOAT
+`64e7198988b69941cd2eec5160fd734df9074302` (`origin/dev`). GOAT paths below
+refer to that read-only Git revision. The independent architecture inventory
+informed this comparison. The audit recorded in `000` covers an earlier named
+snapshot; the current revision and its independent review remain open.
+
+| Surface | Existing evidence | Initial gap to investigate |
+| --- | --- | --- |
+| Desktop checks | OpenCodex checks after 30 seconds and every six hours (`desktop/src-tauri/src/updater.rs:448`). GOAT starts after 15 seconds and checks every four minutes (`apps/desktop/src/updates/DesktopUpdates.ts:69`, `:1036`, GOAT revision above). | Decide cadence and user controls; do not add a duplicate scheduler. |
+| GOAT Electron integration | GOAT wraps the platform updater and installs a request deadline before first use (`apps/desktop/src/electron/ElectronUpdater.ts:90`, `:100`, `:120`, GOAT revision above); its test verifies lazy initialization and that deadline (`apps/desktop/src/electron/ElectronUpdater.test.ts:38`, `:46`). The Electron app wrapper exposes quit/relaunch and the before-quit-for-update event (`apps/desktop/src/electron/ElectronApp.ts:166`, `:174`, `:234`, GOAT revision above). Automatic-update tests cover pause, retry, reservation and busy-at-admission cases (`apps/desktop/src/updates/DesktopUpdates.automatic.test.ts:91`, `:116`, `:271`, `:652`, GOAT revision above). | Use bounded requests and verify equivalent behavior through Tauri's own owner; no Electron code is imported into OpenCodex. |
+| Desktop download and install | OpenCodex downloads through the pinned updater, which verifies the signature, before its guarded restart and install; failed install can recover the runtime (`desktop/src-tauri/src/updater.rs:388`, `desktop/src-tauri/src/updater.rs:423`). | Expose progress and verified staged readiness while preserving signature verification and drain/recovery. |
+| Desktop badge | The desktop snapshot includes a session, version, availability, timestamp and phase (`src/update/desktop-badge.ts:4`); the badge projection currently exposes availability and unknown but discards the phase (`src/update/desktop-badge.ts:74`). | Carry authoritative progress and failure states to the footer without treating missing or stale evidence as current. |
+| Sidebar | `gui/src/components/sidebar-github-row.tsx:77` polls the badge and `:157` renders the existing download button; `gui/src/App.tsx:591` opens the native update page on desktop. The version is rendered at `gui/src/App.tsx:602` and deliberately ellipsized by `gui/src/design/bridge.css:429`. Native update commands require the trusted update page (`desktop/src-tauri/src/lib.rs:272` at the baseline above). | Make that button the requested stateful update-panel entry and resolve the reported truncated version; retain browser/package behavior and the existing native command origin boundary. |
+| Package updates | The Node launcher imports the existing orchestrated plan/apply, installation ownership, transactional install and guarded runtime mechanisms (`bin/ocx.mjs:50`, `bin/ocx.mjs:69`). The CLI advertises latest/preview plans and token-bound apply (`bin/ocx.mjs:1322`). npm publication uses trusted publishing with generated provenance (`.github/workflows/release.yml:1152`). | Reuse ownership and guarded apply contracts, and require the requested publisher signature verification for update artifacts. Validate a concrete signed supply and its availability before treating provenance as a solution. Distinguish package and signed desktop-bundle supply. |
+| Package notification | Version cache refresh interval is 20 hours, with a 40-hour maximum cache age (`src/update/notify.ts:19`); background refresh is detached when stale (`src/update/notify.ts:182`). The in-process scheduler honors `OCX_DISABLE_UPDATE_CHECK=1`, avoids unsupported installers and backs off after failed checks (`src/update/refresh-scheduler.ts:24`, `:52`, `:87`). | Preserve the existing opt-out and no-refresh semantics while adding explicit independent controls. |
+| Browser dialog | The existing dashboard update dialog offers latest/preview, checks, errors, restart choice and an explicit update action (`gui/src/pages/dashboard-dialogs.tsx:29`). | Keep its installed-package semantics consistent with any shared state and copy. |
+| Channels | OpenCodex's configured Tauri feed uses `latest.json` (`desktop/src-tauri/tauri.conf.json:68`); release verification generates that manifest only with the updater signing key and requires signatures in that case (`.github/workflows/release.yml:824`). npm uses `latest` and `preview` tags (`.github/workflows/release.yml:1051`). GOAT publishes stable and nightly feeds; its documented preview is a PR build, not a third published feed (`docs/internals/release-channels.md:19`, `docs/internals/release-channels.md:80`, GOAT revision above). | Establish actual artifact availability before presenting a channel as usable; never invent a nightly feed or treat a digest as a signature. |
+| GOAT update states and policy | GOAT's state includes install policy, available and downloaded versions, release notes, download percent and retry information (`apps/desktop/src/updates/updateMachine.ts:18`, GOAT revision above). Its default downloads and restarts when local servers are idle **or on quit**; alternatives are install on quit and paused, with manual update still available (`docs/user/updating.md:120`, `:128`). WSL servers that cannot report idleness defer idle restart until quit or manual installation (`:136`). The coordinator disables electron-updater's implicit auto-download and install-on-quit and controls the sequence itself (`apps/desktop/src/updates/DesktopUpdates.ts:1398`); the paused policy blocks automatic downloads (`:620`). | Model the full state and user-controlled sequence without confusing library flags with the actual product policy. |
+| Linked machines | Home fan-out sync selects and version-checks the Child executable before invoking sync (`structure/remote-link.md:9`, `src/link/ssh-argv.ts:281`); this is configuration sync, not evidence of binary update capability. OpenCodex already derives link compatibility from authenticated relay version and peer features (`src/link/compatibility.ts:71`, `src/remote/features.ts:44`). GOAT describes exact-build updates, idle gating and protocol-range compatibility (`docs/internals/remote-updates.md:19`, `:100`, GOAT revision above). GOAT's runtime supply documents HTTPS plus `SHA256SUMS`, without detached signatures (`docs/internals/remote-updates.md:163`, GOAT revision above). | Extend the existing compatibility and authorization owners for update capabilities rather than duplicating them; prove signed provenance and safe remote execution separately from sync. The user's signature requirement is stricter than the referenced runtime supply and remains mandatory. |
+| Fan-out and orchestrator contracts | [PR #8100](https://github.com/lidge-jun/opencodex/pull/8100) merged to `dev` as `4948c04d9ced0cae90779ce03196088cc97aa906`. Current source advertises `sync.home-fanout/1` and `update.orchestrated/1` (`src/cli/capability-contracts.ts:1`). Fan-out waits for a current Home catalog, skips client-initiated/disconnected links, bounds concurrency to three and records per-Child results (`src/cli/sync-fanout.ts:99`). | Preserve both advertised contracts and avoid converting catalog sync into implicit binary installation; add a separately versioned update operation. |
+| Server rollout and recovery | GOAT's server auto-update is on by default and separately disableable; stable/nightly remain on their channel and preview/dev/PR do not auto-update (`structure/decisions/ADR-0019-update-compatibility.md:50`, GOAT revision above). Stable waits 48 hours then rolls out over 72 hours, nightly rolls out over 12 hours without soak, and missing publication time is ineligible (`apps/server/src/cloud/autoUpdatePolicy.ts:79`). Busy activation retries every 10 minutes; a failed target is suppressed (`structure/decisions/ADR-0019-update-compatibility.md:57`, `:64`). A launcher trial needs exact-version and protocol health within 30 seconds, followed by 10 minutes' probation and owner-checked snapshot rollback (`:69`). | Assess each guarantee against OpenCodex's actual installation and service model; checksums alone must not be presented as publisher signature verification. |
+| Isolated mock feed | GOAT's mock server serves files from a configured real root on localhost (`scripts/mock-update-server.ts:19`, `:111`, `:144`, GOAT revision above); its tests reject traversal and escaping symlinks (`scripts/mock-update-server.test.ts:23`, `:50`). Its release smoke uses a temporary root and validates merged manifests (`scripts/release-smoke.ts:216`, `:298`, GOAT revision above). | Build equivalent isolated fixtures for Tauri and package formats with test-only keys, bounded responses, failure injection and no access to real homes. GOAT's preview manifest smoke fixture is not evidence of a published preview feed. |
+| Telemetry | GOAT updater code emits internal logs and spans (`apps/desktop/src/updates/DesktopUpdates.ts:485`, `:1232`, GOAT revision above). GOAT documents product usage and relay telemetry and an opt-out (`docs/user/telemetry.md:3`, `:23`, GOAT revision above); its desktop observability layer can export logs/traces to configured OTLP endpoints (`apps/desktop/src/app/DesktopObservability.ts:778`, `:803`, `:826`, GOAT revision above). This does not establish a dedicated updater analytics event. | Do not describe GOAT as categorically telemetry-free. The OpenCodex implementation must introduce no update telemetry; verify network destinations and privacy scan. |
+
+The coordinator described the desktop sidebar's existing footer and requested
+states: current, available, downloading percentage, ready to restart and failed;
+the panel should provide channel selection, automatic download and install
+controls, release notes, restart and deferral. No screenshot is present on this
+host. Defaults, supported channels, exact source changes and verification
+remain decisions for the audited roadmap, not claims of implemented behavior.
+
+Baseline verifier discovery on 2026-11-07, before implementation: after
+`bun install --frozen-lockfile` at the repository root and in `gui/`,
+`bun test tests/update/update-desktop-badge.test.ts tests/update/update-badge.test.ts`
+passed 17 tests. From `gui/`,
+`bun test tests/sidebar-version-layout.test.ts tests/sidebar-rows.test.ts`
+passed 12 tests. The first pair exercises badge source modules; the second pair
+checks sidebar source and header styling, not rendered footer geometry. The
+initial runs failed because the checkout had no installed dependencies; the
+GUI test paths also need to be run from `gui/` or explicitly prefixed with `./`.
+These baseline results do not prove any future change.
+
+Further baseline verifier discovery on 2026-11-07:
+`bun test tests/clients/desktop-update-surface.test.ts tests/ci-workflows/release-desktop-scripts.test.ts`
+passed 48 tests, including the existing desktop-page harness and release
+signature verifier. `bun test tests/cli/sync-fanout.test.ts
+tests/clients/link-peer-compatibility.test.ts` passed 22 tests, exercising
+the existing fan-out classifier and link compatibility implementation. These
+tests directly read or import their existing owners; they do not prove any
+future signed package or remote-update design.
+
+`bun run structure:check` also passed at the baseline (exit 0). Its script
+checks the `structure/` manifest, source ownership and invariant bindings
+(`scripts/structure-ssot.ts:299`, `:518`, `:573`); it does not validate this
+`devlog/` roadmap's correctness. Roadmap review is a separate human/independent
+audit, and new structure files must be staged before rerunning that gate.
+`bun run typecheck` passed at the baseline (exit 0; 2,326 files, no type
+errors). This checks TypeScript, not Rust, rendered UI or this prose roadmap.
+`bun run privacy:scan` passed after staging the draft unit, because that scanner
+enumerates tracked/indexed files (`scripts/privacy-scan.ts:64`). Re-run it on
+the final staged roadmap before publication.
+`bun test tests/lab/core-lab-boundary.test.ts` passed 25 tests (exit 0); it
+checks protected core import graphs and the synchronous Lab activation window.
+`bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts` passed
+29 tests (exit 0), including the repository membership oracle and expected
+fixture comparison. It checks test placement and map consistency, not whether
+the planned regression tests are adequate.
+`bun scripts/file-size-ratchet.ts` passed (exit 0) on the baseline; it reads
+the repository baseline and current file sizes without updating the caps
+(`scripts/file-size-ratchet.ts:212`, `:224`).
+The ratchet's general threshold is 2,000 lines and Rust is not among its
+scanned extensions (`scripts/file-size-ratchet.ts:4`). At this baseline
+`src/update/job.ts` has 1,986 lines and no per-file baseline entry, leaving
+only 13 additional lines before it reaches the rejected threshold. Prefer a
+sibling module for substantive additions; recheck current counts after each
+prerequisite lands. A missing baseline entry does not mean a tracked file is
+untracked by Git.
+
+The bundled desktop page is `desktop/ui/update.html`; its existing status,
+check and install commands are guarded to that page in
+`desktop/src-tauri/src/lib.rs:272` and `desktop/src-tauri/src/window.rs:115` at
+the baseline above.
+`tests/clients/desktop-update-surface.test.ts:38` exercises the page and its
+command boundary. The dashboard receives desktop state via
+`src/server/management/sidebar-routes.ts:137`, whose store strictly validates
+the snapshot field set (`src/update/desktop-badge.ts:31`); any additional state
+must be designed end to end, including older shells and stale sessions.
diff --git a/devlog/_plan/261107_auto_update_parity/002_gap_ledger.md b/devlog/_plan/261107_auto_update_parity/002_gap_ledger.md
new file mode 100644
index 00000000000..121d0a07a97
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/002_gap_ledger.md
@@ -0,0 +1,29 @@
+# GOAT versus OpenCodex update gap ledger
+
+Evidence and source revisions are in [001_inventory.md](001_inventory.md).
+Every row is **open** until its named unit ships and its acceptance evidence is
+recorded. A baseline check or draft plan does not close a gap.
+
+| Requested capability | Existing OpenCodex surface | Gap owner and closure evidence |
+| --- | --- | --- |
+| Background discovery and cadence | Package cache scheduler and Tauri six-hour checks | `030`, `040`: policy-controlled, bounded checks and failure/backoff tests; user can disable them. |
+| Stable, preview and nightly selection | npm latest/preview; Tauri latest feed | `010`, `030`, `040`, `050`: signed channel metadata, actual publication routing, no implicit channel changes, unavailable-feed state. Feed publication itself is outside this task and must be reported if outstanding. |
+| Signed download and verification | Pinned Tauri updater verifies bytes; package path checks registry SRI | `010`, `030`, `040`, `070`, `090`, `100`: exact signed artifact and complete executable closure, offline positive/negative verification and owner-bound stage for each owner. |
+| Download progress and staged readiness | Tauri download currently occurs inside installation | `030`, `050`: distinguish progress, verified ready and unknown; retain or re-establish stage with safe recovery. |
+| No-interruption automatic activation | Terminal drain can abort turns at its deadline | `020`, `030`, `040`, `060`, `070`, `080`, `090`, `100`: fenced idle admission, owner-bound stop, race and long-stream tests, and safe deferral for each owner and link direction. |
+| Restart, Later and user controls | Explicit desktop install and package dialog | `030`, `040`, `050`: persisted independent check/download/install controls, defined default, explicit install-on-quit option, Later and restart behavior across quit/restart. |
+| Sidebar state and release notes | Existing availability dot and update page | `050`: current, available, downloading percentage, ready and failed state; safe localized panel, release notes and keyboard/visual proof. |
+| Sidebar version readability | Footer version currently ellipsizes | `050`: stable and long version geometry and accessible full value at relevant viewport/zoom sizes. |
+| Failure, rollback and health | npm transaction rollback and desktop failed-install recovery | `030`, `040`, `060`, `070`, `090`, `100`: signed recovery material, durable journal, target health, probation, failed-target suppression, recovery and uncertainty tests for each owner. |
+| Linked-machine update and sync | Home fan-out sync, peer features and compatibility | `060`, `080`: separate authenticated update contract for both link directions, independent Child updates, destination policy and Desktop owner bridge, target compatibility, durable fleet resumption, per-Child results and post-update sync; sync alone does not close this gap. |
+| Standalone installation updates | Standalone release artifacts exist; current orchestrated plan refuses unsupported ownership | `070`: signed platform artifact, owner-bound replacement, guarded stop, rollback and health proof for every supported target. |
+| Telemetry-free update behavior | Existing update checks and local logging | All units: no new update telemetry, bounded local status and passing privacy scan. GOAT's broader telemetry is documented in `001`; do not misstate it. |
+| Mock and isolated testing | Existing isolated update fixtures and GOAT mock reference | `010` and each implementation unit: test keys, local mock feed, isolated homes and no real service or installation changes. |
+| Security, docs and delivery | Existing ownership, release and test gates | Each unit: independent security PASS where required, docs/locale/structure sync, Lab boundary, file-size/test-layout guards, exact-head CI and a `dev` PR reported to the coordinator without merging. |
+
+Open design blockers: signed trust-root provisioning and rotation, exact
+manager-specific signed staging and rollback, complete activity fencing and
+stop-to-installer handoff, remote operation authorization and future-version
+compatibility metadata, nightly publication routing, cross-platform recovery,
+and exact field-chain/test mapping. Their resolution and independent audit are
+required before the roadmap can be locked.
diff --git a/devlog/_plan/261107_auto_update_parity/003_contracts.md b/devlog/_plan/261107_auto_update_parity/003_contracts.md
new file mode 100644
index 00000000000..6e290a10b28
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/003_contracts.md
@@ -0,0 +1,161 @@
+# Proposed update contracts — draft
+
+These names bind the roadmap drafts to one vocabulary; none is implemented or
+approved yet. Security review must resolve exact encoding, authorization and
+rotation before a unit is executable. Existing `ocx.update.plan/1`,
+`ocx.update.result/1`, `ocx.sync.fanout/1` and desktop v1 snapshots remain
+compatible contracts; do not add fields to their strict parsers in place.
+
+| Contract and fields | Creation → serialization → parsing → consumers |
+| --- | --- |
+| `ocx.update.root/1`: `schema`, `revision`, `expiresAt`, `rootKeys`, `rootThreshold`, `releaseKeys`, `releaseThreshold`, `channels` | A planned trust-root record would define public verification material and rotation; the design remains subject to independent security review. |
+| `ocx.update.release/1`: `version`, `channel`, `sequence`, `sourceCommit`, `rolloutNotBefore`, `targets` | The proposed release builder would create and sign metadata (`010`); the planned `signed-supply.mjs` would verify the signature before strict parsing, and policy, package and remote selection would consume the verified candidate (`040`, `060`). Native Desktop would select its target and delegated artifact key from that record and separately verify the artifact through Tauri (`030`). Unknown schemas or signatures would be refused. |
+| `ocx.update.tree/1`: `version`, `target`, `entries` with path/type/mode/size/digest and internal-link target | After package preparation (`010`), the proposed signed release record would bind inventory bytes and archive digest. A strict extractor and planned `signed-tree.mjs` would validate the complete set before execution; the package adapter and recovery would consume the verified stage (`040`). No transport should introduce extra tree entries. |
+| `ocx.update.channel-freshness/1`: `channel`, `freshnessSeq`, `releaseSeq`, `releaseDigest`, `issuedAt`, `expiresAt`, `rootRevision` | Renewable signed channel freshness is proposed; a compatible publisher and discovery path have not been demonstrated. Installation owners would validate expiry, selected release and per-channel high-water state as specified in `010`, subject to its open publication and validity blockers. |
+| `ocx.link-capabilities/1`: exact version, channel, target and artifact digest; provided and required protocols; update actions; installer, recovery and journal-reader versions | A planned release descriptor would bind compatibility information to its candidate for fleet comparisons. |
+| `ocx.update.policy/1`: `revision`, `channel`, `check`, `download`, `install` | Proposed native and package policy owners would persist and validate policy records, with corrupt or unreadable state disabling automatic effects. |
+| `ocx.update.prepared/1`: `operationId`, candidate identity, stage identity/digest, installation owner, policy revision | Installer owners would create a verified, owner-bound prepared state and expose only status to the UI. |
+| `ocx.update.activation/1`: operation ID/generation, runtime instance, endpoint, installation owner, candidate/stage digest, policy revision, deadline | A planned local coordinator would bind admission to the exact operation and owner before activation. |
+| `ocx.update.stop/1`: operation identity, disposition, runtime-exit and manager observations, teardown state | A proposed stop owner would provide an observed, operation-bound result before a verified swap. |
+| `ocx.desktop.update/2`: current version, candidate identity/channel, phase, bounded progress/error, policy revision and values, release-note text, deferred state | The planned native panel status would represent the candidate, stage readiness, check errors, policy state and safe release notes independently. |
+| Desktop snapshot v2: `schema`, `sessionId`, `sequence`, `currentVersion`, `latestVersion`, `available`, `checkedAtMs`, `phase`, `progressPercent`, `errorCode`, `installedChannel`, `policyChannel`, `candidateChannel` | A proposed versioned publisher and decoder would provide a bounded display-only snapshot with explicit unknown values and legacy negotiation. |
+| `ocx.desktop.badge/2`: `schema`, `sessionId`, `currentVersion`, `latestVersion`, `updateAvailable`, `unknown`, `phase`, `progressPercent`, `errorCode`, `installedChannel`, `policyChannel`, `candidateChannel` | A planned versioned desktop badge would project nullable channel and availability observations, while the old badge retains its existing compatibility behavior. |
+| `ocx.link-update.envelope/1`, `ocx.link-update.request/1`, `ocx.link-update.result/1`: operation identity, link, candidate, destination policy revision, expiry and action | A proposed destination-authorized update exchange would carry bounded operations and results independently of sync. |
+| `ocx.link-update.grant/1`: grant identity/epoch, Home public key identity, Child/configuration/installation and link binding, allowed actions and expiry | A planned destination-local grant would restrict authorized update operations and be revocable. |
+| `ocx.link-update.dashboard-status/1`, `ocx.link-update.dashboard-action/1`, `ocx.link-update.dashboard-action-readback/1` | Proposed management endpoints would expose bounded status and accept revision-bound actions without transferring destination authority. |
+
+The new field names above must be reflected verbatim in each unit's final
+file map and tests. The proposed per-platform signed tree is a publisher
+attestation of final build output; it is not a claim of reproducible builds.
+Avoid sensitive data in all status records and logs. Capability absence or an
+unknown schema produces an explicit unavailable/unknown result, never a
+fallback to an unverified install.
+
+## Exact release-candidate binding
+
+`ocx.update.release/1` is a closed object containing `schema`, `version`,
+`channel`, positive safe-integer `sequence`, full source commit,
+`rolloutNotBefore`, `receipt`, `releaseNotes` and a bounded nonempty `targets`
+array. `receipt` binds a SHA-256 and size. Each unique target has an exact
+platform/architecture/owner-compatible `id`, `artifactKind`, `artifact`
+(`sha256`, `size`, `signatureBox`, `signerFingerprint`), `tree` (null for
+native bundle formats or signed inventory digest/size), `capabilities`
+(descriptor digest/size), and approved source/asset identity. Reject duplicate
+target IDs, malformed hashes, missing fields and unknown enums. A consumer
+never accepts an arbitrary filesystem path or URL from a caller as install
+authority. `010` derives the approved release URL and `030` enforces its
+production transport policy.
+
+The release builder first verifies the exact source, four version files and
+artifact set and emits a bounded `ocx.update.verification/1` receipt containing
+source commit, version, channel, workflow run/attempt, version-source digests
+and ordered asset digests/sizes. The receipt does not contain the later release
+record or its own digest. After verification, the signed release record binds
+the exact receipt bytes and artifacts, avoiding a circular digest. Target
+selection returns a candidate ID derived from a domain separator, the exact
+release-record SHA-256 and the length-prefixed target ID. It is a lookup value,
+not a replacement for an owner-held prepared handle or consent.
+
+`010` owns the shared strict parser and golden vectors; `030`, `040`, `060`
+and `070` create their owner-specific prepared state only from the verified
+candidate. Tests must reject changed receipt/target/signature/bytes, wrong
+channel or owner, ambiguous targets, same-sequence substitutions and an
+unverified release listing. The selected candidate, policy revision and actual
+owner are rechecked before every side effect. Old strict output schemas remain
+separately decoded.
+
+## Publisher payload and manager output
+
+The publisher-signed tree inventory is an exact closed set for publisher
+payload: every listed file/link/type/mode/digest must match and no unlisted
+publisher payload is accepted. Package-manager output is a separate local
+owner record, never relabeled publisher-signed. Each supported manager/version
+adapter has a closed allowlist of generated metadata and bin/link paths, parsed
+schemas and canonical wrapper templates; bind every target and path to the
+selected installation, global/bin/cache/store identity and verified payload.
+Reject unknown generated entries, executable text that differs from an
+adapter-owned exact template, links escaping allowed roots, unexpected paths,
+unrecognized manager versions/layouts and missing or conflicting ownership.
+Do not use a locally generated checksum as publisher authentication.
+
+The adapter journals the exact validated generated-output digest and owner
+identity for readback and recovery; that digest detects drift but does not by
+itself authenticate the manager. Manager execution is permitted only through
+the selected trusted installed manager and under installation-scoped exclusion;
+verify the signed consumed inputs and installed payload again before any new
+candidate executable runs. A nonparticipating or unknown writer remains an
+explicit unresolved boundary under `020`, not an assumed lock guarantee.
+Per-manager tests cover every allowed metadata/wrapper class, foreign entries,
+path changes, unsupported layouts and interrupted recovery. The actual supported
+version/platform allowlist is enabled only after those tests and independent
+security review pass.
+
+## Signed-root draft encoding
+
+The root is a strict JSON object with `schema: "ocx.update.root/1"`, positive
+integer `revision`, RFC3339 `expiresAt`, `rootKeys` and `releaseKeys` as bounded
+arrays of `{keyId, algorithm, publicKey}`, integer thresholds, and `channels`
+as a distinct subset of `latest`, `preview`, `nightly`. `algorithm` is the
+explicit Tauri-compatible minisign prehashed format; `keyId` is the full
+public-key fingerprint, with the format's short identifier checked as well.
+Each release-key entry additionally names its nonempty `channels` and `roles`
+subsets; a target may select it only for an explicitly delegated channel and
+role. Root-key entries have no release delegation fields. Allowed release
+roles are `release`, `tree`, `desktop-artifact`, `standalone-artifact` and
+`guardian-artifact`; threshold and unique-key checks apply separately to the
+role being verified. The proposed desktop-artifact role is constrained to a
+single signer until a multi-signature bridge is designed and reviewed.
+The representation of independently configurable role thresholds remains
+unresolved: the table's singular `releaseThreshold` is a placeholder, not a
+final schema. Resolve and independently review that encoding, including
+desktop-artifact constraints, before any implementation relies on it.
+Detached signatures carry `{keyFingerprint, signatureBox}` in the envelope
+defined in `010` and authenticate the exact file bytes. Reject duplicate keys, duplicate JSON names, thresholds outside the
+unique key count, unknown fields and unsupported algorithms. Initial threshold
+is one pinned root key and one delegated release key; a feed cannot add a new
+root without the rotation rule above. The signature-envelope proposal and
+required cross-runtime test vectors are in `010`; they require independent audit.
+
+The native candidate bridge is a planned integration. Its security design and verification remain subject to independent review.
+
+Channel discovery freshness, expiry, renewal and rotation are proposed in
+`010`. Publication through an existing authorized path and independent
+security review remain prerequisites; none of these contracts is implemented.
+
+## Future link compatibility descriptor
+
+`ocx.link-capabilities/1` is a closed JSON object with `schema`, `version`,
+`channel`, `target`, `artifactSha256`, `homeProvides`, `childRequires`,
+`updateActions`, `installerAdapters`, `recoveryProtocols` and
+`fleetJournalReaders`. Each of the last six fields is a bounded array of
+unique `{id, version}` pairs: `id` is a stable ASCII protocol/adapter/action
+identifier and `version` is a positive integer. Arrays express exact supported
+versions, not ranges inferred from release strings. Empty arrays are explicit;
+missing, malformed, duplicated or unknown schema data is `unknown`. The
+signed target record binds the digest and length of the exact descriptor bytes;
+the descriptor's artifact digest and target must equal the selected target.
+This avoids a circular release-record digest. A descriptor for a retained old
+artifact must itself be publisher-authenticated and bound to that exact
+artifact; a current peer report is not a substitute.
+
+Add `compatibilityEpoch` and `compatibilityVectorSha256` to the descriptor.
+The vector is canonical encoding of the provided/required protocols, update
+actions, recovery and journal-reader versions relevant to interoperation, with
+strict ordering and duplicate rejection. Publisher release checks certify that
+every artifact declaring the same epoch and vector implements that vector; a
+changed vector requires a new epoch. The artifact-specific descriptor still
+binds version, target and digest, while the shared vector identifies the
+compatibility horizon rather than claiming different artifacts have the same
+digest. The release verifier must test this invariant against the source-owned
+feature and protocol implementations before signing.
+
+The `060` comparator first verifies both descriptors and their exact artifact
+bindings. For each required `{id, version}` in the Child's `childRequires`, the
+Home's `homeProvides` must contain the identical pair. The selected update
+action, destination installer adapter, recovery protocol, and journal-reader
+version required by the specific operation must likewise be explicitly present
+in the relevant descriptor. A valid descriptor with a missing required pair is
+`incompatible`; absent or unverifiable evidence is `unknown`. Neither result
+permits automatic activation. Comparator test vectors cover all Home/Child and
+recovery pairs, same release with different artifact, explicit empty arrays,
+unknown schema, duplicate entries and forged or missing descriptor binding.
diff --git a/devlog/_plan/261107_auto_update_parity/010_signed_supply.md b/devlog/_plan/261107_auto_update_parity/010_signed_supply.md
new file mode 100644
index 00000000000..32f51412cf0
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/010_signed_supply.md
@@ -0,0 +1,386 @@
+# Signed supply and channel contracts — draft unit
+
+**Status:** evidence and design options only. The exact artifact and trust
+contract requires the architect proposal, independent security review and
+roadmap audit before this becomes an executable unit.
+
+## Existing supply
+
+The release verifier expects standalone archives with SHA-256 sidecars, while
+its required updater signatures cover desktop updater targets
+(`desktop/scripts/verify-release-assets.ts:45`). The Tauri feed names exact
+desktop artifact URLs and signatures (`desktop/scripts/updater-manifest.ts:18`).
+The verifier pins the configured updater public key and verifies Ed25519
+signatures (`desktop/scripts/verify-release-assets.ts:180`). npm trusted
+publishing generates provenance (`.github/workflows/release.yml:1151`); a
+publisher-verification contract must be established for the requested update
+experience. Integrity metadata alone is not a publisher signature.
+
+## Required design decision
+
+The read-only `auto-update-signed-supply-research-01` investigation recommends
+publisher-signed, self-contained installation trees as the primary candidate.
+Existing npm provenance remains an alternative, but its entire executable
+dependency closure and offline verification are not yet established. The
+proposal is not a security review or proof that artifacts already exist.
+Prove a reproducible, offline-testable signature contract for the *exact bytes*
+used by every automatic installation, including publisher, version, platform,
+channel, key rotation and artifact-to-install binding. A release must never be
+published as part of this task.
+
+Proposed signed supply consists of a versioned trust root
+(`ocx.update.root/1`), a release record (`ocx.update.release/1`), a complete
+tree inventory (`ocx.update.tree/1`) and a directly signed tree archive for
+each supported platform. Sign exact UTF-8 metadata bytes, not a reconstructed
+serialization. The release record binds source commit, version, channel,
+sequence, target, archive digest and size, and tree-inventory digest. The tree
+inventory covers every executable and dependency, including Bun, native keyring,
+optional native modules and generated compatibility assets. Reject duplicate or
+unknown fields, unsupported algorithms, invalid paths and oversized input.
+The release builder freezes the tree only after package preparation
+(`package.json:71`).
+
+Proposed initial trust root: the existing updater public key already pinned in
+`desktop/src-tauri/tauri.conf.json:68`, using the Tauri-compatible
+Ed25519/BLAKE2b signature format and verifier precedent
+(`desktop/scripts/verify-release-assets.ts:186`). The release record and tree
+archive must each verify under a release key delegated by that pinned root;
+a feed-provided key alone is never a trust root. Root revision updates require
+authorization from both the currently trusted and replacement roots; persist
+the highest accepted revision and channel sequence. A lost or compromised root
+requires a separately authorized manual software/key recovery; do not silently
+trust a replacement from the feed. Test fixtures use separate keys only.
+
+The research lane also proposed a two-of-three offline root. The initial plan
+does not assume three existing maintainer keys or perform a key ceremony; it
+starts from the repository's existing publisher trust anchor. Security audit
+must assess that operational tradeoff, the exact signer availability and
+rotation procedure before implementation. No private signing material is
+created, requested or used during this task's local tests.
+
+## Proposed detached signature format and rotation
+
+The existing release verifier parses the Tauri public-key box as `Ed`, an
+eight-byte key ID and 32 public-key bytes, and its updater signature as `ED`,
+an eight-byte key ID and a 64-byte signature
+(`desktop/scripts/verify-release-assets.ts:155`, `:186`). `ED` verifies ordinary
+Ed25519 over the BLAKE2b-512 digest of the **exact input bytes**, followed by
+the minisign global signature over the raw signature and trusted comment
+(`:204`, `:212`). It is not Ed25519ph. Keep Tauri's base64-wrapped minisign
+signature box for artifacts; do not invent a different cryptographic meaning
+for an existing `.sig` file.
+
+For root, release and tree metadata, propose a separate closed
+`ocx.update.signatures/1` JSON envelope `{schema, signatures}`. Each entry is
+`{keyFingerprint, signatureBox}`, where `keyFingerprint` is lowercase SHA-256
+of the 32-byte Ed25519 public key and `signatureBox` is canonical standard
+base64 of the four-line Tauri minisign box. Each signature authenticates the
+exact accompanying metadata file bytes; no parse-and-reserialize step occurs
+before verification. The expected metadata schema and verification role are
+chosen by the consumer and checked after cryptographic verification; a root
+record cannot be accepted as a release record. Limit root metadata to 64 KiB,
+release metadata to 1 MiB, tree inventory to 16 MiB, signature envelope to
+64 KiB, and keys to 16 per root or release role. A root-rotation envelope may
+carry up to 32 signatures so disjoint old and new root thresholds can both be
+satisfied; other role envelopes remain limited to 16. Reject duplicate fingerprints,
+noncanonical encodings, unknown algorithms, short packet IDs that disagree with
+the full authorized key, malformed UTF-8, invalid global comments, duplicate
+JSON keys and schema/role mismatches. Initial key threshold is one, as stated
+above; threshold counting is over distinct authorized full fingerprints.
+
+The proposed embedded initial root delegates release keys per channel and role; the
+verified release record selects exactly one authorized artifact key fingerprint
+for each target. Until a multi-signature artifact bridge is designed and
+independently reviewed, the desktop-artifact threshold is exactly one; other
+thresholds retain their own role rules. The native client would verify the root and release first, select
+the target's delegated key, then passes the corresponding verified minisign
+public-key box to the updater builder's `pubkey` override. The locally installed
+`tauri-plugin-updater` 2.14.1 exposes this override in `src/updater.rs:348`;
+the signed target digest and signature still bind the exact downloaded bytes.
+The existing release verifier currently checks the configured pinned updater
+key (`desktop/scripts/verify-release-assets.ts:202`), so the supply unit must
+update it to check only an authorized delegated key and exercise both old and
+rotated configurations. Root rotation accepts a strictly higher root revision
+only when signatures by distinct keys meet both the current-root and proposed-
+root thresholds; write the validated root and high-water mark atomically before
+using newly delegated keys. Replayed or partially authorized root transitions,
+unknown channels, mismatched key IDs and an interrupted persistence step refuse.
+Include a disjoint old/new root-key vector at the maximum thresholds, plus
+desktop artifact role and rotation vectors.
+
+Desktop artifact verification and the release-verification path require independent security review and validation before implementation.
+
+## Platform and owner matrix to implement
+
+| Surface | Existing published build targets in source | Required automatic behavior |
+| --- | --- | --- |
+| Tauri desktop | macOS arm64/x64, Windows x64, Linux x64 AppImage/deb (`desktop/scripts/updater-manifest.ts:30`) | Use the signed desktop feed for the exact bundle target; a missing target/channel is unavailable. Verify actual release assets and the Linux bundle-type selection. |
+| Standalone runtime | macOS arm64/x64, Windows x64, Linux x64/arm64 (`scripts/standalone-targets.ts:7`) | A signed exact-platform artifact and an owner-specific replacement/recovery contract are required; current standalone ownership is not automatically eligible. |
+| npm global | npm-owned package roots and bin links; platform-specific dependency tree selected by installation | Provide a signed complete tree for each supported platform and prove ownership, bin compatibility, stage and rollback. Missing closure is unsupported. |
+| pnpm and Bun global | Existing owner-specific global operations, not a proved download-only stage | Implement and independently verify each manager's signed offline staging, activation, shims and rollback. Until proven, show unsupported; these rows remain open parity work. |
+| mise, source, unknown or foreign owner | Existing plan refuses them (`src/update/orchestrated-plan.ts:28`) | Keep the refusal and provide the owning tool's manual guidance. A desktop-owned bundle uses the native desktop owner, never a package-manager swap. |
+
+The release record's `targets` entries must identify platform/architecture,
+owner-compatible artifact kind, signed archive size/digest, signed tree digest
+and supported link capability metadata. They are a closed list of what the
+release actually produced, not inferred from the host version. Tests must prove
+each advertised target has a matching signed artifact and that an unlisted
+target cannot be selected.
+
+Each signed release record also binds bounded, plain-text release-note items
+to its exact version and channel. The panel may gather verified same-channel
+records newer than the installed version through the selected candidate and
+format them under the bounded grouping contract in `050`; an untrusted
+release listing locates records but supplies no authenticated notes or count.
+Missing history remains explicitly unknown and never changes artifact trust.
+
+Client preparation verifies signed metadata and archive bytes, extracts as
+data without lifecycle scripts, validates every inventory entry and rejects
+unlisted entries, then binds the owned stage and installation identity to an
+opaque prepared handle. At activation, reacquire the existing service ownership
+and sync/update guards, obtain the installation-wide coordination required by
+`020` once designed and proven, revalidate the handle and stage, and execute only the
+verified absolute paths after the guarded stop. The client must not fetch a
+replacement dependency or run a package lifecycle script after verifying a
+different archive. Publisher-signed output authenticates the publisher's
+artifact; it is not a claim that the build is reproducible from source.
+
+The user requires signatures for automatic updates, stronger than GOAT's
+documented HTTPS-plus-checksum runtime supply
+(`docs/internals/remote-updates.md:163`, GOAT reference revision). A missing,
+invalid, unknown-publisher or mismatched signature yields a visible refusal,
+never a fallback to unsigned bytes on the migrated path. Existing manual
+commands retain their current behavior until each manager's verified adapter
+and recovery contract are ready. Each later migration must preserve documented
+command interfaces while clearly reporting that missing verified supply can
+cause a previously successful installation to be refused; it must not claim an
+unimplemented manager has already migrated.
+
+Channel parity is an open deliverable, not a UI-label substitution: current
+release authority accepts `main`/`latest` and `preview`/`preview`
+(`scripts/release.ts:499`, `.github/workflows/release.yml:1051`), while GOAT
+publishes a distinct nightly feed. The final design must specify how a signed
+nightly feed would be produced under the existing workflow trigger and
+permission constraints, how preview and stable feeds are selected and
+authenticated, and how a client behaves before a feed has actually been
+published. This task may implement and test the release machinery but may not
+publish a release. An unavailable feed remains an explicit outstanding item,
+not proof of parity.
+
+Separate signed stable, preview and nightly manifests are a desired outcome,
+not a demonstrated publication route. The earlier idea of
+stamping a version into a workflow checkout after CI is rejected: current
+release publication requires successful push CI for the exact source commit
+(`.github/workflows/release.yml:1077`) and all version sources to match that
+commit (`scripts/ci/release-preflight.sh:59`). A transformed source tree has
+not received that proof and this task cannot weaken the CI policy.
+
+The existing release authority permits only `main`/`latest` and
+`preview`/`preview`. A separate nightly npm tag or feed has no proven route
+under those unchanged gates. Keep nightly unavailable until an independently
+reviewed publication design demonstrates compatibility with the current
+branch/tag policy, workflow triggers, permissions and CI requirements; do not
+change those gates to make this roadmap work. No part of this task creates a
+release commit or publishes a release.
+
+The four sources are `package.json`, `desktop/src-tauri/tauri.conf.json`,
+`desktop/src-tauri/Cargo.toml` and the `opencodex-desktop` entry in
+`desktop/src-tauri/Cargo.lock` (`scripts/release-version-sources.ts:153`).
+Keep the expected-SHA/trusted-dispatch gate and exact commit CI proof. In every
+packaging and publishing job, checkout that SHA and verify the four sources
+against the selected version before generating assets; no workflow-side version
+stamp is allowed. A signed release record binds channel, version, source SHA,
+the verification-receipt digest and each target artifact digest. The receipt
+also records the release workflow run/attempt and the exact four source-file
+digests; verification checks the source bytes against the checkout and receipt
+before signing. Release-generated packaging output is separately identified and
+verified, not represented as a tested source commit.
+
+Preserve both early and final branch/tag/version gates, release argument parsing,
+cross-channel global-floor checks, prerelease classification and resume logic
+(`scripts/release.ts:499`, `scripts/ci/release-preflight.sh:34`,
+`.github/workflows/release.yml:1051`, `:1371`). Resume remains bound to the same
+SHA and version under existing publication-side identity checks. Any future
+compatible channel design must prove that existing tags and exact-SHA CI remain
+protected and that changed or absent receipts fail closed. npm trusted-publisher
+configuration and the initial signed feed remain operational prerequisites; no
+feed is claimed live until separately published by a maintainer.
+
+If a compatible nightly route can be established, version spelling alone must
+not be used to infer its channel. Every supported installed owner stores an
+owner-private authenticated provenance record binding installation identity,
+exact artifact digest, signed release-record digest and channel. The installer
+writes it only after verification and preserves it across startup and recovery;
+the new code verifies the release record and actual owner before trusting it.
+For a legacy installation without a provenance record, a stable version can
+bootstrap as `latest` only with matching authenticated evidence; an ambiguous
+prerelease is `channel-unknown` until the owner explicitly selects a channel
+and a verified release establishes provenance. Corrupt, missing or conflicting
+records never fall back from nightly to preview. A channel switch is an explicit
+policy change and invalidates prior candidate authority.
+
+Update the complete channel field chain: Node launcher `bin/ocx.mjs:129`,
+`src/update/index.ts:78`, `:229`, `src/update/refresh-scheduler.ts:58`,
+`src/update/notify.ts`, `src/update/job.ts:226`,
+`src/update/orchestrated-arguments.mjs`, `src/update/orchestrated-entry.ts`,
+`src/update/desktop-badge.ts:87`, management routes in
+`src/server/management/config-routes.ts:948`, and the browser owners named in
+`050`. New three-channel check/apply requests use separately versioned routes
+and schemas; preserve existing two-channel wire shapes. A new nightly owner
+refuses legacy check/apply requests that would infer preview, returning an
+actionable upgrade-client state. A new server receiving an old Desktop snapshot
+without authenticated channel projects unknown provenance in the new DTO.
+For the legacy endpoint it preserves the existing availability projection and
+compatibility channel label as specified in `050`; that label is not provenance
+or installation authority.
+Do not send a nightly artifact to an old updater that cannot preserve its
+channel. Test first install, lost/corrupt policy, old/new clients and rollback,
+and prove no inferred cross-channel installation occurs.
+
+For discovery, the client may inspect the GitHub release listing as an
+untrusted index to locate a channel-specific signed record at an exact release
+tag. Only a verified `ocx.update.release/1` record selects a target and
+version; listing order or a mutable dist-tag is never installation authority.
+Each immutable release attaches its own signed channel metadata and matching
+desktop feed, rather than requiring an existing published asset to be edited.
+Missing, stale, malformed, unavailable or unverified metadata leaves the
+channel unknown or unavailable. The first in-scope release infrastructure unit
+must prove exact source-version and record-to-artifact binding with fixtures;
+this routing is still a proposal until the independent security audit passes.
+
+The proposed `ocx.update.channel-freshness/1` is a closed UTF-8 JSON object
+with `schema`, `channel`, a positive safe-integer `freshnessSeq`, the selected
+`releaseSeq` and exact `releaseDigest`, `issuedAt`, `expiresAt`, and
+`rootRevision`. The existing proposed detached envelope signs its exact bytes
+using a release-role key delegated for that channel by the currently accepted
+root. Renewal increments `freshnessSeq` even when the release is unchanged.
+The installation's update owner would persist the highest accepted freshness
+sequence and its exact digest per channel: a lower sequence or equal sequence
+with different bytes is refused; equal sequence and digest is a retry. The
+freshness record must match the selected verified release, be within its
+validity window and name the accepted, unexpired root; otherwise unattended
+activation defers without discarding a separately valid retained stage.
+
+The proposed publisher would publish new immutable freshness records through a
+publication path authorized under the existing workflow triggers and policy;
+any mutable locator is merely an untrusted index. Neither the publisher nor the
+path is established. No workflow trigger change is authorized. The publication
+mechanism must be demonstrated under that constraint before implementation.
+After a root rotation, the publisher would issue freshness under the newly
+accepted root; a stale-root record cannot authorize an effect. If renewal or
+rotation publication is unavailable, the channel remains unavailable rather
+than accepting expired or previously authorized metadata. Test quiet release
+periods, renewal without a release, expiry, stale sequence, digest substitution,
+root/key rotation, lost publication, concurrent checks and restart. This is a
+draft contract, pending independent security and operational review.
+
+**Open publication and validity blockers:** current release preflight refuses
+republishing an existing version, and published release assets are immutable.
+No proven renewal location or discovery path exists yet for a quiet channel
+under the current trigger and permission constraints. The draft must also set
+an independently reviewed maximum validity window, ordering and clock-skew
+rules, and show how the unchanged release remains verifiable after root/key
+rotation. Do not implement or claim freshness parity until a compatible
+publisher and fixtures establish those properties without changing workflow
+triggers or CI policy.
+
+Proposed rollout for unattended package and fleet activation: stable waits 48
+hours after a signed rollout-not-before value, then opens deterministically
+across local buckets over 72 hours; preview and nightly open over 12 hours only
+for an explicitly selected channel. The bucket is computed locally from a
+random per-installation identifier and exact target version, is never sent as
+telemetry, and is stable across retries. Missing or invalid signed time,
+unsupported channel, a downgrade and the recorded failed target are ineligible.
+Manual checks can show an ineligible release and explain its wait; explicit
+manual installation still requires signature and owner checks. GOAT's own
+stable/nightly rollout constants are documented at
+`apps/server/src/cloud/autoUpdatePolicy.ts:79` in the reference revision; this
+proposal deliberately defines preview as a separate opt-in OpenCodex channel.
+The current desktop manifest's `pub_date` is generated before publication
+(`desktop/scripts/updater-manifest.ts:69`); it is not authenticated actual
+publication time. Persist per-channel the highest verified sequence, its exact
+release digest and version, and the latest accepted wall time in private owner
+state, with an atomic write before automatic effects. A lower sequence, or the
+same sequence with a different digest, is refused; an equal sequence and digest
+is a retry of the same candidate. Root revision has its own independent
+high-water mark. Record first observation per exact candidate; unattended
+eligibility uses the later of signed `rolloutNotBefore` and local first-seen,
+plus the channel delay. If the clock is unreadable or earlier than the last
+accepted time, retain the state and defer until corrected. Test rollback,
+restart, same-sequence substitution and write failure. These local checks do
+not establish a trusted wall clock against a deliberate forward jump; document
+that limit and do not describe them as a publication timestamp proof.
+
+Only an independently established first installation may initialize absent
+root, release and freshness high-water state. If any required state is missing,
+corrupt, unreadable or its initialization is ambiguous after that point, retain
+remaining evidence and disable unattended update effects; do not treat the loss
+as a first install or reset sequence and digest marks. An explicitly authorized
+local recovery may resume only after independently establishing non-regressive
+root, release and freshness marks and persisting them atomically under the same
+owner lock. If their prior bounds cannot be established, refuse automatic recovery
+and require a separately reviewed recovery procedure. Defining and proving that
+recovery evidence and its authority is an implementation blocker. Test each lost,
+corrupt and unreadable record after initialization, stale or incomplete recovery
+evidence, failed persistence, concurrent checks and restart; prove that recovery
+never accepts a lower mark or a different digest for the same sequence.
+
+The planned recovery must also handle an accidental forward clock jump followed
+by correction: a deliberate local operator action may reset the accepted-time
+and first-observation timing baselines only after fresh validation, while
+preserving all sequence and digest high-water marks. Restart the observation
+delay from the corrected time; never make a target eligible earlier. Exercise
+forward-jump, correction, restart and attempted rollback cases in isolation.
+The installation's local update-policy owner would perform this action through
+an explicitly authorized local control, serialized under the same durable
+state lock as discovery, high-water writes and other timing resets. It must
+re-read the latest state under that lock, persist the reset atomically, and
+refuse if ownership, prior state or validation cannot be established. Tests
+must include concurrent checks and resets, stale controls, interrupted writes,
+failed persistence and restart, proving that no sequence or digest mark moves
+backward. Inside this authorized, effect-free recovery operation only, validate
+the corrected operator-confirmed clock and freshly verified metadata without
+using the suspect prior wall-time baseline as a gate. Continue enforcing all
+signature, channel, sequence, digest and root high-water checks. Re-read state
+under the lock, atomically persist the corrected timing baselines, then resume
+normal validation; no update effect is permitted before that commit. Invalid
+clock or metadata, state changes, or failed persistence leave the old state and
+automatic effects disabled. Test this exact recovery, concurrent checks and
+failed resets across restart.
+
+## Candidate file map to resolve
+
+| Operation | Path | Purpose and evidence |
+| --- | --- | --- |
+| MODIFY if needed | `.github/workflows/release.yml`, `desktop/scripts/verify-release-assets.ts`, `desktop/scripts/updater-manifest.ts` | Produce and verify the selected signed supply and channel-specific manifests from exact release inputs; preserve the existing verified-bundle handoff (`release.yml:859`) and no new triggers, broadened permissions or unpinned third-party actions. |
+| NEW | `src/update/signed-supply.mjs`, `src/update/signed-tree.mjs`, `src/update/prepared-install.mjs` and declaration siblings | Resolve signed metadata, verify bounded exact bytes and complete tree inventory, and retain an owner-held prepared handle. Final APIs, root provisioning, archive extraction and every caller remain audit blockers. |
+| NEW | `src/update/update-contracts.mjs`, `src/update/update-contracts.d.mts`, `tests/update/update-contracts.test.ts`, `tests/fixtures/update-contracts/` | Own the first implementation of closed root, release, tree, policy, prepared, activation and capability-descriptor parsers plus shared test vectors described in `003_contracts.md`. `010` lands these before `020` consumes them; downstream owners create records and enforce their own authority. Register the test in both test-layout maps. The comparator remains owned by `060`. Exact exports and byte encodings still require final audit. |
+| NEW | `scripts/build-update-tree.ts`, `desktop/scripts/sign-update-supply.ts` | Produce final self-contained per-platform trees and sign the verified bundle in an existing authorized release job; no additional workflow triggers, permission expansion or unpinned third-party actions. Bind exact SHA, toolchain and lock inputs in the signed release record. |
+| NEW | `scripts/generate-update-trust-root.ts`, `src/update/trust-root.json` and a generated native resource under `desktop/ui/` | Derive identical initial public trust-root bytes from the existing Tauri public key, with deterministic output and a `--check` mode. The source key is public; tests compare Node/Bun and native inputs byte-for-byte. Never embed private signing material. Persist subsequent verified root revisions under the respective installation's private update state, not in the replaceable package tree. |
+| NEW | `tests/helpers/mock-update-server.ts` and isolated fixture builders | Serve only test-generated signed metadata and artifacts from an isolated real root on loopback, using a test key, never the real install or production key. Mirror GOAT's bounded-root and traversal/symlink tests (`scripts/mock-update-server.ts:37`, `:144`; `scripts/mock-update-server.test.ts:23`, GOAT reference revision) and add controllable truncation, wrong-signature, timeout and progress responses. Verify a new helper is actually imported by the focused tests. |
+| MODIFY if needed | `src/update/orchestrated-plan.ts`, `src/update/orchestrated-entry.ts`, `bin/ocx.mjs` | Bind automatic plan/apply to exact verified artifact identity while retaining current runtime ownership and token checks (`orchestrated-plan.ts:21`). Avoid widening existing explicit commands by accident. |
+| MODIFY | `tests/ci-workflows/release-desktop-scripts.test.ts`, `tests/ci-workflows/release-pipeline-contract.test.ts`, focused `tests/update/` | Failing-first signature, publisher identity, tampered bytes, missing metadata, target/channel mismatch, fail-closed and release receipt tests. Register new domain test files in both layout and expected-fixture maps. |
+| MODIFY | Source owners named by `structure/INDEX.md`; public update and release docs and translated pages | Describe only shipped behavior and supported channels; do not advertise a feed without a proven publisher. |
+
+The candidate file map is not the final implementation. Enumerate every
+creation, serialization, parsing and consumer site for any new contract before
+locking the plan. Design an isolated fixture with a dedicated test key or
+test identity; never use production signing secrets. Prove that a verified
+payload is the payload actually installed, not just a separately verified copy.
+
+## Acceptance and verification to ground
+
+- A correctly signed target with the expected identity and metadata succeeds in
+ an isolated home. Wrong key or publisher, altered bytes, absent signature,
+ wrong version/platform/channel, stale metadata and failed verification all
+ stop before installation or runtime mutation.
+- Desktop stable and preview/nightly choices correspond only to signed feeds
+ the release workflow actually publishes. Unsupported choices are explicit;
+ no downgrade or cross-channel move occurs implicitly.
+- Release dry-run fixtures verify exactly the assets that would be attached;
+ receipt version and commit bind the verified set. Tests prove no workflow
+ trigger, permission or unpinned-action expansion.
+- Run the targeted release-script and update tests, `bun run typecheck`,
+ `bun run privacy:scan`, `bun run structure:check`, and exact-head CI with an
+ independent security PASS. Confirm every command's actual inputs and record
+ fresh results before locking or review readiness; no full root suite.
diff --git a/devlog/_plan/261107_auto_update_parity/020_activation_admission.md b/devlog/_plan/261107_auto_update_parity/020_activation_admission.md
new file mode 100644
index 00000000000..43f40fcd4e3
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/020_activation_admission.md
@@ -0,0 +1,27 @@
+# Automatic activation admission — draft unit
+
+**Status:** open. No automatic activation or admission guarantee is implemented
+by this plan. Architecture, platform behavior and independent security review
+must be completed before implementation.
+
+## Planned outcome
+
+Automatic activation would proceed only when the responsible runtime and
+installation owner can establish that work is idle and the selected update is
+still eligible. Busy, unavailable or uncertain state must defer activation.
+An operator's ordinary shutdown behavior remains a separate contract.
+
+## Evidence required before delivery
+
+- Define and independently review the ownership and coordination contract for
+ each supported runtime, installation owner and linked-machine direction.
+- Demonstrate safe deferral, cancellation, restart and recovery with isolated
+ test homes and controlled concurrent work; include process interruption and
+ every supported platform.
+- Preserve the core/Compatibility Lab boundary and update the relevant
+ `structure/` owners only when the behavior actually ships.
+- Run focused failing-first regression tests, test-layout and file-size guards,
+ typecheck, privacy and structure checks, and exact-head CI before delivery.
+
+Security working analysis and implementation details are kept outside the
+tracked roadmap until the corresponding work is public.
diff --git a/devlog/_plan/261107_auto_update_parity/030_desktop_engine.md b/devlog/_plan/261107_auto_update_parity/030_desktop_engine.md
new file mode 100644
index 00000000000..519c1ae0846
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/030_desktop_engine.md
@@ -0,0 +1,357 @@
+# Staged desktop update engine — draft unit
+
+**Status:** pending the signed-supply and activation-admission units, architect
+reflection and roadmap audit. This is a design draft, not an implementation
+authorization.
+
+## Objective and policy proposal
+
+The planned desktop app would check its selected published channel, download a
+signed update in the background, expose progress, and retain a verified stage
+until the person chooses to restart or an enabled policy permits installation.
+The design must retain the existing download-verification and ownership-aware
+drain boundaries (`desktop/src-tauri/src/updater.rs:388`,
+`desktop/src-tauri/src/exit.rs:546`).
+
+Proposed defaults: background checks and signed auto-download on; automatic
+restart/install off until the user enables it. GOAT defaults to restart when
+idle (`docs/user/updating.md:128` at the reference revision), but OpenCodex's
+existing drain must not be assumed to prove no request is in flight. This
+conservative default avoids a surprise restart while still staging updates;
+explicit opt-in may activate only after a tested idle/admission signal and a
+fresh guarded drain. If a reliable idle signal does not exist, the enabled
+policy must defer with an explanatory state, never silently approximate idle.
+The user can turn off automatic download, turn off automatic installation, and
+choose Later; manual actions remain possible. Finalize defaults and interaction
+with quit after architect reflection and audit.
+
+The pinned Tauri updater exposes `Updater::restore_update` for cached metadata;
+it requires signed-version binding for restored metadata, and `Update::verify`
+and `Update::install` verify cached bytes again. This was inspected in the
+locally installed `tauri-plugin-updater` 2.14.1 crate (`src/updater.rs:666`,
+`:896`, `:911`); the repository pins it at
+`desktop/src-tauri/Cargo.toml:32` and locks the registry checksum at
+`desktop/src-tauri/Cargo.lock:4285`. Therefore durable staging is a concrete
+candidate: persist bounded raw metadata plus private staged bytes, restore with
+the current configured updater, verify before showing ready, and verify again
+at install. Reject signatures lacking the signed version on restoration; do not
+serialize a caller-supplied `Update` or trust a saved ready flag. Cross-platform
+filesystem ownership, bounded sizes, atomic publication and cache recovery
+still need an exact design and negative tests.
+Before relying on restoration, verify in release fixtures that the actual
+signing tool emits the signed version required by `restore_update`, including
+Windows re-signing; otherwise keep the update available for a fresh check
+instead of representing the cached stage as ready.
+
+The native staged engine owns acquisition in a new
+`desktop/src-tauri/src/update_download.rs`. It creates the plugin `Update`
+only from the verified release record's exact version, channel, target,
+artifact digest/size, signature, approved source and delegated key. Reconstruct
+and compare these bindings before each download, restoration and installation;
+never accept caller-supplied `Update` fields as authority. Use the existing
+pinned `reqwest` and `sha2` dependencies (`desktop/src-tauri/Cargo.toml:20`,
+`:23`): read response chunks, count before extending a bounded buffer, hash as
+they arrive and abort immediately if the signed size or an implementation
+maximum is exceeded. Require exact final size and digest, then call the pinned
+plugin's `Update::verify` on those same bytes before publishing ready. On
+macOS/Linux, `Update::install` verifies them again at installation; on Windows
+the verified guardian backend below performs the mandatory final verification
+before its observed installer handoff. Reject
+partial or oversized responses and remove only this operation's private
+temporary file on cancellation/failure. A Content-Length header is advisory,
+never the bound.
+
+Production acquisition requires HTTPS and a source derived from the verified
+release record's approved source/asset identity. Initially allow only the exact
+`github.com/lidge-jun/opencodex/releases/download//` source; permit
+at most three HTTPS redirects to exact hosts `github.com`,
+`release-assets.githubusercontent.com` or `objects.githubusercontent.com`,
+without userinfo or non-default ports. Refuse an unlisted host rather than
+silently widening this set. Send no management, link or user credentials to the
+artifact service or redirect target. A loopback HTTP mock is accepted only by
+injected test transport, never a production setting. Cap signed size at 1 GiB;
+use a 10-second connect deadline, 30-second idle-read deadline and 20-minute
+total deadline, with retryable bounded error codes. Test each hop and timeout,
+missing/misleading lengths, exact-size boundary, abort cleanup and wrong target,
+digest, signature or version. This is a proposed owner and acceptance contract;
+the release-asset host set and platform implementation require audit.
+
+Proposed persisted desktop policy v1, stored by the native owner under its
+app-config directory as `update-policy-v1.json`, is a closed object with
+`schema: 1`, monotonically increasing `revision`, `channel` (`latest`, `preview`
+or `nightly`), `check` and
+`download` (`automatic` or `manual`), and `install` (`manual`, `on-quit` or
+`when-idle`).
+Proposed first-install defaults are automatic checks and downloads, manual
+install. Apply them only when an independent owner-controlled initialization
+record establishes a genuinely new installation and the installed channel has
+the verified provenance in `010`. A missing policy after initialization, or an
+ambiguous initialization state, is a recovery error and disables automatic
+effects; it is never treated as first install. Ambiguous or missing channel
+provenance likewise disables automatic effects. Malformed or unreadable records
+also disable automatic effects and report an error. Writes are serialized and
+atomic; an unsuccessful write never updates effective policy. Every action
+captures the revision and rechecks it before download and activation.
+
+The proposed owner must durably reserve a strictly increasing revision before
+publishing each policy, including a repair, under the same serialized mutation
+boundary. A separate owner-controlled high-water record must survive a corrupt
+policy and restart; interrupted writes may consume a revision but never reuse
+one. If that record is missing, unreadable, corrupt or exhausted and a genuinely
+new installation cannot be established independently, refuse policy mutation
+and automatic effects rather than reset the counter. Recovery cannot assume a
+revision from an unparseable policy. Tests must cover interruption at each
+durable step, high-water loss, policy deletion after a manual choice, stale
+requests and prepared actions after recovery, concurrent writes, and restart.
+
+The planned native policy owner would also own the guarded local corrupt-policy
+repair command in `050`. It would serialize repair with policy writes, verify
+the current owner-issued observation, preserve recoverable prior data and use
+manual defaults with an explicitly selected channel and a freshly reserved
+revision above the durable high-water mark. A stale observation,
+unreadable record or failed preservation refuses without changing policy.
+Tests must cover repair, competing writes and owner restart before delivery.
+
+Later records the exact staged candidate as deferred until the person explicitly
+installs it or selects a newer candidate; it survives app restart and ordinary
+quit. Ordinary quit must not silently install a deferred or manual candidate.
+Enabling `when-idle` permits a non-deferred stage only after the admission unit
+proves idle and performs the guarded handoff, including an eligible quit.
+`on-quit` attempts installation only on an explicit quit. For either policy,
+busy or unknown activity defers the update instead of forcing installation;
+the ordinary requested quit follows its existing separate semantics. Later
+prevents the candidate's automatic installation on quit as well as while idle.
+A successful explicit install or a newer candidate clears the previous
+deferral; failed targets remain suppressed for automatic retries until a
+different candidate or explicit retry. These are proposed contracts for
+review, not evidence of implementation.
+
+Discovery and staging are separate state owners. If a later check fails,
+returns no update, or returns the same candidate with no notes, retain a still
+verified staged candidate, its ready/install action, Later decision and notes;
+surface the check error separately and allow retry. Invalidate the stage when
+channel or owner changes, a newly selected candidate, expired or failed
+trust/identity verification, or an explicit discard requires it. A policy
+revision change invalidates prior activation authority and requires revalidation;
+it does not by itself erase a still-valid stage or Later. An invalid stage never
+remains installable. Throttle native progress projection
+to changed integer percentages or a one-second heartbeat; sidebar polling is a
+separate display concern (`050`). Test the check-versus-ready races, stale
+completion, deferred stage and retained notes.
+
+## Proposed durable recovery contract
+
+Before the first irreversible stop, atomically journal the candidate, prior
+version, install owner, policy revision, operation ID and the pre-stop
+Stop/Resume intent. States are `prepared`, `activating`, `observing`,
+`installed-stopped`, `healthy`, `failed` and `uncertain`; a process restart never turns an
+`activating` record into success without target-version and runtime-owner
+readback. A failed target is suppressed until an explicit retry or a distinct
+candidate. Record only bounded reason codes, no request data or credentials.
+
+The candidate activation unit must establish a verified recovery path before
+enabling unattended installation: retain a separately verified, signed prior
+release artifact and the state needed to restore it, or refuse automatic
+activation with a visible manual recovery action. If the recorded effective
+intent requires a running runtime, a successful installer exit starts
+observation; the exact target and owned runtime must answer health within a
+bounded 30-second initial window, followed by a ten-minute probation for
+runtime failure. If the person explicitly quit or the runtime was intentionally
+stopped, verify the installed artifact and record `installed-stopped`; do not
+start a runtime just to satisfy health. On the person's next deliberate start,
+observe the exact target and start the health/probation window; retain signed
+recovery material until it completes. `installed-stopped` is not `healthy`.
+A failed trial/probation triggers the verified owner-led
+recovery path; it cannot discard state written by another owner. If safe data
+restoration cannot be proven, leave the outcome uncertain and require explicit
+recovery rather than presenting a rollback as successful. Desktop platform
+installer constraints, the actual persistent journal location and state
+ownership need platform-specific design and test evidence before plan lock.
+
+Proposed recovery actor: a verified, destination-owned guardian is started from
+an isolated signed recovery payload **before** the native installer handoff.
+The guardian and its journal live outside the replaceable app bundle and hold an
+operation-specific lease. It observes installer termination and the exact
+replacement's startup/health even if the old app exits. If initial health or
+probation fails, it invokes only the verified retained recovery artifact through
+the appropriate platform owner; it records `uncertain` when termination, owner
+intent or state-write ownership cannot be established. A missing authenticated
+previous artifact prevents unattended activation; an explicit manual path
+shows the owner action rather than claiming automatic rollback.
+
+On Windows, the guardian must survive the MSI handoff that ends the old app
+(`desktop/src-tauri/src/updater.rs:397`), and wait for the installer process
+and owned replacement. On macOS and Linux it survives the app relaunch and
+observes the same journal. Rollback may select only the exact retained prior
+signed artifact named by the operation record; normal channel sequence and
+downgrade restrictions remain in force for ordinary selection. Guarded recovery
+is a separate bounded exception, not permission to install an arbitrary older
+version. The proposed owners are a new
+`desktop/src-tauri/src/update_recovery.rs` coordinator and the separately signed
+`desktop/update-guardian/` helper described below; packaging and platform
+supervision still require proof before automatic activation.
+
+The pinned updater's Windows handoff is a specific design constraint: in the
+locally installed `tauri-plugin-updater` 2.14.1 source,
+`src/updater.rs:1013–1050`, it calls `ShellExecuteW` and exits the app without
+returning an installer process handle. Merely watching the old app exit cannot
+prove that MSI/NSIS finished or that it is safe to start rollback. The
+guardian must obtain a trustworthy installer completion observation through a
+platform-specific, independently reviewed handoff; if that cannot be proved,
+it records `uncertain` and never launches a competing installer. The unit must
+choose and test the actual mechanism, including elevation cancellation and a
+live installer after guardian death. Do not claim the current plugin exposes a
+handle that it does not expose.
+
+Build the guardian as a separately signed platform artifact and bind its exact
+digest in the release record from `010`, rather than assuming a helper inside
+the app bundle will survive replacement. The native owner verifies it before
+copying it into an account-private update-operation directory outside the
+bundle, verifies the copied bytes again, and launches it with a bounded
+operation descriptor and no secret command-line arguments. The guardian owns a
+durable journal and exclusive operation lease; the replacement app's startup
+and any later owner retry reconcile that journal before starting another
+installer. A process surviving application exit is not proof of survival
+across logout or reboot: define a startup reconciliation path and leave an
+interrupted operation `uncertain` until it observes the prior worker/installer
+and owner state. No persistent service registration is implied by this plan.
+
+| Platform/owner | Proposed recovery boundary | Evidence required before enabling |
+| --- | --- | --- |
+| macOS bundle | A verified guardian outside the `.app` observes the owned bundle replacement and version-bound startup, then applies only the retained verified prior artifact through the approved platform owner. | Signed/notarized helper execution from private storage, app replacement and restoration on the same and different volumes, permission/elevation refusal, guardian loss, Stop/Quit intent and data ownership. |
+| Windows MSI/NSIS | A verified guardian outside the installed tree observes a bound installer operation and replacement process; rollback waits for confirmed installer termination. | Installer process/operation identification across elevation and app exit, job/session survival, cancellation, reboots, concurrent installer exclusion and exact-version readback. `ShellExecuteW` success alone is insufficient. |
+| Linux AppImage | A verified guardian outside the image observes the owner-bound replacement and can restore the signed prior image. | Same-filesystem replacement/restore, permissions, interrupted write/rename and exact image verification; the pinned updater's internal temporary backup is not a durable probation guarantee. |
+| Linux deb | The guardian uses the selected package owner and waits for verified package-manager completion; it never rewrites a system-owned tree directly. | Ownership and privilege checks, cancellation, manager-lock handling, signed previous package restoration and post-install health. If unavailable, report owner action and keep this parity item open. |
+
+The exit coordinator adds a distinct `quit-for-update` transition before a
+normal UserQuit claims the drain. An eligible explicit Quit atomically records
+the user's quit intent and claims this transition; an already claimed UserQuit,
+newer Stop/Quit, stale stage or busy/unknown activity follows ordinary quit and
+skips update activation. A runtime that was already stopped uses a separate
+owner-bound absence proof and installation-wide participant fence; it does not
+call the live `/api/update/activation` endpoint. An unknown or foreign runtime
+still refuses. Tests cover already stopped, absent, running, busy, concurrent
+Quit/Stop and quit ownership claimed before update admission.
+
+Selected implementation direction: add a small independently signed Rust
+guardian artifact per supported target under a new `desktop/update-guardian/`
+crate, built and verified in the existing release workflow without adding a
+trigger or permission. It reads a private versioned operation journal, acquires
+the installation-scoped owner lease from `020`, verifies its own approved
+operation identity and both signed target/recovery payloads, then records every
+handoff transition before performing it. Its stable private operation directory
+is under the Desktop owner's app-config directory, outside the replaceable
+bundle. Startup reconciliation launches only the recorded verified guardian;
+it never starts a second guardian while the original lease or installer status
+is live or unknown. No automatic installation is enabled until packaged tests
+show this helper survives the relevant application exit and preserves owner
+intent on that platform.
+
+For Windows, the selected direction is a guardian-owned MSI/NSIS launcher using
+`ShellExecuteExW` with a requested process handle, after the native plugin's
+`Update::verify` and a second guardian-side verification of the exact staged
+installer bytes. The helper accepts only the signed target's approved installer
+kind and fixed arguments; it must not accept a caller-supplied command. Observe
+the returned handle, installer exit and platform installation state before any
+health check or rollback. If elevation is cancelled before launch, restore the
+prior intent; if launch succeeds but no trustworthy handle or completion state
+can be established, retain the operation fence as `uncertain` and never start
+another installer. Test elevated/unelevated execution and Windows Installer's
+actual completion semantics. Because the current plugin's `Update::install`
+does not return the Windows installer handle, this backend replaces that launch
+only after preserving its mandatory exact-byte signature/version verification;
+it does not disable or weaken verification. Any direct OS binding is pinned,
+reviewed and covered by native CI.
+
+The durable Windows milestones are `guardian-ready`, `app-exit-requested`,
+`app-exited`, `installer-started`, `installer-exited`, `installed`, and either
+`observing` or `installed-stopped`. The guardian proves its own readiness and
+ownership before the app requests a coordinated exit; it waits for the exact
+old app process to exit before launching the installer. It records and observes
+the installer handle and completion before starting a replacement app, and
+never calls the old immediate `complete_restart` while an installer is live.
+If old-app exit fails, no installer starts; if UAC is cancelled before launch,
+restore the prior intent. Lost replies, guardian death, handle absence or an
+unconfirmed installer retain `uncertain` and prevent another installer. A
+replacement starts only when the latest recorded intent requests it; a user
+quit remains quit. Exercise every milestone with isolated process/installer
+fixtures and packaged Windows proof before enabling the capability.
+
+On macOS and Linux AppImage, retain the pinned plugin's verified `install` path
+and have the independently running guardian observe its bound completion,
+exact bundle/image identity and health; the guardian restores only the signed
+previous artifact when ownership and data-write checks permit it. For Linux
+deb, use a separately supervised approved package-manager operation with an
+observable process and package-owner readback; a denied privilege prompt or
+unknown process outcome remains explicit. Standalone `070` uses the same
+signed guardian protocol for its executable owner, with per-platform replace
+and restore adapters; it does not infer ownership from a path alone. Each
+platform needs failing-first process-loss, reboot/reconciliation, UAC/privilege
+and no-concurrent-installer tests before its automatic capability is enabled.
+
+Bootstrap for an older installation requires a publisher-signed previous
+artifact and authenticated metadata that bind the installed owner and exact
+version to the retained recovery material. A version string alone is not
+proof of installed bytes. If the old release has no verifiable recovery
+material, automatic activation stays disabled until an explicit owner-guided
+bootstrap establishes it; the signed-supply and release units must create this
+path rather than silently treating unsigned historical bytes as rollback proof.
+Tests inject failures before and after guardian launch, stop, installer handoff,
+replacement startup and probation; they assert the number of concurrent
+installers, the selected owner, and the observed signed recovery outcome.
+
+## Proposed file map
+
+| Operation | Path | Before → after |
+| --- | --- | --- |
+| MODIFY | `desktop/src-tauri/src/updater.rs` | Six-hour check and one combined download/install (`:388`, `:448`) → a single owner for check, signed download with progress, staged readiness and guarded install. Preserve generation, claim and tray races (`:28`, `:291`); do not create a parallel installer or introduce a second scheduler. Extract siblings if size or ownership calls for it. |
+| NEW | `desktop/src-tauri/src/update_policy.rs` | Typed, validated channel and download/install preferences; define explicit defaults, durable atomic persistence under the desktop app's own config directory, failure behavior and migration from no record. Resolve exact existing persistence convention before finalizing this path. |
+| NEW | `desktop/src-tauri/src/update_stage.rs` | Own a staged exact version and the verified package bytes/metadata returned by the pinned updater; retain verification provenance and invalidate on channel or version change. Persist only if the plugin supports safe re-verification across restart; otherwise recover by rechecking and re-downloading rather than asserting an old stage remains verified. |
+| MODIFY | `desktop/src-tauri/src/lib.rs`, `desktop/src-tauri/src/tray.rs` | Route UI/tray operations through the same engine and installation claim. New privileged commands remain guarded by the bundled update page; no new capability is granted to the dashboard origin. |
+| MODIFY | `desktop/src-tauri/src/exit.rs` only if necessary | Preserve `prepare_restart` ownership, drain and stop confirmation; any idle evidence must be tested and checked again at admission, not inferred from a timer. |
+| MODIFY | Desktop Rust tests and `tests/clients/desktop-update-surface.test.ts` | Add tests for state transitions, concurrent check/download/install, failures, settings and restart admission; use injected clock, updater and temporary app homes. Existing test/source registration rules apply to new domain tests. |
+| MODIFY | `structure/desktop-shell.md`, `structure/companion.md`, `structure/ops/desktop-supervision.md`, `structure/runtime.md`, `structure/ops/service-and-sidecars.md` and any other affected owner identified in `structure/INDEX.md`; user docs | Review all mapped owners (`structure/INDEX.md:182`); update each whose documented contract changes. Sync relevant translations. |
+
+## Required scenarios
+
+1. Startup and subsequent checks use one bounded scheduler. Offline, timeout,
+ stale or malformed feed shows a retryable failure or unknown, not current;
+ a delayed older check cannot overwrite a newer result or active install.
+2. A trusted test feed returns an exact signed update; progress becomes visible
+ and readiness is emitted only after the pinned plugin verifies the bytes.
+ Missing, corrupt, wrong-key, wrong-channel or superseded artifacts never
+ become installable. Test signing uses a dedicated fixture key and isolated
+ storage; never use production credentials or a real installation.
+3. Auto-download disabled, auto-install disabled and Later each prevent the
+ corresponding automatic effect. Changing a preference during a pending
+ action is rechecked at its mutation boundary. Unknown or malformed settings
+ fail safely and remain visible as an error.
+4. Automatic install requires enabled policy, current verified exact stage,
+ proven idle, the reservation handoff in `020_activation_admission.md`, a
+ fresh admission recheck and `prepare_restart` success. An
+ active request, foreign/unknown owner, failed drain, quit race or changed
+ stage defers/refuses without replacing the package. Manual install still uses
+ the same guarded drain.
+5. A failed installation invokes the existing recovery owner and reports the
+ failure; a restart returning on the old version is not called success. A
+ failed target must not enter a tight automatic retry loop. Explicit retry
+ is possible. Windows handoff and Linux package type stay covered.
+6. With `on-quit`, an idle, non-deferred verified stage is offered to the
+ guarded installer only at explicit quit. Busy or unknown activity skips the
+ update; Later, manual mode and failed-target suppression prevent it. With
+ `when-idle`, the same eligible quit path is available. Test quit races and
+ verify no in-flight request is interrupted by update activation.
+
+## Candidate verification
+
+Run failing-first focused Rust tests and the desktop update-page tests against
+isolated homes and a local mock update server; verify the real pinned plugin's
+signature rejection as well as injected state-machine failures. Run root
+`bun run typecheck`, `bun run privacy:scan` and `bun run structure:check`;
+collect exact-head PR checks and an independent security review PASS. The
+candidate `cargo test --locked --manifest-path desktop/src-tauri/Cargo.toml`
+and platform-specific test commands must be grounded and cost-checked before
+roadmap lock. No full root suite. A PR-branch `lane=all` requires an explicit
+maintainer request on that PR; run it once on the exact intended head, never
+redispatch after a head moves or rerun to green, and cancel owned superseded
+runs. Ordinary full dispatches remain batch checks on `dev` under `AGENTS.md`.
diff --git a/devlog/_plan/261107_auto_update_parity/040_package_preparation.md b/devlog/_plan/261107_auto_update_parity/040_package_preparation.md
new file mode 100644
index 00000000000..026cfff248f
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/040_package_preparation.md
@@ -0,0 +1,166 @@
+# npm package preparation and activation — draft unit
+
+**Status:** depends on signed supply and automatic admission; independent
+architect reflection and roadmap audit are pending. Do not implement yet.
+
+## Goal and current behavior
+
+Support user-controlled staged automatic updates for eligible package-owned
+installations while preserving each package manager's ownership, the
+sync/update mutex, guarded runtime stop and recovery. npm already stages beside
+the live tree and then swaps with backup and rollback, but preparation and
+activation are currently one transaction
+(`src/update/transactional-install.mjs:388`, `:410`, `:475`). pnpm operates on
+its selected global owner (`src/update/pnpm-global-install.mjs:472`); safe
+independent staging for pnpm and Bun remains to be demonstrated. Existing
+`ocx.update.plan/1` and `ocx.update.result/1` consumers retain their contracts.
+
+Automatic preparation must use independently reviewed verification and preserve manager ownership. Any manager without a proven safe preparation and recovery contract remains unsupported.
+
+pnpm's global virtual store and Bun's global metadata/shims need distinct
+adapters and isolated proof that the exact signed local bundle can be installed
+without fetching or executing unsigned dependencies, and that signed previous
+bytes and the manager's links can be restored. Do not rename pnpm store entries
+or treat `bun add -g` as preparation. They are separate in-scope units `090`
+and `100`; this npm unit cannot close them.
+
+Preparation holds only its own stage lease; it does not hold the sync/update
+mutex while waiting for a person or idle. Activation reacquires the existing
+home-scoped sync/update mutex and service ownership guard, and obtains the
+installation-wide coordination and runtime reservation proposed by `020` only
+after those mechanisms are designed and proven. It revalidates the stage and
+policy before performing the receipt-backed stop and atomic swap. The
+complete lock order and process-exit fencing must be proven against the
+selected manager and platform before the unit is audited.
+
+The existing `OCX_DISABLE_UPDATE_CHECK=1` environment opt-out
+(`src/update/refresh-scheduler.ts:35`) overrides automatic package checks and
+downloads even if the stored policy requests them. An explicit manual check
+remains available; any new update flow, including a manual install from its
+panel, requires the signed-supply verification. Existing explicit CLI behavior
+is inventoried separately and is not silently reused as an unsigned fallback. Turning the
+environment opt-out off does not clear the user's persisted policy or a
+candidate deferral; the UI explains the effective state.
+
+Persist an operation journal with exact candidate, previous version, owner,
+stage identity and `prepared` / `activating` / `observing` / `healthy` /
+`failed` / `uncertain` state. Require signed, verified recovery material before
+unattended activation. Observe the owned target's version and health within a
+30-second initial window and retain rollback material for a ten-minute
+probation; do not restore stale data over a newer owner's writes. A failed target
+is not retried automatically. A manager unable to guarantee these conditions is
+explicitly unsupported until its own implementation unit proves them.
+
+## Candidate change map
+
+| Operation | Path | Before → proposed after |
+| --- | --- | --- |
+| MODIFY | `src/update/transactional-install.mjs` and sibling modules | Extract owner-held `prepare` and `commit` around current isolated staging, tree verification, authorization, swap and rollback. A prepared handle binds verified candidate, exact installation, policy revision and stage identity. Preserve legacy command interfaces and process-tree exit fences while requiring verified supply for installation. |
+| MODIFY | `src/update/orchestrated-plan.ts`, `src/update/orchestrated-entry.ts`, `src/update/orchestrated-node-driver.mjs`, `src/update/orchestrated-session.mjs` | Add a separately versioned automatic preparation/activation contract around the existing identity-bound plan and mutation lease. Enumerate construction, transport, decoding, admission and every consumer before locking design; reject stale policy/owner/candidate. Do not change exact-key existing JSON silently. |
+| REVALIDATE | `src/update/pnpm-global-install.mjs`, Bun-owned update paths | Verify their existing command interfaces and ownership refusals remain unchanged in this unit. Their signed-supply migration and manager-specific automatic implementations belong to `090` and `100`. |
+| MODIFY | `src/update/refresh-scheduler.ts`, package update coordinator sibling | Discovery remains read-only with existing opt-out, cache and backoff; the separately persisted policy owns whether an exact signed target may be downloaded or activated. Integrate admission unit `020`, failed-target suppression and serving-target health readback. |
+| NEW | `src/update/automatic-policy.ts`, `src/server/management/update-policy-routes.ts` | The package owner stores a strict `ocx.update.policy/1` record under its effective OpenCodex configuration directory, atomically updates it with a revision check, and exposes authenticated policy reads/changes through the management owner. Only an independently established first installation uses documented defaults; missing after initialization, ambiguous, corrupt or unreadable policy disables automatic effects and requires recovery. Never use a dashboard snapshot as authorization. Wire the route through the existing registry with focused authorization tests. |
+| MODIFY | `tests/update/update-transactional.test.ts`, `tests/update/update-orchestrated-*.test.ts`, `tests/update/update-pnpm.test.ts`, `tests/update/update-sync-mutex.test.ts`, relevant CLI/GUI tests | Add red-to-green signature, stage integrity, owner, lease, process-exit, retry, policy and race cases using isolated homes and local fake installers. Register new domain test files in both test-layout maps. |
+| MODIFY | `structure/ops/service-and-sidecars.md`, `structure/ops/update-supervised-recovery.md`, `structure/ops/desktop-supervision.md`, any other mapped owners; docs-site and matching locales | Describe actual supported managers, controls, recovery and manual guidance. |
+
+Existing explicit package commands retain their current behavior until a
+manager's verified-supply migration ships. The planned migration preserves
+their argument and result interfaces, but may refuse an installation that the
+current command permits when verified supply is unavailable. Ship each
+manager's migration only after its signed adapter, recovery and compatibility
+tests pass; `090` and `100` own pnpm and Bun respectively. Document the change
+and supported manual alternatives. Until then, do not claim those managers
+have verified automatic updates or silently change their existing commands.
+
+### Package policy contract
+
+The proposed `src/update/automatic-policy.ts` would own the strict `ocx.update.policy/1` record
+at `/update-policy-v1.json`: `schema`, nonnegative safe-integer
+`revision`, `channel` (`latest`, `preview`, `nightly`), `check` and `download`
+(`automatic`, `manual`), and `install` (`manual`, `when-idle`). Only an
+independently established first installation may use the verified installed
+channel provenance in `010` and automatic check/download with manual install.
+A missing policy after initialization, or ambiguous initialization state, is a
+recovery error that disables automatic effects; it must not reset earlier
+manual choices. The package owner must preserve a durable revision high-water
+mark and refuse recovery if it cannot establish a fresh revision. Test policy
+deletion after a manual choice, owner restart and stale requests. Missing or ambiguous
+provenance is `channel-unknown`, not a version-derived preview fallback; an owner
+can explicitly choose a channel and establish verified provenance. If the current
+channel or install owner cannot be determined, report unknown and do not start
+automatic effects. This default permits preparation without a surprise restart;
+the current explicit user action is still needed to install until changed.
+The Desktop owner additionally supports `on-quit` (`030`); package-owned
+background services have no Desktop quit event and reject that value rather
+than inventing one. The shared parser accepts the union, and each owner enforces
+its own supported values before persisting.
+
+Use `withConfigMutationLockSync` (`src/config/mutation-lock.ts:96`) for a
+short compare-and-swap transaction around an atomic, private no-follow file
+write. Parse a valid record and compare `expectedRevision` before writing the
+next revision; an unreadable, malformed or unknown-schema existing record is
+not treated as missing. Lock/write failure or revision overflow changes
+nothing and returns a bounded error. Persist candidate deferral and failed
+target identity in the operation owner, not in the channel cache; a policy or
+owner change invalidates stale prepared authority. Revalidate the revision at
+each effect boundary. The `OCX_DISABLE_UPDATE_CHECK=1` process override blocks
+automatic checks and downloads without rewriting the stored policy; the status
+must distinguish configured from effective behavior.
+
+Add authenticated `GET /api/update/policy` and `PUT /api/update/policy` to
+`src/server/management/route-registry.ts` and its management dispatcher, with
+closed JSON objects and the existing management authentication/CSRF boundary.
+GET returns `{schema, policy, effective, error}` where `effective` includes
+the environment override and nullable unavailable reason. PUT accepts exactly
+`{expectedRevision, channel, check, download, install}` and returns the new
+status or a conflict without writing. These routes do not authorize an install
+and must not accept the desktop snapshot capability. Old update/check/status
+and badge schemas stay unchanged. Add domain-registered
+`tests/update/update-automatic-policy.test.ts` and
+`tests/server/update-policy-routes.test.ts` for defaults, invalid/missing
+records, concurrent revisions, failed writes, environment override, old-client
+compatibility and authorization; no real user home is used.
+
+## Required scenarios
+
+- Valid signed exact candidate stages without stopping or replacing the running
+ proxy. Missing, wrong-key, altered, changed target, untrusted transitive
+ executable or unconfirmed installer exit prevents activation and leaves the
+ live install untouched.
+- Policy, owner, serving root, target, staged tree identity and idle admission
+ are revalidated before swap. A sync/update conflict, altered stage, changed
+ channel, ownership handoff or stale reservation refuses without mutation.
+- Installation failure restores and verifies the prior tree and applies the
+ existing manager-aware runtime recovery. A success is distinguished from a
+ target version serving and passing health; uncertain outcomes never trigger a
+ duplicate install. Failed targets do not loop automatically.
+- Test npm independently on supported platforms; retain interface and ownership
+ regressions for unchanged pnpm and Bun explicit paths. Their signed-supply
+ migration belongs to `090` and `100` after each adapter is proven.
+ Unsupported or externally managed
+ installers display an honest actionable reason. The `090` and `100` units
+ remain open until their own automatic adapters are proven.
+
+## Candidate verification
+
+Run focused transactional, orchestrated, mutex, supervisor and manager tests
+with isolated homes, test keys and mock feed; demonstrate red then green for
+the new behavioral regressions. Explicitly run subprocess and source-oracle
+tests. Run `bun run typecheck`, `bun run privacy:scan`, `bun run structure:check`,
+the core Lab boundary test, and exact-head required PR checks. Obtain an
+independent security PASS for package installation and signing. Resolve exact
+test commands and prove their input coverage before locking the roadmap. No
+full root suite; the coordinator receives a PR and evidence, not a merge.
+
+Baseline verifier discovery, 2026-11-07: `bun test
+tests/update/update-orchestrated-plan.test.ts` passed seven tests using
+temporary homes and injected child behavior. `bun test
+tests/update/update-transactional.test.ts -t 'manifest verifies|happy path
+swaps'` passed two tests and filtered out ten; it uses temporary package trees
+and an npm stub. These checks exercise existing plan and transactional owners,
+not signature verification or the proposed split prepare/commit contract.
+
+`bun test tests/update/update-refresh.test.ts -t 'package cache refresh'`
+passed 16 tests with 17 filtered out (exit 0) against the existing scheduler's
+injected clock and lookup. This verifies existing refresh/coalescing/backoff and
+opt-out behavior; it does not read a future policy file or prove signed updates.
diff --git a/devlog/_plan/261107_auto_update_parity/050_sidebar_panel.md b/devlog/_plan/261107_auto_update_parity/050_sidebar_panel.md
new file mode 100644
index 00000000000..e821aeb3f08
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/050_sidebar_panel.md
@@ -0,0 +1,361 @@
+# Sidebar and update panel — draft unit
+
+**Status:** pending architecture proposal, upstream engine contract and
+independent roadmap audit. Do not implement from this draft yet.
+
+## Outcome and boundaries
+
+The proposed dashboard footer would expose desktop update state through the
+existing download orb and open the planned update panel. For browser/package
+installations, the existing dashboard update flow remains available. The native
+update page retains sole native update-command authority
+(`desktop/src-tauri/src/lib.rs:272` at the inventory baseline,
+`desktop/src-tauri/src/window.rs:115`).
+The footer's displayed version must fit ordinary stable versions; long versions
+remain fully readable without overlapping action buttons, at normal and narrow
+widths and in supported zoom modes.
+
+## Proposed file map and contract changes
+
+| Operation | Path | Before → after |
+| --- | --- | --- |
+| MODIFY | `desktop/src-tauri/src/updater.rs` | Six-field snapshot (`:159`) → versioned, bounded projection of the upstream engine's authoritative phase and progress; preserve session identity, heartbeat and stale-state semantics. The precise shape must be agreed with the engine unit. |
+| MODIFY | `src/update/desktop-badge.ts` | Exact six-field parser and availability-only projection (`:31`, `:74`) → explicitly versioned decoder supporting old shell snapshots and the new projection; validate all phase/progress combinations and preserve unknown on missing or expired evidence. |
+| MODIFY | `src/server/management/sidebar-routes.ts` | Keep authenticated/capability-bound snapshot publication, its body limit and existing read route (`:105`); only change if the agreed projection requires it. Never add an installation action to this endpoint. |
+| MODIFY | `desktop/src-tauri/src/proxy.rs`, `src/update/badge.ts`, `gui/src/lib/desktop-shell.ts` | Negotiate snapshot schema from the bound publication response and use a distinct desktop badge v2 DTO; leave package badge and legacy DTO shapes unchanged. |
+| MODIFY | `gui/src/components/sidebar-github-row.tsx` | Availability-only accent (`:133`) → localized and accessible current, available, downloading percentage, ready and failed indications. Unknown/checking is distinct from current. Continue using the existing download button. |
+| MODIFY | `gui/src/App.tsx`, `gui/src/design/bridge.css` | Preserve desktop page navigation (`App.tsx:591`); fix footer version sizing and wrapping (`bridge.css:428`) without covering star, update or terminal buttons. Keep full version accessible on mobile. |
+| MODIFY | `desktop/ui/update.html`, `desktop/src-tauri/src/lib.rs` | Existing check/install page → native panel consuming the engine's typed status and controls: channel, download and install preferences, release notes, restart and Later. New commands, if required, use the existing local update-page guard; no raw HTML insertion for release notes. |
+| MODIFY | `gui/src/i18n/{en,ko,ja,zh,zh-TW,fr,de,ru,pt,tr,vi}.ts` and applicable desktop localization owner | Add every new visible label in all supported locales, with real translations; verify the desktop page's existing localization mechanism before selecting its implementation. |
+| MODIFY | `tests/update/update-desktop-badge.test.ts`, `tests/server/sidebar-routes.test.ts`, `tests/clients/desktop-update-surface.test.ts`, `gui/tests/sidebar-version-layout.test.ts`, `gui/tests/desktop-shell.test.ts` | Add meaningful behavior cases for old/new snapshots, state projection, page authority, panel controls and version accessibility. Source-read tests must run explicitly because `test:changed` cannot follow file reads. Add a focused GUI behavior test if no current harness covers the footer. |
+| MODIFY | Relevant `structure/` owners from `structure/INDEX.md`, `docs-site/src/content/docs/getting-started/installation.md` and corresponding translated pages | Record current behavior only after implementation; update each affected owner and avoid contradictory localized instructions. |
+
+New repository test files belong in their `tests//` directory and
+require layout and expected-shard registration. Keep ratcheted files within their current line caps; move code to
+appropriately owned siblings rather than increasing baselines. The complete
+file map must be revalidated after the engine unit lands.
+
+For package-owned browser installations, extend the existing dialog rather
+than creating a second installer UI: `gui/src/pages/dashboard-dialogs.tsx`,
+`gui/src/pages/use-dashboard-data.ts` and `gui/src/pages/dashboard-shared.ts`
+read the versioned package policy/status, expose latest/preview/nightly and
+check/download/install controls, and send revision-bound policy changes. Add
+the separately versioned check/apply routes and owner-provenance consumers named
+in `010`/`040`; the current two-channel routes and old dialog keep their exact
+wire contract. An old client against a nightly installation gets an actionable
+unsupported state, not an inferred preview update. Add focused GUI and
+management tests for three-channel selection, changed revision, unavailable
+provenance, old/new clients and explicit manual update. Update all eleven GUI
+locale catalogs and the affected docs-site translations.
+
+## Proposed desktop snapshot v2
+
+The native publisher adds `schema: 2`, a monotonically increasing per-session
+`sequence`, `phase`, `installedChannel`, `policyChannel`, `candidateChannel`
+and a bounded integer `progressPercent` (0–100 or null) to the existing
+session/current/latest/availability/check-time fields. Allowed
+phases are `idle`, `checking`, `current`, `available`, `downloading`, `ready`,
+`installing` and `failed`; failed requires a closed, non-sensitive `errorCode`,
+while other phases use null, never raw errors or release notes. Enforce an exact key set and
+phase-specific constraints under the existing 1 KiB request cap
+(`src/server/management/sidebar-routes.ts:113`; native proxy cap must also be
+kept). Sequence ordering is per session: a late lower sequence cannot overwrite
+newer state; a heartbeat at the same sequence may extend expiry only for the
+same payload. Unknown versions and stale/missing session evidence remain
+unknown. Reject malformed v2 without changing a prior good state.
+
+Each channel field is `latest`, `preview`, `nightly` or null. The native owner
+populates `installedChannel` only from verified provenance bound to the current
+installation and actual artifact, `policyChannel` from its valid selected
+policy, and `candidateChannel` only from the verified candidate. These fields
+can differ after an explicit channel switch. `candidateChannel` must be null
+when `latestVersion` is null; it may be null for a retained target whose
+provenance is unavailable. Null remains unknown and never implies preview from
+the version spelling. The session-authenticated publisher sends the values in
+the same bounded v2 body; the server parses and projects them only as display
+observations, without granting install authority. A new runtime/session clears
+the projection and renegotiates. Legacy v1 snapshots remain decodable but
+their absent installed and candidate channel evidence projects unknown; their
+availability-only behavior remains intact.
+
+Upgrade the server decoder and projection before publishing v2. It accepts both
+v1 and v2; a v1 shell keeps its existing availability-only behavior. Proposed
+negotiation: a new shell first publishes a valid v1 snapshot. A new server's
+successful response adds a bounded `snapshotSchemas: [1, 2]`; the old server's
+existing `{ok:true}` means v1 only. The native publisher upgrades to v2 only
+after a successful response from the currently attested runtime; when the
+runtime identity changes it resets to v1 and negotiates again. Authentication,
+transport and parsing failures do not count as negotiation or current status.
+`desktop/src-tauri/src/proxy.rs:218` currently discards the success body and
+must return the validated schema selection without weakening its body-bound
+publication capability. Add old-shell/new-server, new-shell/old-server,
+runtime-replacement, malformed-response and auth-failure tests. Add a separate
+desktop projection DTO without changing package badges.
+
+### Desktop badge response compatibility
+
+The existing `GET /api/update/badge?surface=desktop&session=` and its
+`UpdateBadge` DTO retain their legacy field names and non-null two-channel type
+(`src/update/badge.ts:6`). If the current snapshot has verified installed
+provenance for `latest` or `preview`, the old endpoint would return the old
+shape with that channel. For a v1 snapshot it would preserve the current
+availability projection and legacy `defaultUpdateTag(currentVersion)` label;
+that compatibility label is not verified provenance or installation authority.
+Missing or expired snapshots retain the existing unknown response. A v2
+snapshot whose provenance is absent or is `nightly` would return HTTP 409 with
+bounded code `desktop_channel_unavailable`, rather than invent a third legacy
+enum. The package endpoint without `surface=desktop` remains unchanged. Test
+an old sidebar against a new server and both v1 and v2 shell snapshots.
+
+Add separately registered `GET /api/update/desktop-badge-v2?session=`
+with the same dashboard-session read authorization as the existing GET. Its
+closed `ocx.desktop.badge/2` object has exactly `schema`, `sessionId`,
+`currentVersion`, `latestVersion`, `updateAvailable`, `unknown`, `phase`,
+`progressPercent`, `errorCode`, `installedChannel`, `policyChannel`, and
+`candidateChannel`. `schema` is the literal `ocx.desktop.badge/2`; `sessionId`
+is the requested validated UUID or null when no session is supplied;
+`currentVersion`, `latestVersion`, `phase`, `progressPercent`, `errorCode` and
+the three channels are null when no current trusted snapshot supplies them.
+`updateAvailable` is false and `unknown` true without a current snapshot.
+With one, phase/progress/error and nullable channels obey snapshot v2 rules;
+a v1 snapshot can retain available/current display state but supplies null
+channels: v1 `error` and `install-failed` map to v2 `failed` with errorCode
+`unknown`, while other v1 phases retain their names and progress is null.
+No field is installation authority. Reject duplicate or unexpected
+query keys and invalid UUIDs with a bounded 400 result; absent session returns
+the unknown DTO. No cache may carry a projection between sessions.
+
+The updated desktop sidebar constructs the v2 URL and strictly parses this
+DTO in `gui/src/lib/desktop-shell.ts` and
+`gui/src/components/sidebar-github-row.tsx`; a 404 from an old server, malformed
+response, or unavailable session produces unknown while leaving the update
+control clickable. It must not use the old inferred channel as a fallback.
+The GUI must also use `lastAttemptOk` or an equivalent explicit freshness flag:
+after a rejected request, malformed JSON or timeout, retained warm-cache data
+is labelled stale/unknown and cannot be presented as a current successful
+observation. An HTTP 409/404 or session change clears or isolates prior data;
+an independently verified staged install remains controlled by its native
+owner, not by the badge cache. Test a warm `current` and `ready` observation
+followed by rejection, malformed JSON, timeout, 409 and runtime replacement.
+Add explicit serialized-response, versioned GUI parser and route tests in
+`tests/update/update-desktop-badge.test.ts`,
+`tests/server/sidebar-routes.test.ts`, `gui/tests/desktop-shell.test.ts` and a
+new `gui/tests/sidebar-desktop-badge-v2.test.tsx`. Cover old/new shells and
+servers, missing/expired/v1/v2 snapshots, same version spelling with preview
+versus nightly provenance, policy/candidate changes and runtime replacement;
+assert the package and representable legacy responses retain their exact DTO.
+
+The v2 decoder uses the existing UUID, version and checked-time bounds. The
+new `sequence` is a nonnegative JSON safe integer, and `progressPercent` is null
+or an integer from 0 through 100. `errorCode` is null or one of `network`,
+`feed-unavailable`, `verification`, `ownership`, `busy`, `installation`,
+`recovery` or `unknown`; the UI localizes codes and never renders raw errors.
+All keys are required even when their value is null. `available` is exactly
+whether `latestVersion` is non-null; any target requires `checkedAtMs`.
+
+| Phase | Target and time | Progress | Error |
+| --- | --- | --- | --- |
+| `idle` | no target; no check time | null | null |
+| `checking` | prior target/time may be retained; absent target may retain a prior check time | null | null |
+| `current` | no target; check time required | null | null |
+| `available` | target and check time required | null | null |
+| `downloading` | target and check time required | integer or null while total is unknown | null |
+| `ready` | target and check time required | null | null |
+| `installing` | target and check time required | integer or null while total is unknown | null |
+| `failed` | target and check time may be retained together; with no target, check time may be null | null | required code |
+
+Reject a phase/field combination outside this table, a nonidentical payload at
+the same sequence, and a lower sequence without replacing the last good
+snapshot. A new session never inherits a prior session's sequence or state.
+The exact size cap applies after serialization, including multibyte text.
+Test equal `-preview.*` version strings with distinct authenticated preview and
+nightly provenance; policy loss and channel switching; candidate invalidation;
+session/runtime replacement; old v1 snapshots; malformed/oversized v2; and
+stale ordering. Assert each v2 provenance channel separately and refuse inferred
+preview as provenance or installation authority. Test the legacy v1 display-only
+compatibility label and availability exception described above.
+
+The v1 encoder uses only the existing exact six-field shape. `idle`,
+`checking`, `current`, `available` and `installing` preserve their old
+meanings; `downloading` and `ready` project as `available` with the target
+and its successful check timestamp. `failed` projects as `install-failed`
+when a target is retained, or `error` when none is known. `current` carries
+no target and a successful check timestamp; `idle` carries neither. A failed
+check with no known target stays unknown, never current. Validate these cases
+against the existing v1 parser (`src/update/desktop-badge.ts:31`) and the
+oldest supported shell/server combinations before locking the contract.
+
+The desktop footer refreshes the session-scoped badge every five seconds and
+every second during `downloading` or `installing`; package/browser polling stays
+at its current ten-minute cadence. Apply resource deduplication and stop polling
+when the component unmounts. Release notes are fetched from the trusted panel
+status, not placed in the 1 KiB heartbeat. These schema and timing values are
+proposals subject to audit, and must be tested against both shell/server orders.
+
+## Proposed native panel commands
+
+All commands require the existing exact local update-page guard. Add
+`update_panel_status()` returning `ocx.desktop.update/2` with current version,
+candidate version/channel, phase, bounded progress/error code, policy and
+revision, sanitized release-note text, and deferred state. Add
+`update_panel_policy({expectedRevision, channel, check, download, install})`
+with compare-and-swap semantics; a mismatch returns the current status without
+changing it. The install control exposes manual, on-quit and when-idle policy
+with the consent and Later semantics in `030`. Add `update_prepare({candidateId})`,
+`update_defer({candidateId})`, and `update_activate({candidateId})` returning
+the same status plus a bounded accepted/refused result. The native owner
+validates IDs and rechecks candidate, policy and ownership; the caller cannot
+supply a filesystem path or trust decision. Keep existing `update_status`,
+`update_check`, `update_install` and `return_to_dashboard` compatibility and
+route them to the same claim and verified installer owner. Exact result field
+names and legacy compatibility tests must be frozen with the engine unit.
+
+Proposed `ocx.desktop.update/2` is a closed result with `schema`,
+`currentVersion`, `candidate`, `checkedAtMs`, `phase`, `progressPercent`, `errorCode`,
+`stageReadiness`, `checkError`, `policyState`, `policyObservation`, `policy`, `releaseNotes`,
+`omittedReleaseCount` and `deferred`. `stageReadiness` independently reports
+`none`, `ready` or `unknown`; `checkError` is a bounded code or null. A failed
+check may coexist with a retained ready stage, so the panel shows the error and
+the valid install action together. `policyState` distinguishes valid, corrupt,
+unreadable and missing-after-initialization policy; `policy` is null unless
+valid. For a safely observed corrupt or missing-after-initialization record,
+`policyObservation` is a bounded opaque owner-issued token;
+otherwise it is null. It contains no record contents or credentials and expires
+on owner restart. `candidate` is null or
+`{id, version, channel}` where `id` is the lowercase SHA-256 identity of the
+verified candidate; `policy` is `{revision, channel, check, download, install}`
+with the values defined by `030`; `releaseNotes` is an array of at most six
+`{version, items}` groups, each with at most eight plain-text items of at most
+220 Unicode characters, and `omittedReleaseCount` is a nonnegative safe integer
+or null when the total cannot be established. The whole encoded result is
+bounded to 32 KiB; `deferred` is boolean and may be true only for a candidate.
+Phase, progress and error follow the table above; a target-requiring phase must
+have a candidate. Construct release links from an allowlisted release origin
+and the validated exact version; do not navigate to a feed-supplied arbitrary
+URL. A malformed or unavailable status is displayed as unknown.
+
+For corrupt or missing-after-initialization policy, disable revision-bound preference changes and offer the
+proposed `update_panel_repair_policy({expectedObservation, channel})` only from
+the guarded local update page after a deliberate operator action. The native
+policy owner serializes repair with normal writes, rechecks that the observation
+still describes the same record or verified absence, preserves any recoverable prior data privately,
+and atomically creates a valid policy with the explicitly chosen channel and
+manual check, download and install settings, using a newly reserved revision
+above the durable high-water mark described in `030`. If the mark cannot be
+established, repair refuses; it never reuses an earlier revision. A changed observation refuses the
+repair and returns fresh status. If the record cannot be read or safely
+preserved, repair refuses and the panel shows recovery guidance; it never
+silently replaces an unreadable record. Successful repair returns a valid policy
+and revision before normal changes resume. Owner and panel tests must cover
+corrupt, unreadable and lost records, refusal, successful repair, stale observations,
+stale policy changes and prepared actions across repair, high-water loss,
+concurrent writes and owner restart.
+
+The signed release owner supplies same-channel notes for releases newer than
+the installed version through the selected candidate. It verifies every
+release record used to create a group, orders groups newest first, and reports
+omitted groups when a complete bounded history is known; a missing or
+unverifiable history gives a null omission count rather than inventing one.
+`010` binds each record's notes to its exact version/channel. Render every
+item as text. Preserve the staged candidate's previously verified notes when a
+later check fails, reports no update or returns the same candidate without
+notes (`030`). Test multi-version grouping, channel isolation, omissions,
+truncation, malformed/unavailable history and retained ready state. GOAT's
+reference groups and limits are `apps/desktop/src/updates/releaseNotes.ts:19`,
+`:177`, `:208` at the pinned GOAT revision.
+
+`update_panel_status` accepts no arguments and returns that object.
+`update_panel_policy` accepts exactly `{expectedRevision, channel, check,
+download, install}`; the revision is a nonnegative safe integer and the
+values are closed enums. `update_panel_repair_policy` accepts exactly a valid
+owner-issued `{expectedObservation, channel}`. `update_prepare`, `update_defer` and
+`update_activate` accept exactly `{candidateId}`. Mutation commands return
+`{schema: "ocx.desktop.update-command/1", outcome, reason, status}` where
+`outcome` is `accepted` or `refused`, `reason` is a closed non-sensitive code
+or null, and `status` is the freshly observed object above. The native owner
+serializes and validates all arguments and uses its existing shared install
+claim; an accepted request means accepted work, not installed/healthy.
+Unparseable arguments or an origin rejected by the guard are command errors
+without state mutation. Client timeout remains unknown until status is read;
+the page does not resubmit activation automatically.
+
+Keep the legacy commands' existing five-field `PageUpdateStatus`
+(`desktop/src-tauri/src/updater.rs:263`) and legacy failure behavior for older
+bundled pages, with one engine/claim behind both versions. The new page must
+detect an unavailable v2 command, display an actionable compatibility state,
+and never issue a second install after an uncertain old-command result. Test
+legacy/new pages with old/new shell APIs and concurrent old/new command calls.
+
+## Locale handoff
+
+The dashboard owns its selected locale (`gui/src/App.tsx:152`) and persists it
+on the dashboard origin (`gui/src/i18n/provider.tsx:13`). The trusted bundled
+update page is a different origin (`gui/src/lib/desktop-shell.ts:36`), so it
+cannot rely on that local storage. Proposed navigation passes only the selected
+closed-set locale as an encoded query value to the existing `/update.html`
+path. The bundled page validates it against the eleven supported locale IDs
+(`gui/src/i18n/shared.ts:6`), falls back to `en` for missing/invalid input, and
+sets the matching HTML language. Locale is display data and grants no native
+authority; `require_update_page` must continue to check the exact bundled
+origin and path (`desktop/src-tauri/src/window.rs:115`).
+
+The existing Back command uses a stricter settings-page guard that rejects all
+queries (`desktop/src-tauri/src/lib.rs:301`, `desktop/src-tauri/src/window.rs:134`).
+Change its update-page branch to accept either no query or exactly one `locale`
+query whose decoded value is one of the eleven supported IDs. Continue to
+require the main window, exact app origin/path, no userinfo or fragment, and
+reject duplicate or unknown query keys. The CLI settings page keeps its
+existing no-query rule. Add Rust URL-predicate tests and desktop-page tests for
+`/update.html?locale=ko` returning to the dashboard, invalid/duplicate values,
+other origins and CLI-page queries. The current guard is not assumed to accept
+the new navigation without this change.
+
+Produce `desktop/ui/update-locales.json` deterministically from a React-free
+typed table `gui/src/i18n/desktop-update-copy.ts` using
+`scripts/generate-desktop-update-locales.ts`; include a `--check` mode in the
+desktop build to detect drift. Bundle all eleven translations and keep the
+source table as the single owner of panel copy. Render only text, never
+HTML from release notes or locale input. Add a test that every locale has each
+key and that invalid locale inputs cannot change origin or inject markup.
+Test the generator, build check and bundled Tauri asset loading. Do not add a
+runtime network fetch for translations.
+
+## Activation scenarios and evidence
+
+- Old desktop snapshot is accepted by the newer server and preserves its prior
+ available/unknown meaning; unknown phases and malformed fields are rejected.
+- Missing, expired or another session's snapshot shows unknown; a check failure
+ never displays up-to-date; a retained ready stage stays installable while the
+ check error is shown. A current result, available version, progress,
+ staged result and failure each produce distinguishable labels and states.
+- A browser dashboard click reaches its existing package dialog; a desktop click
+ reaches the bundled panel. Requests from a non-update page cannot invoke
+ privileged native update commands.
+- Preferences survive reopening; Later defers activation and does not erase the
+ verified staged update. Release notes render as safe text with an appropriate
+ release link. A failed download or install gives a retryable, accurate state.
+- At normal and narrow desktop widths, mobile drawer width, 90% and 100% zoom,
+ the footer text and all controls remain visible and usable; test long version
+ identifiers, keyboard focus and screen-reader labels. Capture before/after
+ UI screenshots through the authorized `pr-assets` path, never in the PR branch.
+
+## Candidate validation
+
+Baseline discovery has run the two badge test files (17 pass) and, from `gui/`,
+the sidebar version and row tests (12 pass); see `001_inventory.md`. During
+implementation run failing-first focused tests, the explicit desktop surface
+and sidebar-route files, `bun run typecheck`, `bun run privacy:scan`,
+`bun run structure:check`, and from `gui/` `bun run lint:i18n`, `bun run lint`,
+`bun test tests` and `bun run build`, plus rendered browser/native proof. These
+commands are candidate gates; establish their exact change coverage and record
+fresh results before the roadmap is locked or the PR is review-ready. No full
+root suite. PR CI must be evaluated at the exact head; request the separate
+security review if any authority, updater or installation surface changes.
+
+Baseline on 2026-11-07: `bun test tests/server/sidebar-routes.test.ts
+tests/server/link-management-relay.test.ts` passed 31 tests (exit 0) with
+isolated test homes and ephemeral listeners. The sidebar file exercises the
+existing six-field snapshot's route, session separation, input limits and
+authorization. The relay file exercises the existing passive link boundary.
+Neither test proves the proposed v2 snapshot or native panel; add those cases
+and demonstrate their initial failure before implementation.
diff --git a/devlog/_plan/261107_auto_update_parity/060_linked_updates.md b/devlog/_plan/261107_auto_update_parity/060_linked_updates.md
new file mode 100644
index 00000000000..3fc1d2973bb
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/060_linked_updates.md
@@ -0,0 +1,451 @@
+# Linked-machine updates — draft unit
+
+**Status:** depends on the signed supply and local update contracts; architecture,
+transport authority and independent security audit are unresolved. Do not
+implement from this draft yet.
+
+## Outcome
+
+The proposed operator experience would show and control eligible Home and Child
+updates, including per-machine state, deferral and failure. Automatic work would be permitted only when
+the destination's own setting allows it, the exact artifact is signed and
+verified, the destination identity and installer ownership are established,
+compatibility is known, and the guarded stop/drain and idle admission succeed.
+The Home must not report an offline, stale, unverified or unsupported Child as
+updated. All tests use isolated homes, fake links and local mock supply.
+
+## Proposed transport and durable state
+
+Use the existing host-key-pinned, non-interactive SSH exec transport for
+Home-initiated links (`src/link/ssh-argv.ts:1`, `:70`) and select the Child
+executable once, as the pinned sync path does (`:244`). Client-initiated links
+without an authorized Home-to-Child exec route are explicitly unsupported until
+a separately audited transport exists. Keep the management relay read-only
+(`src/link/management-routes.ts:1`); peer reports are compatibility
+observations, not authorization (`src/link/peer-report.ts:1`).
+
+Proposed new CLI operation is `ocx link-update` with `prepare`, `status`,
+`commit` and `cancel` subcommands and a versioned bounded JSON request/result
+schema: `ocx.link-update.envelope/1`, `ocx.link-update.request/1` and
+`ocx.link-update.result/1`. Inputs include an operation UUID, exact link id, candidate identity,
+destination policy revision, plan identity and expiry. A `status` read is
+idempotent; `prepare` and `commit` record their operation ID before work;
+replaying the same request returns the recorded state, while a conflicting
+request with the same ID is refused. `cancel` cancels only an uncommitted
+operation; after a handoff the caller observes it rather than starting another.
+Transport request data is passed as bounded structured input, not interpolated
+into shell commands. The Home stores an atomic fleet journal before activation
+with target identities and per-Child outcomes, then resumes observations after
+its own restart. A healthy Home already at the selected version satisfies the
+Home-first prerequisite. A stale or unverified Home does not.
+
+The proposed exact envelope is `{schema, keyId, payload, signature}` with
+`schema: "ocx.link-update.envelope/1"`; `payload` is base64url of the exact
+UTF-8 request bytes and the signature covers those bytes and a protocol-domain
+separator. The closed request has `schema`, `action`, `requestId`, `nonce`,
+`issuedAt`, `notAfter`, `fleetId`, `homeId`, `childId`, `configurationId`,
+`installationId`, `linkId`, `linkBindingEpoch`, `grantId`, `grantEpoch`,
+`operation` and action-specific `args`. `prepare` names the destination policy
+revision and exact signed candidate digests; `commit` names the prepared handle,
+plan digest, expected operation revision and Home health barrier; `cancel`
+names the operation and expected revision; `status` is a read-only discovery or
+one-operation query. Each action is separately signed and cannot carry an
+arbitrary executable path, command or download URL. Bound the encoded envelope
+and result to 64 KiB, decoded request to 32 KiB, input to five seconds, and
+request validity to five minutes; these are proposed testable limits.
+
+The response `ocx.link-update.result/1` binds `requestId`, request digest and
+nonce, destination and grant/binding identities, operation identity/revision,
+durable phase, bounded reason, candidate and policy revision, and separate
+`commitAccepted`, `mutationStarted`, `installationVerified`, `runtimeHealthy`
+and `rollbackVerified` observations. It is signed by the enrolled destination
+response key under a separate domain. Unknown schema, malformed encoding,
+duplicate keys, mismatched request binding or unavailable identity is a refusal
+or unknown result, never success. These schemas require independent security
+review and an exact implementation-level field chain before the plan is locked.
+
+The Child independently checks its durable link connection under the lifecycle
+and configuration locks used by its existing expectation check
+(`src/client/sync-expectation.ts:108`), its own opt-in policy/revision, selected
+executable and install owner, capability, exact signed candidate and fenced
+idle admission. SSH authentication alone and the unkeyed plan-consistency hash
+(`src/update/orchestrated-token.mjs:3`) are not a replacement for destination
+consent or operation authorization. The request proof, replay journal and
+lock ordering need a concrete protocol and independent security review before
+the roadmap is locked; this paragraph does not claim those controls exist.
+
+The independent `auto-update-linked-authority-research-01` lane recommends a
+separate Home update signing key and a destination-local grant bound to the
+Home, Child, installation and link generation. Creating or approving that grant
+is an explicit action by the destination owner; an agent must not auto-answer
+it, and old links get no implicit update authority. Separate local policy
+controls preparation and activation. A Home request cannot enable a disabled
+destination policy or clear its deferral. Key provisioning, response proof,
+rotation and revocation are proposed design work, not existing features or a
+security PASS; tests use isolated keys and homes only.
+
+Proposed owner-facing local commands are `ocx link-update grant list --json`,
+`ocx link-update grant approve --request `, and
+`ocx link-update grant revoke --grant `. Approval and revocation act only
+on the destination's local grant store and require the repository's existing
+person-consent boundary; a Home request may present a public-key enrollment
+request but cannot approve it. The CLI registry, capabilities and generated
+skill surface advertise the new contract only once dispatch and grant
+operations exist. A lost approval response is reconciled by grant ID, not
+automatically approved again. No agent or test may approve a real user's grant;
+tests use isolated fake destinations.
+
+The proposed grant store is `/link/update-grants.json`, owned by
+the destination account and written atomically with the existing private-file
+policy. Each strict `ocx.link-update.grant/1` entry contains `grantId`,
+`grantEpoch`, `homeId`, `homeUpdatePublicKey`, `childId`, `configurationId`,
+`installationId`, `linkId`, `linkBindingEpoch`, `apiKeyId`, `scopes`,
+`expiresAt`, `revoked` and `childResponsePublicKey`. The destination response
+public key is enrolled with the grant; private key material belongs in the existing secure
+credential owner, never in a status projection or source control. Grant
+approval, revocation, connection change and local policy changes serialize
+with the durable destination binding. The precise key-storage adapter and
+person-consent API must be verified against existing platform implementations.
+
+The proposed key owner is a new `src/link/update-key-store.ts`, independent of
+provider API-key accounts. Store the Home update signing key and each Child
+response private key under the respective owner's private link directory using
+the existing no-follow, owner-only file and required Windows ACL patterns
+(`src/codex/installation-id.ts:19`, `:40`); verify a newly written public key
+against the private key before publishing a grant. Never print, transmit or
+copy private keys into journals, status or config. An OS keyring may be used
+only through an explicitly tested adapter; locked/unavailable storage refuses
+the operation rather than generating a replacement identity or falling back
+silently. File ownership and ACLs protect the account boundary, not against an
+arbitrary process already acting as that account. Tests use temporary homes
+and generated fixture keys only.
+
+The explicit local grant approval must validate the presented Home public key,
+link identity, scopes and expiry and require the destination owner's action;
+an incoming Home request cannot approve itself. For CLI-driven approval use the
+existing agent-driven and interactive-consent controls
+(`src/cli/agent-driven.ts:60`, `src/cli/interactive-confirm.ts:1`), with a
+separately reviewed dashboard equivalent if one is added. Refuse absent or
+unreadable local consent and persist the approved binding before returning
+success. Revocation is an owner action and never waits for Home reachability.
+
+Binding migration must cover the actual writers, not just the connection type:
+`commitClientConnection` and `clearClientConnection` in
+`src/client/state.ts:202`, `:230`; connect/rotation and disconnect in
+`src/client/connect.ts:577`, `:1193`; Child-initiated link sidecar publication
+and removal in `src/client/link-join.ts:159` and `src/client/link-state.ts:129`,
+`:149`; Home link creation, enrollment, compensation and revocation through
+`src/server/management/link-routes.ts:99`, `:233`,
+`src/server/management/link-enrollment.ts:165` and
+`src/server/management/link-route-state.ts:153`. The current Child sidecar
+parser has an exact field set (`src/client/link-state.ts:46`): define and test
+an explicit old-record migration before adding any field, and preserve its
+fail-closed marker behavior. Search all config and key writers again when the
+unit begins; this list is a reviewed starting point, not proof of exhaustiveness.
+
+On relink, ownership change or key rotation, first persist the invalidation of
+the old binding under the relevant locks, then publish the new connection; a
+crash between them leaves update access disabled and requires a new local
+grant. On revoke, mark the grant revoked and advance its epoch durably before
+acknowledging, then stop future prepare/commit dispatch. An already accepted
+operation is reconciled by its destination owner to a safe outcome; revocation
+does not authorize a second installer or erase its recovery record. Status
+under an invalidated binding is available only to the local owner; a stale
+remote grant receives a refusal. Add focused tests for each writer,
+interrupted invalidation, concurrent commit/revoke and old-record migration.
+
+For each operation, persist request identity and a monotonically increasing
+destination operation revision before starting work. Identical retries return
+the recorded outcome; conflicting reuse is refused. Cancellation and commit
+compete in a single durable transition; cancellation after accepted commit is
+too late, and recovery continues under the destination owner. An expired or
+lost request is observed through a new status request, never blindly replayed
+as a fresh install. Windows SSH currently ties the selected command to the
+channel lifetime (`src/link/remote-windows.ts:37`), so a destination-owned
+worker handoff must be proven on that platform before accepted work is claimed
+to survive disconnect.
+
+The proposed lock order is `U → O → A → N → L → C → J`: existing home
+sync/update mutex (`U`), proposed installation-wide ownership coordination (`O`), link apply
+exclusion (`A`), native integration (`N`), client lifecycle (`L`), config
+mutation (`C`), then a short journal transaction (`J`). Existing client code
+documents `N → L → C` (`src/client/lifecycle-lock.ts:1`); the combined order
+is a proposed extension and must be proven against every caller; `O` is not an
+existing installation-wide lease. No inner lock
+is held over SSH, installer execution or readiness waits. Preparation holds
+only a stage lease while waiting; activation reacquires and revalidates the
+relevant owners. A durable binding epoch must change before a relink, key
+rotation or installation-owner change can become effective.
+
+Compatibility for the future Home/Child pair must be established from signed
+release capability metadata for each candidate, rather than extrapolated from
+current peer reports. Unknown or invalid future compatibility defers. A Child
+that cannot update through its actual installer reports a specific unsupported
+or owner-action state. The full installer/platform matrix and target metadata
+format remain open deliverables, not a diagnostic-only completion condition.
+
+The signed compatibility descriptor must bind each exact release artifact and
+its provided/required link protocols, update actions, recovery protocol,
+installer adapter and fleet-journal reader versions. Before Home-first
+activation, check the current pair, the new Home with each old Child, the new
+Home with each new Child, Child recovery under the new Home, and Home recovery
+after a Child has moved. Unknown pairs defer. After Home restart, obtain fresh
+identity and health evidence before resuming Child commits; a healthy Home
+already on the selected signed artifact can satisfy the prerequisite without a
+new update. Initial rollout activates one Child at a time, and post-update
+catalog sync is reported separately.
+
+`003_contracts.md` defines the closed descriptor and exact-pair comparison.
+Add `src/link/update-compatibility.ts` and
+`tests/clients/link-update-compatibility.test.ts`: consume only descriptors
+validated by the shared supply parser and compare each required Home/Child and
+recovery pair. Validate the Home fleet journal reader and destination adapter
+for the actual operation, not merely one matching link protocol. The release
+unit owns generation and parser vectors; this unit owns the comparator and
+fleet gating. Older artifacts without authenticated descriptors stay unknown
+until the publisher provides a descriptor bound to their exact bytes or the
+owner performs an explicit manual bootstrap. This proposal does not assert
+that historical descriptors already exist.
+
+The Home-first barrier applies to a coordinated fleet operation, not to every
+independent server update. An owner may request an individual Child update
+while Home remains on its current release if the authenticated descriptor
+proves that current Home and both the target and recovery Child are compatible.
+The Child may also follow its own enabled signed channel and idle policy while
+the Home is offline. Such work retains destination consent, signed supply,
+durable operation identity and guarded activation. This preserves the
+independence documented by GOAT (`docs/internals/remote-updates.md:21`, GOAT
+reference revision); lack of a live Home is not permission to weaken those
+local checks. Fleet Home-first resumption and individual Child operation need
+separate tests and status.
+
+Independent automatic updates may race, so a comparison of only today's peer
+versions is insufficient. For an offline or uncoordinated move, require both
+the currently installed artifact and its target/recovery artifacts to carry the
+same publisher-authenticated `compatibilityEpoch` and
+`compatibilityVectorSha256` described in `003`; each endpoint retains the
+last verified peer epoch/vector and installation binding. This is a
+publisher-certified invariant over every artifact in that epoch, not a claim
+that a peer observation is fresh. A target that changes either value cannot
+activate independently: it must use the coordinated fleet transition, which
+holds the fleet generation lease and checks all transitional/recovery pairs.
+An unknown, revoked or unavailable epoch also defers. A current peer reading
+is still required before a coordinated commit; an old authenticated reading is
+not treated as current. An operator may perform a separately authorized manual
+recovery if coordination is unavailable, without weakening signature or owner
+checks.
+
+The release build must derive the compatibility vector from the owning feature
+and protocol declarations and prove the epoch invariant in release tests; a
+publisher must not reuse an epoch after a compatibility change. Add
+`tests/clients/link-update-compatibility.test.ts` cases where Home and Child
+simultaneously validate their old peer and select individually compatible but
+mutually incompatible targets: changed epochs must refuse independent
+activation. Also test matching epoch/vector with an offline Home, stale or
+revoked descriptors, rollback targets and a coordinated breaking transition.
+Before a coordinated epoch change, the Home durably records the transition and
+obtains an authenticated generation-bound update fence from every participating
+Child. Each Child closes new independent admission, reports and reconciles any
+already accepted operation, and retains that fence until the Home commits or
+cancels the same fleet generation. An offline or uninspectable participant
+blocks that breaking transition; the coordinator must not infer an ack from a
+stale report. Lost acknowledgements are read back against the same generation,
+and a restarted coordinator reconciles the durable fences before dispatching.
+Test an independent update accepted just before the fence, one racing it, a
+lost acknowledgement and offline participants. Existing same-epoch automatic
+updates can resume only after their local fence is released or reconciled.
+This permits independent updates only where the signed compatibility horizon
+proves the resulting cross-product, and remains a design pending security and
+release review.
+
+The proposed Home journal is `/link/fleet-updates.sqlite`; the
+Child journal is `/update/link-operations.sqlite`. Both use private
+permissions and durable transactions. The Home stores fleet ID, generation,
+frozen member/link identities, exact artifact digests, Home health barrier,
+per-Child operation IDs, request digests, latest authenticated result and
+separate sync outcome. The Child stores grant/binding epochs, operation sequence,
+request digest, local policy revision, prepared-stage identity, durable phase,
+installer/stop receipts and outcome. Request or result secrets are not logged.
+Home startup takes one fleet lease, reconciles unresolved intents before
+dispatch, re-proves its own target/health and establishes a fresh barrier;
+Child startup reconciles its own unfinished operation before accepting another.
+The initial implementation must prove SQLite durability and platform file
+ownership using isolated crash/restart tests, not just schema serialization.
+
+## Existing owners and candidate change map
+
+| Operation | Path | Before → proposed after |
+| --- | --- | --- |
+| MODIFY | `src/link/compatibility.ts`, `src/remote/features.ts`, `src/link/peer-report.ts` as needed | Reuse authenticated relay and peer-feature compatibility (`compatibility.ts:71`); advertise a versioned update capability only when it is actually implemented. Unknown and older peers stay readable but are not admitted to unattended replacement. |
+| NEW / MODIFY | `src/cli/link-update-command.ts`, `src/cli/dispatch.ts`, `src/cli/registry.ts`, `src/cli/capability-contracts.ts`, `src/cli/capabilities-base.ts`, `skills/ocx/` | Add separate local grant and remote operation entry points, advertise `link-update/1` only when implemented, and regenerate the CLI skill surface. Preserve existing command output and use the established person-consent controls for grant changes. |
+| MODIFY | `src/client/connect.ts`, `src/client/state.ts`, `src/types/config.ts` and all connection-state readers identified by search | Create and persist a binding epoch, parse older records safely, invalidate grants before connect/disconnect/rotation or installation ownership changes, and recheck the same binding under existing locks. The full writer/consumer list and migration tests must be enumerated before implementation. |
+| NEW / MODIFY | `src/link/` update orchestration sibling, `src/cli/sync-fanout.ts`, `src/cli/sync-command.ts` | Existing fan-out selects known linked Children and bounds work (`sync-fanout.ts:99`, `sync-command.ts:207`). Reuse identity, deadline and per-child verdict patterns while keeping update and catalog sync separate contracts. Establish whether a shared runner is appropriate before editing. |
+| MODIFY | `src/link/ssh-argv.ts`, `src/link/remote-windows.ts` only after design approval | For Home-initiated SSH, select the existing pinned executable, frame its output and carry an exact validated update request; never interpolate untrusted shell text. Preserve existing `sync --expect-link` behavior. |
+| MODIFY | `src/update/orchestrated-plan.ts`, `src/update/orchestrated-entry.ts`, `src/update/orchestrated-admission.mjs` only where required | Existing plan rejects ambiguous ownership, source/desktop/mise installs and downgrades (`orchestrated-plan.ts:21`); reuse plan token, deadline, installer and observed-result contracts, adding exact verified artifact and destination-bound authorization without weakening them. |
+| NEW | Destination-owned policy, operation journal and admission modules under the established owners | Persist explicit local enable/defer policy and bounded status. Recheck link identity, consent, signed target, compatibility and idle immediately before mutation. The owner and wire schema need architect and security review. |
+| NEW / MODIFY | `desktop/src-tauri/src/link_update_bridge.rs`, native update owner in `desktop/src-tauri/src/updater.rs`, `src/link/` destination worker | A destination-owned Desktop app accepts only a locally granted, operation-bound request and applies it through its existing native updater, consent policy and Stop/Quit-aware recovery. A package worker never replaces a Desktop bundle. An offline app, absent local grant or unverifiable app identity returns owner-action/unknown; completing this unit requires supported remote Desktop activation, not a permanent refusal. |
+| NEW / MODIFY | `src/server/management/link-update-routes.ts`, `src/server/management-api.ts`, `src/server/management/route-registry.ts`, `gui/src/remote-link-update-api.ts`, `gui/src/pages/RemoteLink.tsx`, `gui/src/components/remote-link-update-panel.tsx`, all GUI locales and relevant docs-site locales | Register the separate versioned status/action endpoints below, parse their DTOs in the GUI and render destination status, retry and Later. Preserve `parseRemoteLinkStatus` and the old `/api/link/status` response. Never claim completion until destination evidence proves version and health. |
+| MODIFY | `tests/cli/sync-fanout.test.ts`, `tests/clients/link-peer-compatibility.test.ts`, focused `tests/update/` and `tests/clients/` | Cover authenticated identity, mixed old/new peers, Home/Child ordering, duplicate requests, deadlines, signed payload mismatch, busy/idle races, recovery and sync interaction using isolated fixtures. Register new domain test files in both layout maps. |
+| MODIFY | `structure/remote-link.md`, `structure/remote-messaging.md`, `structure/ops/service-and-sidecars.md`, other source owners from the index | Review both `src/link/` owners (`structure/INDEX.md:366`); record actual post-change ownership, wire schema, compatibility, outcomes and invariant tests in affected docs. |
+
+### Dashboard management boundary
+
+`src/server/management/link-update-routes.ts` owns separately registered
+`GET /api/link/updates/v1/status`, `POST /api/link/updates/v1/action` and
+`GET /api/link/updates/v1/action-result?requestId=`;
+`src/server/management-api.ts` dispatches the sibling through its existing
+on-demand management composition and `route-registry.ts` records both methods
+and the mutation. Both require the existing operator-paired dashboard session
+(`link-route-state.ts:67`); reject admin-token-only, unpaired, Tailscale
+identity, passive relay and snapshot-capability callers. Keep existing CSRF,
+origin, body and mutation controls, and prove the new handler is never exposed
+through the read-only link management relay. Neither endpoint can approve,
+create, widen or revoke a destination grant or alter destination policy.
+
+Status returns a closed `ocx.link-update.dashboard-status/1` object with
+`schema`, `fleetId`, `fleetRevision`, `destinations` and `nextCursor`. A page
+contains at most 100 destinations; `nextCursor` is null at the end or an opaque,
+bounded server-issued cursor bound to fleet ID/revision and this paired session.
+The only optional query is `cursor`; stale or invalid cursors refuse rather than
+mixing revisions, and the GUI restarts the read on a changed fleet revision.
+Each item has
+`linkId`, `bindingEpoch`, `operationId`, `operationRevision`, `candidateId`,
+`destinationPolicyRevision`, `lastActionRequestId`, `phase`, `reason`, `commitAccepted`,
+`mutationStarted`, `installationVerified`, `runtimeHealthy`, and `deferred`.
+Nullable identities and outcomes mean unknown, never success. `phase` is one
+of `unknown`, `unavailable`, `eligible`, `preparing`, `ready`, `deferred`,
+`committing`, `installed`, `healthy`, `failed`; `reason` is a bounded closed
+code, not raw remote errors. Values come from the local durable Home journal
+and authenticated Child result; the DTO is display evidence only. A stale or
+offline Child remains unknown/unavailable and cannot be called healthy.
+`lastActionRequestId` is null or the newest journaled dashboard request for the
+link, including an unresolved request. It lets a newly opened GUI recover the
+request identity after a tab or Home restart; it is not an action capability.
+
+The closed action request `ocx.link-update.dashboard-action/1` contains
+`schema`, fresh UUID `requestId`, `action` (`retry` or `later`), `linkId`,
+`bindingEpoch`, `operationId`, `expectedOperationRevision`, `candidateId` and
+`destinationPolicyRevision`. Require exact keys, bounded bodies and matching
+journal/binding/policy revisions; null operation/candidate is rejected for an
+action. `later` durably suppresses this candidate for that destination until an
+explicit retry or a different verified candidate; it never cancels a committed
+handoff or changes the Child's policy. `retry` clears that local deferral only
+after rechecking destination grant, local policy, compatibility, candidate,
+identity and admission; a new operation is allowed only after reconciliation of
+the old one. The response contains `schema`, `requestId`, `operationId`,
+`operationRevision`, `disposition` (`pending`, `accepted`, `deferred`, `refused`
+or `uncertain`) and `reason` (a bounded code or null), with literal schema
+`ocx.link-update.dashboard-action-result/1`. It is an acknowledgement, not proof of
+installation. Before external effects, atomically persist `requestId`, SHA-256
+of the exact accepted request bytes, original operation ID A, destination/link
+and binding identity, and a pending receipt. The journal then records resulting
+operation ID B (or null), resulting revision and disposition. On repeated POST,
+look up request ID and compare the exact-body digest before stale-revision
+checks: identical bytes return the existing pending or recorded receipt only;
+a different digest refuses. Neither replay nor a readback may issue an effect.
+First execution still validates current grant, policy, binding, candidate,
+operation revision and owner before dispatch. A crash between journal and
+dispatch is reconciled by the durable Home owner and destination protocol;
+pending does not mean the effect did or did not occur.
+
+The separate `action-result` GET requires a newly authenticated paired session,
+accepts exactly one UUID `requestId` and returns a closed bounded
+`ocx.link-update.dashboard-action-readback/1` object: `schema`, `requestId`,
+`requestDigest`, `linkId`, `bindingEpoch`, `originalOperationId`,
+`resultingOperationId`, `operationRevision`, `state` (`pending`, `recorded`,
+`unknown`) and `disposition`/`reason` (nullable until recorded). For `unknown`,
+all evidence fields except `schema` and requested `requestId` are null. A known
+request is looked up from the durable Home journal, not from the currently
+observed peer; after relink or grant revocation it is historical evidence only.
+Unknown, expired or pruned records return null evidence and `unknown`, never
+evidence that an effect did not occur. Retain unresolved requests and their
+operation bindings until reconciliation; a bounded terminal-retention policy
+may prune only settled receipts. After a lost response, GUI reload, Home restart
+or session replacement, the GUI recovers `requestId` from its pending request or
+the status projection, reads that exact request, then observes resulting B if
+known. An unresolved or unknown result disables further effectful retry until
+owner reconciliation; no mere possession of the request ID grants authority.
+
+`gui/src/remote-link-update-api.ts` strictly parses these separately versioned
+objects; `RemoteLink.tsx` renders a sibling
+`gui/src/components/remote-link-update-panel.tsx` keyed by link and operation,
+without appending fields to `parseRemoteLinkStatus`. On a 404 or unsupported
+schema from an older server, show an unavailable state and disable actions;
+never infer success. Unit `080` uses this same Home-side DTO after its own
+Child-pull authority and durable operation contract are implemented. Add
+`tests/server/link-update-dashboard-routes.test.ts`,
+`tests/clients/link-update-dashboard.test.ts` and
+`gui/tests/remote-link-update-panel.test.tsx` as future tests, with the two
+root-test layout registrations where applicable. Cover paired versus unpaired,
+admin and relay rejection, malformed and stale revisions, old servers, mixed
+links, offline status, Later persistence, revocation/policy change, duplicate
+requests, A→B Retry with a lost acknowledgement, identical replay before stale
+revision rejection, conflicting request bytes, pending/unknown retention,
+repaired sessions, Home restart, pagination and both SSH and Child-pull
+transports, and translated accessible
+states. A reachable implementation and these tests require separate security
+review; these DTOs themselves are not authorization.
+
+The Desktop bridge is a separate local owner IPC, not a call to privileged
+WebView commands. The native app creates an owner-private endpoint under its
+validated update-operation directory: a Unix-domain socket on macOS/Linux or a
+current-user-ACL named pipe on Windows. Its private identity record binds app
+PID/process instance, install ID, endpoint and a fresh secret; the destination
+worker validates file ownership, challenges the live app and binds the reply to
+the same connection and process instance before sending the already signed,
+bounded request. The app verifies the destination-local grant, link binding,
+policy, candidate and operation revision again, then invokes its existing
+native update owner; neither the IPC caller nor the dashboard gets an install
+path or verification bypass.
+
+Each IPC request carries operation ID/generation and a nonce plus a body-bound
+capability derived from the private app secret; the reply binds that request
+digest, nonce and native owner generation. Record `accepted` durably before a
+success reply. Repeated identical requests read the same operation; changed or
+stale requests refuse. The app and its separately signed guardian preserve the
+journal and reconcile after application exit; a lost IPC reply is observed via
+the destination journal, not retried as a second install. If the app is not
+running or its identity/endpoint is unknown, return owner-action/unknown; do
+not start or replace it as a package. Add isolated POSIX and Windows IPC tests
+for forged or stale app identity, unauthorized callers, changed grant/policy,
+lost replies, process exit, concurrent native-page/remote actions and Stop/Quit
+intent. The IPC boundary is separate from the bundled update-page guard
+(`desktop/src-tauri/src/lib.rs:272` at the inventory baseline) and requires
+independent security review.
+
+## Required activation scenarios
+
+1. For a fleet operation, Home is updated and proven healthy before Child
+ commits; for an independently requested Child operation, the current Home and
+ target/recovery compatibility can authorize the Child without updating Home.
+ Each eligible Child uses its own exact target and independently verifies the
+ signature. Catalog synchronization remains separately reported.
+2. A disabled or deferred Child, changed link identity, disconnected Child,
+ unknown capability, incompatible protocol, source/mise-owned install,
+ missing signature or target downgrade produces a specific non-success state
+ without touching the installation. Device-side consent is not inferred from
+ Home control alone.
+3. An active request or unknown idleness defers. If idleness, identity, policy,
+ target, owner or plan token changes after planning, a fresh admission refuses
+ before any mutation; the existing stop/drain path never interrupts a request.
+4. A timeout or disconnected client cannot be called success. A Child that
+ continues after a lost reply is observed rather than blindly retried; a
+ failed or rolled-back target is not automatically retried in a loop. Result
+ readback verifies version and health, with explicit uncertain outcomes.
+5. Old peers and malformed/unknown capabilities cannot acquire update authority;
+ standard sync remains functional. Exercise POSIX and Windows command framing,
+ duplicate and partial outputs, concurrent operations and isolated fake feeds.
+6. A linked Desktop owner applies an authorized request only through its native
+ updater; missing local consent, a stopped/offline app, changed native owner or
+ busy activity is reported without a package installer taking over. Test both
+ successful native delegation and refusal, including Stop/Quit intent.
+
+## Candidate verification
+
+Run red-to-green focused fan-out, compatibility, orchestrated-update and new
+integration tests, including subprocess/source-read files explicitly. Run
+`bun run typecheck`, `bun run privacy:scan` and `bun run structure:check`;
+validate relevant GUI locales/build if visible UI changes, and obtain a separate
+independent security PASS. Exact commands and evidence that they reach the
+changed paths must be established before locking the plan. No full root suite;
+required exact-head PR CI and any authorized platform proof remain mandatory.
diff --git a/devlog/_plan/261107_auto_update_parity/070_standalone_updates.md b/devlog/_plan/261107_auto_update_parity/070_standalone_updates.md
new file mode 100644
index 00000000000..ed28a54bcbf
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/070_standalone_updates.md
@@ -0,0 +1,66 @@
+# Signed standalone updates — draft unit
+
+**Status:** depends on signed supply, update-stop admission, package/guardian
+recovery and linked update contracts. The current orchestrated plan maps a
+non-Desktop standalone installation to unknown (`src/update/orchestrated-entry.ts:128`);
+this unit must not change that refusal until the new owner is proven.
+
+## Outcome and proposed file map
+
+Support an explicitly owned standalone executable on the existing release
+targets (`scripts/standalone-targets.ts:7`). The owner downloads a signed exact
+platform artifact, verifies it and a signed previous recovery artifact, stages
+both outside the live executable and uses the same policy, idle reservation,
+receipt-backed stop and durable recovery contracts as the package unit.
+Unknown, foreign, source and externally managed paths remain refused.
+
+| Operation | Proposed path | Required change |
+| --- | --- | --- |
+| NEW | `src/update/standalone-owner.ts`, `src/update/standalone-stage.ts` | Resolve and bind the selected real executable and install owner; verify the signed exact target and recovery material; expose an owner-held stage without executing downloaded bytes. |
+| NEW | `src/update/standalone-replace.ts` and a signed recovery helper | Perform a same-owner replacement and rollback using existing mutation controls and the update-stop authority that `020` must first design and prove. On Windows, use a verified external helper that waits for the executing image to exit; no uncontained replacement or unverified helper. |
+| MODIFY | `src/update/orchestrated-plan.ts`, `src/update/orchestrated-entry.ts`, `src/cli/capability-contracts.ts` | Advertise a separate versioned automatic capability only after its owned standalone path is implemented; preserve existing `ocx.update.plan/1` and result behavior. |
+| MODIFY | `scripts/build-standalone.ts`, `desktop/scripts/verify-release-assets.ts`, release workflow within its existing permissions | Produce and verify signed target/recovery artifacts and the exact compatibility metadata. No release is published by this task. |
+| NEW | `tests/update/update-standalone-automatic.test.ts`, `tests/cli/cli-standalone-update-owner.test.ts` | Domain-registered, isolated fixtures for signed staging, owner changes, idle admission, process termination, crash at each swap, rollback and health. |
+| MODIFY | `structure/runtime.md`, `structure/ops/service-and-sidecars.md`, relevant release and user docs plus translations | Describe actual supported owners, platforms and recovery. |
+
+The standalone build is a multi-file installation: it includes the compiled
+executable, a target-specific native keyring addon and `gui/dist`
+(`scripts/build-standalone.ts:84`, `:95`). Replacing only the executable does
+not establish a matching signed release. The selected adapter owns an explicit
+managed installation directory containing versioned complete signed generations
+and a stable launcher/selection record; it verifies the complete inventory,
+including the addon and GUI, before a cutover. A legacy loose executable or
+foreign directory first needs an explicit owner-guided bootstrap into that
+layout; do not infer ownership or delete neighboring files. The bootstrap and
+its failure/recovery tests are part of this unit, not a deferred exclusion.
+
+Use the signed guardian and operation journal from `030`: stage an exact target
+generation and separately authenticated prior generation, acquire the `020`
+installation fence, prove runtime/process exit, and publish one durable
+generation selection under the owner. On POSIX, prove same-filesystem atomic
+publication and recovery of the selection; on Windows, the verified guardian
+waits for the executing image and observes its own replacement operation before
+readback. If the platform cannot establish selection atomicity or durable
+recovery, refuse that owner's automatic update and keep its parity row open.
+Validate the real launcher, selected generation, native addon, GUI and serving
+version together after restart; a matching executable version alone is not
+health. Tests cover loose-layout bootstrap, unrelated neighboring files,
+cross-volume refusal, interrupted selection, guardian death and signed prior
+generation restoration on every supported release target.
+
+## Activation and evidence
+
+- Valid signed exact-platform bytes stage in a temporary owner-controlled home;
+ missing/wrong signatures, wrong architecture, altered stage, ambiguous owner
+ and absent signed recovery material refuse before runtime stop.
+- Busy or unknown activity defers. A successful owner-bound stop precedes any
+ replacement, and the previous executable remains recoverable until target
+ health and the probation period complete.
+- Simulate interruption before/after each durable journal, rename or helper
+ boundary, lost response and surviving process. Verify either the previous
+ signed executable is restored or an honest uncertain state is retained;
+ never run a second installer on an unconfirmed outcome.
+- Exercise each supported OS/architecture through isolated tests and relevant
+ native CI. Require the separate security PASS, typecheck, privacy/structure,
+ Lab boundary, test-layout/file-size guards and exact-head CI. Commands and
+ platform-specific helper implementation remain to be concretized and audited.
diff --git a/devlog/_plan/261107_auto_update_parity/080_child_initiated_updates.md b/devlog/_plan/261107_auto_update_parity/080_child_initiated_updates.md
new file mode 100644
index 00000000000..3d1e6ee226c
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/080_child_initiated_updates.md
@@ -0,0 +1,108 @@
+# Child-initiated linked updates — draft unit
+
+**Status:** depends on the signed supply, destination grant and durable update
+operation in `060`. Home-initiated SSH alone does not cover this link direction.
+Do not treat the current `transport-unsupported` state as final parity.
+
+## Outcome and proposed transport
+
+For a Child-initiated link with no Home-to-Child SSH exec route, the proposed
+Child would poll its authenticated Home connection for a request addressed to
+its locally approved update grant. The existing passive management relay stays
+read-only. A separate versioned exchange would carry bounded signed requests
+and results; the Child would check its grant, policy, candidate, installer
+ownership and idle admission before any effect. Home could request but not
+approve or widen the destination grant. Existing link sync would remain a
+separate operation and status.
+
+The existing Home listener combines its route allowlists in
+`src/server/index/optional-listeners.ts:90` and admits the configured link key
+before dispatch (`src/server/index/serve-options.ts:387`). The Child's
+`fetchBoundLinkRelay` authenticates the Home and sends on the same socket
+(`src/client/link-relay-transport.ts:240`); the existing parity reporter is a
+bounded Child-to-Home POST example (`src/client/link-home.ts:344`). These are
+transport precedents, not update authorization. In particular, the Home
+management relay remains a separate read-only allowlist.
+
+Proposed exchange uses two exact Home link-listener routes, both POST with no
+query, upgrade or redirects: `/v1/link-update/poll` and
+`/v1/link-update/result`. Add an update-specific route predicate and dispatch
+sibling; do not add either route to `link-management` or the Child's local data
+ingress. Before the handler, retain the current link-key admission and require
+its configured key ID to match the current link record. The handler then
+verifies the separately enrolled destination response key and grant/binding
+epochs from `060`; link-key possession alone cannot select or acknowledge an
+operation. Unknown route, old peer, missing grant, revoked binding, stale key or
+unreadable state returns unavailable and causes no update effect.
+
+`ocx.link-update.poll/1` contains `requestId`, random `nonce`, `issuedAt`,
+`notAfter`, `childId`, `installationId`, `linkId`, `linkBindingEpoch`,
+`grantId`, `grantEpoch` and the latest observed operation ID/revision or null.
+The destination signs the exact bytes with its enrolled response key using a
+separate poll domain; the bounded envelope follows `060`. A valid response is
+`ocx.link-update.poll-result/1` with the poll request digest and nonce,
+destination/grant bindings, server time and either null or one already journaled
+Home-signed `ocx.link-update.envelope/1`. The Home signs the exact response
+bytes with its enrolled update key. The Child verifies the response, then the
+contained request's independent Home signature, action, expiry, local grant,
+policy, exact candidate and current binding before recording or performing it.
+The Home may not use the poll response to widen the destination grant.
+
+`/result` carries a destination-signed `ocx.link-update.result/1` from `060`,
+including the originating request digest, nonce, operation and revision; the
+Home stores it only after signature, admitted key, current binding and journal
+intent all match. Repeated identical results are idempotent; a conflicting
+revision is refused. Return only a bounded acknowledgement bound to that result
+digest. Limit each envelope and response to 64 KiB, decoded payload to 32 KiB,
+input/read time to five seconds, and signed request validity to five minutes.
+No endpoint returns local paths, credentials or arbitrary execution commands.
+
+The destination poller starts only for an active Child-initiated connection
+with a locally enabled grant. Use one in-flight poll, a 30-second normal
+interval and bounded 5/15/30/60-second retry backoff; stop on disconnect, runtime
+shutdown or revoked grant. Reconnect first reconciles its durable operation and
+binding before polling again. A lost poll response is retried with a fresh poll
+nonce; a delivered operation is deduplicated by its journal identity, not
+re-executed. Accepted installation work is handed to the same destination-owned
+worker and recovery contract as `060`, independent of the tunnel lifetime.
+No status changes to healthy until the destination observes the exact installed
+target and owned serving runtime. Exact worker startup and platform survival
+remain a proof requirement, especially for Windows channel containment.
+
+| Operation | Proposed path | Required change |
+| --- | --- | --- |
+| NEW | `src/link/update-exchange.ts`, `src/client/link-update-poller.ts` | Dedicated authenticated POST poll/result exchange and Child-owned bounded poller. The poll reads only a previously journaled request; result publication uses its own authorized operation, not management relay writes. |
+| MODIFY | `src/server/index/optional-listeners.ts`, `src/server/index/serve-options.ts` (extract an owned sibling if ratcheted), `src/remote/features.ts` | Admit the two exact exchange paths only on the authenticated Home link listener, dispatch with the configured key identity and advertise the capability only when implemented. Preserve existing connection-bound relay authentication and passive-read routes. Unknown peers and absent grants remain unavailable. |
+| MODIFY | Destination and fleet journals from `060` | Persist request intent and outcome across Home/Child restart; reconcile the same operation after response loss, expiry or reconnect. Recheck binding epochs, local policy and owner before commit. |
+| MODIFY | `src/server/management/link-update-routes.ts`, `gui/src/remote-link-update-api.ts`, `gui/src/components/remote-link-update-panel.tsx` | Project Child-pull operations through the same `060` versioned dashboard status/action/readback DTOs; add old-peer, disconnect, reconnect, Retry A→B with lost reply and uncertain-result cases without granting the passive relay write authority. |
+| NEW | `tests/clients/link-update-child-initiated.test.ts`, `tests/server/link-update-exchange.test.ts` | Isolated linked homes, fake transports and signed test artifacts: no grant, stale link, duplicate delivery, lost responses, busy/unknown runtime, cancellation, crash recovery and separate sync results. Register both domain files in the two test-layout maps. |
+| MODIFY | `structure/remote-link.md`, `structure/remote-messaging.md`, relevant user docs and all affected translations | Record implemented transport, policy, prerequisites and actionable unsupported cases. |
+
+## Activation and evidence
+
+Prove that a Child with no locally approved grant receives no installation
+authority, that Home cannot change Child policy, and that duplicate or late
+requests cannot start a second install. A signed exact candidate and future
+compatibility proof are necessary but not sufficient: the Child independently
+rechecks its own owner, current policy and idle reservation. A reconnect or
+credential change invalidates stale operation bindings. Network loss leaves
+accepted work under its destination-owned recovery; status is not reported
+healthy until exact target and runtime health are observed.
+
+Use fake links, isolated homes, test keys and a local mock feed. Run failing-
+first focused protocol, grant, replay and recovery tests; then typecheck,
+privacy/structure, Lab boundary, layout and exact-head CI. Obtain a separate
+independent security PASS. The exchange uses the `060` `U → O → A → N → L → C
+→ J` order: poll/result network I/O holds none of those locks; the destination
+checks and commits journal/binding state in short transactions and reacquires
+activation authority before mutation. This proposed order and destination
+worker survival still require call-graph, crash and platform proof before audit.
+
+Baseline on 2026-11-07: `bun test tests/server/sidebar-routes.test.ts
+tests/server/link-management-relay.test.ts` passed 31 tests (exit 0). The relay
+suite starts isolated loopback listeners and verifies that existing management
+reads remain passive and reject writes. It does not exercise the proposed
+update exchange. The two new test files in the file map do not exist yet; run
+them with `bun test tests/clients/link-update-child-initiated.test.ts
+tests/server/link-update-exchange.test.ts` only after creating and registering
+them, and record initial red and final green separately.
diff --git a/devlog/_plan/261107_auto_update_parity/090_pnpm_updates.md b/devlog/_plan/261107_auto_update_parity/090_pnpm_updates.md
new file mode 100644
index 00000000000..8693a970495
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/090_pnpm_updates.md
@@ -0,0 +1,112 @@
+# Signed pnpm-owned updates — draft unit
+
+**Status:** follows the common signed supply, idle admission and npm-owned
+stage/recovery work. The existing pnpm updater mutates its selected global
+owner through pnpm and uses pnpm again for rollback
+(`src/update/pnpm-global-install.mjs:464`, `:513`, `:564`). A signed npm-shaped
+tree cannot be renamed into pnpm's virtual store and treated as pnpm ownership.
+
+## Proposed implementation slice
+
+1. Add `src/update/pnpm-prepared-install.mjs` and declaration sibling. Resolve
+ the existing `PnpmGlobalOwner`, its global root, bin root, virtual-store and
+ lock inputs through the existing owner helpers; refuse ambiguous or changed
+ ownership. Bind those facts and the exact signed candidate to an owner-held
+ operation record.
+2. Build a disposable pnpm global layout from the verified local signed
+ dependency closure with lifecycle scripts disabled and network unavailable.
+ Compare all resulting package/store entries and generated bin links against
+ the signed inventory and the selected owner. Never write to the live global
+ group during preparation. The exact pnpm invocation and relocatable layout
+ are an explicit experiment and audit blocker, not a presumed supported flag.
+3. Before activation, reacquire the existing home sync/update guard and service
+ ownership lease. Obtain installation-wide coordination only after `020`
+ designs and proves it; revalidate policy, stage and selected pnpm owner, then use
+ the `020` update-stop reservation. Use a manager-mediated selected-owner
+ transition, journaled before each operation; do not describe it as an atomic
+ group/bin swap. Bind and verify the generated shims, selected final store,
+ complete publisher inventory and process exit before declaring installation.
+ Keep signed previous archives, store identity and metadata for recovery until
+ target health and probation complete.
+4. Extend `src/update/orchestrated-pnpm.mjs` and the automatic coordinator with
+ a separately versioned prepared operation; preserve the existing explicit
+ pnpm command and strict output schemas while requiring verified supply.
+ Unsupported versions/layouts refuse
+ with an actionable state rather than falling back to an online install.
+
+Modify `tests/update/update-pnpm.test.ts` and add domain-registered
+`tests/update/update-pnpm-prepared.test.ts` plus any required platform fixture.
+Use temporary global roots, a local test key, fake or isolated manager process
+and network-denial assertions. Exercise missing/altered artifacts, lifecycle
+script refusal, symlinks/shims, changed global owner, uncertain process exit,
+concurrent sync, crash before/after group transition, verified rollback and
+health. No test touches the real global installation.
+
+Update `structure/ops/service-and-sidecars.md`, other mapped owners and user
+docs/translations. Run red-to-green focused tests, typecheck, privacy/structure,
+test-layout/file-size and Lab boundary checks, relevant native/CI coverage,
+and a separate independent security PASS. Actual supported pnpm versions,
+offline preparation command, platform layout, writer exclusion and durable
+manager-mediated transition must be proved before automatic activation.
+
+## Isolated feasibility observation
+
+On 2026-11-07, the host's pnpm reported `12.18.0`. A macOS `sandbox-exec`
+fixture denied network and writes outside its private temporary root; separate
+probes confirmed those denials. Adding a local inert fixture with
+`--global --ignore-scripts --ignore-pnpmfile` and a fixture-owned store did not
+run its postinstall. The first attempt refused because its isolated bin path
+was not on `PATH`; after correcting that path, pnpm exited 1 with
+`ERR_PNPM_STORE_DIR_OPEN_OPERATION_LOCK` / `Operation not permitted`, including
+when the fixture state directory was supplied. No real global install was
+touched. This shows only that this first sandbox configuration did not complete
+the operation; it does **not** establish a pnpm defect or prove pnpm cannot
+stage offline. A later run with fixture-owned `XDG_RUNTIME_DIR`, store and state
+paths exited 0 without running the postinstall. The decisive configuration
+change and original lock location were not established.
+
+A separate probe used the already-installed pnpm `11.10.0` directly, with a
+fresh fixture-owned global home/store, `--global --offline --ignore-scripts
+--ignore-pnpmfile`, and the same network/outside-write sandbox. It exited 0,
+and the inert fixture's postinstall sentinel remained absent. The resulting
+global group contained a package symlink into the separate fixture store and
+generated bin files. This is positive evidence for one local no-dependency
+fixture only: it does not establish relocatability, signed dependency closure,
+atomic live-group activation or recovery. A further pnpm 11 fixture changed
+versions 1.0.0 → 1.1.0 → 1.0.0 offline with the sentinel absent. Copying its
+global home without the separate store broke the package link; its generated
+bin script still pointed into the original fixture. These observations do not
+prove a safe relocation or a crash-safe live update.
+
+Pre-audit research investigated a manager-supported transition and recovery
+with a locally signed dependency fixture. Exact commands, publisher-signed
+versus manager-generated bytes, store/bin ownership, interruption and cleanup
+evidence are retained under `.tmp/`; no real install was touched.
+
+The follow-up isolated investigation on macOS arm64 with pnpm 11.10.0 used a
+signed local two-package fixture, a private store kept at its final path and a
+selected-owner offline manager re-add. Preparation, activation and restoration
+completed in that fixture; three controlled process interruptions exposed
+intermediate group/bin states and the previous signed fixture was subsequently
+restored. This selects manager-mediated re-add plus a durable owner-held journal
+as the implementation direction, **not** an atomic switch or proof of safe
+production activation. Bind exact signed prior/candidate archives and store,
+global and bin identities, recheck policy/owner/idle, prove process-tree exit,
+verify the complete installed graph and generated wrappers before any candidate
+execution, and reconcile every interrupted transition before another installer.
+Require installation-scoped exclusion from `020`, safe owned cleanup, verified
+recovery, real OpenCodex executable closure and platform/version tests. No
+unproven layout is automatically eligible; the detailed negative evidence
+remains in the private research packet.
+
+For the observed pnpm 11.10.0 fixture, classify the global manifest, lock,
+`.modules.yaml`, manager index and global/internal bin wrappers as generated
+state, separate from signed package files. The adapter permits only the
+version-specific schema and exact wrapper templates it has tested, with each
+resolved command and symlink bound to the selected final store and signed
+payload. Other pnpm versions, store formats, wrapper forms or platforms remain
+unavailable until their own fixtures prove the same contract. Persist the
+validated generated-state digest for drift/recovery readback; do not accept
+unknown files merely because they are under the store. Test changed metadata,
+unexpected executable/link, owner path changes and manager-specific recovery
+without executing candidate code until the full output verifies.
diff --git a/devlog/_plan/261107_auto_update_parity/100_bun_updates.md b/devlog/_plan/261107_auto_update_parity/100_bun_updates.md
new file mode 100644
index 00000000000..5d7bd5a7616
--- /dev/null
+++ b/devlog/_plan/261107_auto_update_parity/100_bun_updates.md
@@ -0,0 +1,93 @@
+# Signed Bun-owned updates — draft unit
+
+**Status:** follows the common signed supply, idle admission and npm-owned
+stage/recovery work. The current Bun-owned update runs a global add
+(`src/update/index.ts:354`); that is not evidence of independently staged,
+publisher-verified activation or rollback.
+
+## Proposed implementation slice
+
+1. Add `src/update/bun-prepared-install.ts` and an owner record for the selected
+ Bun global package root, bin links, global manifest/lock state and exact
+ signed candidate. Use `detectInstallOwnership` and fail closed on unknown,
+ foreign or changed ownership (`src/update/orchestrated-plan.ts:21`).
+2. Materialize the verified signed executable closure into a disposable Bun
+ global layout with scripts disabled and network denied. Bun's installed
+ help exposes `--offline` and `--ignore-scripts`, but the exact invocation,
+ global metadata behavior and relocation semantics must be proven in isolated
+ fixtures before they are claimed as supported. Validate every installed file
+ and link against signed inventory; preparation must not write to the live
+ global tree or execute a downloaded helper.
+3. At activation reacquire the existing home sync/update guard and service
+ ownership lease. Obtain installation-wide coordination only after `020`
+ designs and proves it; revalidate stage, policy and owner, then obtain the `020`
+ idle update-stop reservation. Journal a manager-mediated selected-root
+ transition and verify its package, metadata and bin links before execution;
+ do not claim that the manager changes those files atomically. Retain the
+ signed previous installation until health and probation complete.
+4. Extend `src/update/orchestrated-entry.ts` and the automatic coordinator with
+ a separately versioned prepared operation. Preserve current explicit update
+ output and existing ownership/supervisor refusals while requiring verified
+ supply for installation. Never fall back to an
+ unverified global add when offline preparation or rollback cannot be proven.
+
+Add domain-registered `tests/update/update-bun-prepared.test.ts` and use
+isolated homes, a signed test closure, fake manager workers and network denial.
+Cover changed owner, global lock/bin drift, script and unsigned dependency
+refusals, expired stage, interrupted worker, rollback after each mutation,
+health readback and concurrent sync. Update `structure/runtime.md`,
+`structure/ops/service-and-sidecars.md` and affected user docs/translations.
+
+Run failing-first focused tests, typecheck, privacy/structure, test-layout and
+file-size gates, the Lab boundary test, relevant platform CI and a separate
+independent security PASS. Actual Bun global layout, cache isolation,
+manager-compatible durable transition and signed rollback remain proof blockers
+before this unit can be enabled.
+
+## Isolated feasibility observation
+
+On 2026-11-07, Bun `1.4.4` installed an inert local fixture into an isolated
+`BUN_INSTALL` root with `--global --offline --ignore-scripts --no-env-file`;
+the command exited 0 and the fixture's postinstall sentinel was absent. A
+macOS `sandbox-exec` profile denied network and writes outside the temporary
+root, and separate probes confirmed those denials. The observed output included
+a global manifest/lock, a nested package directory and a relative bin symlink.
+This proves only that one local package can be prepared without its lifecycle
+script in that fixture. A separate offline fixture round trip changed versions
+1.0.0 → 1.1.0 → 1.0.0 with the sentinel absent. Copying the global root
+retained an absolute `file:` source in its manifest, so naïve relocation was
+not established. These experiments do not prove signed dependency closure,
+atomic activation, verified recovery or runtime health.
+
+Pre-audit research investigated a manager-supported transition and recovery
+with a locally signed dependency fixture. Exact commands, publisher-signed
+versus manager-generated bytes, root/bin ownership, interruption and cleanup
+evidence are retained under `.tmp/`; no real install was touched.
+
+The follow-up isolated macOS arm64 investigation with Bun 1.4.4 used a signed
+local two-package fixture and showed offline selected-owner install and
+restoration, plus `--lockfile-only` preparation followed by frozen
+materialization. A separate Bun 1.4.3 fixture did not satisfy the selected-root
+contract; it does not establish a general supported-version floor. Interrupted
+and competing manager operations showed that package, metadata and bin changes
+cannot be treated as one atomic transition. The selected direction is a
+journaled manager-mediated operation under the installation-scoped exclusion in
+`020`, with exact signed prior/candidate inputs, bound root/bin/cache identities,
+complete output/link verification before execution and verified recovery.
+Resolve custom global roots explicitly, preserve unrelated global packages and
+retain operation-owned recovery material through probation. Prove process
+termination, every relevant interruption boundary, cross-platform behavior and
+cleanup before enabling the owner. A lockfile, successful manager exit or this
+fixture is not authentication or crash-safety proof; detailed negative
+evidence remains in the private research packet.
+
+For the observed Bun 1.4.4 fixture, classify global manifest/lock, `.bun-tag`,
+cache metadata and bin links as manager-generated state, separate from signed
+package files. The adapter accepts only version-specific parsed schemas and
+expected link targets bound to the selected root/bin/cache and verified
+payload; no unrecognized generated executable, metadata form or escaping link
+is accepted. A recorded digest detects later drift but does not turn local
+metadata into a publisher signature. Bun 1.4.3 and all untested layouts remain
+unavailable until separately proven. Test foreign entries, custom roots,
+changed bins, unrelated global packages and interrupted recovery before any
+candidate executable is launched.
diff --git a/devlog/_plan/261107_ocx_child_ephemeral_link/000_roadmap.md b/devlog/_plan/261107_ocx_child_ephemeral_link/000_roadmap.md
new file mode 100644
index 00000000000..79f9ec451c9
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_ephemeral_link/000_roadmap.md
@@ -0,0 +1,116 @@
+# Ephemeral Rust Child Link (`native/ocx-child`) Eliminates Full-Proxy Child Footprint While Preserving Exact Link, Sync, and Offline Restore Parity
+
+> Reader contract: For OpenCodex maintainers and reviewers evaluating Remote Link Child architecture, security boundaries, and phased execution on `feat/ocx-child-ephemeral-link` targeting `dev`. Document type: Decision & Roadmap Specification (`000`-range, zero implementation diffs per `LEXICO-SPLIT-01`).
+
+Remote Link today requires every Child machine (`src/server/management/link-enrollment.ts:147-180`) to pre-install and run a full standalone Bun OpenCodex proxy (~100-217 MB runtime footprint) and recycles back into a persistent standalone daemon on disconnect (`src/client/runtime.ts:133`), which is unsuitable for disposable SSH devboxes, containers, and lightweight VMs. How can OpenCodex attach a Remote Link Child with a megabyte-scale, zero-Bun/zero-Node footprint that relays inference/SSE traffic, participates byte-for-byte in Home `ocx sync` fan-out and OCX-6 parity, and restores native `~/.codex` state 100% offline with zero leftover daemon? We introduce `native/ocx-child` (standalone Rust crate compiling to a stripped ~1-2 MB `ocx-child` binary) paired with minimal Home-side discovery and detached-preflight support in `src/link/` and `src/server/management/`, locked to TypeScript behavior via a shared golden conformance fixture (`tests/fixtures/link/ocx-child-conformance.json`).
+
+## Contents
+
+1. [Loop Specification & HOTL Bounds](#1-loop-specification--hotl-bounds)
+2. [Scope](#2-scope)
+3. [Architect Consultation & Design Dispositions](#3-architect-consultation--design-dispositions)
+4. [Dependency-Ordered Work-Phase Map (`PHASE-SPLIT-01`)](#4-dependency-ordered-work-phase-map-phase-split-01)
+5. [Cross-Boundary Field & Type Chains (`PLAN-FIELD-CHAIN-01`)](#5-cross-boundary-field--type-chains-plan-field-chain-01)
+6. [Verification scope](#6-verification-scope)
+7. [Grounded Verifiers (`PLAN-VERIFIER-REAL-01`)](#7-grounded-verifiers-plan-verifier-real-01)
+8. [Non-Goals, Alternatives Rejected, & Consequences](#8-non-goals-alternatives-rejected--consequences)
+
+## 1. Loop Specification & HOTL Bounds
+
+| Field | Specification |
+|---|---|
+| Loop archetype | `satisfy-spec` (Class C4 multi-cycle HOTL PABCD with docs-first `wp0` entry per `LOOP-DOCS-FIRST-01`) |
+| Trigger | Dispatched GOAT task lane `lane-ocx-child-ephemeral-link` on branch `feat/ocx-child-ephemeral-link` + user steering requiring seamless `ocx sync` fan-out, OCX-6 runtime freshness/parity, and full sibling CLI compatibility |
+| Goal | Deliver `native/ocx-child`, Home-side ephemeral-child Remote Link integration, shared TS/Rust golden conformance vectors, updated `structure/` and `docs-site/` documentation, and a verified unmerged PR targeting `dev` |
+| Non-goals | Merging the PR (`authority.merge = false`), force-pushing, dispatching manual `lane=all` CI, mutating `.codexclaw` subagent configs, editing `.github/` workflows, touching `src/lab/` or `core-link-boundary` protected files (`src/router.ts`, `src/server/lifecycle.ts`, `src/server/responses/core.ts`), raising file-size ratchets, or implementing Child-initiated (`ssh -L` `join`) / web-dashboard modes inside `ocx-child` |
+| Verifier | Baseline & gate commands enumerated in [Section 7](#7-grounded-verifiers-plan-verifier-real-01): `bun run typecheck`, `cargo test --locked --manifest-path native/ocx-child/Cargo.toml`, `cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml`, focused Bun conformance/unit/E2E suites, `bun run test:changed`, `bun run structure:check`, `cd docs-site && bun run build`, and `bun run privacy:scan` |
+| Stop condition | All work-phases `wp0..wp4` and criteria `c-1..c-6` met with fresh command receipts, PR opened against `dev` and linked via `goat_pull_request_link`, exact-head PR checks green and recorded via `cxc loop pr-record`, `goat_pull_request_unwatch` called, and `update_goal({ status: "complete" })` accepted |
+| Memory artifact | `devlog/_plan/261107_ocx_child_ephemeral_link/` (`000_roadmap.md` + `010..040` decade PRDs) and `.codexclaw/goalplans/loop-archetype-satisfy-spec-class-c4-hotl-multi/goalplan.json` |
+| Expected terminal outcomes | `DONE`: all `c-1..c-6` verified + green unmerged PR recorded; `NOOP`: N/A (greenfield subsystem); `BLOCKED`: reproducible toolchain/CI/policy refusal reported with `file:line` and command output without weakening gates; `UNSAFE`: unmitigated credential/smuggling/restore defect; `NEEDS_HUMAN`: out-of-scope permission needed; `BUDGET_EXHAUSTED`: host limit reached |
+| Escalation condition | Any requirement to edit outside `writeScope` (`native/ocx-child`, `src/link`, `src/server/management`, `src/cli`, `structure`, `docs-site`, `scripts`, `tests`, `package.json`, `devlog`), weaken a check, or merge the PR. Subagent recovery follows `DISPATCH-RETIRE-01` (inherit `google-antigravity/gemini-4-argon` per user directive) |
+| HOTL resource bounds | Token/cost/wall-clock bounds: `none stated; host limits apply`. Tool/credential scope: local git/bun/cargo, `gh` PR creation/inspection on `feat/ocx-child-ephemeral-link`, GOAT PR link/watch/unwatch |
+
+## 2. Scope
+
+Security working analysis is kept outside the tracked repository.
+
+## 3. Architect Consultation & Design Dispositions
+
+- **Proposals consulted**:
+ - Initial read-only architect handle `01a1b183-8618-7280-bcc9-f1da84898e7d` (retired after wait timeout, completed text captured on `close_agent`) and replacement live read-only architect handle `01a1b18b-a2f3-7b53-971a-9b2c9cccecf7` (both inherited `google-antigravity/gemini-4-argon`).
+- **Main dispositions on `ARCH-01..ARCH-07`**:
+ - **`ARCH-01` (Amended)**: Adopt standalone `native/ocx-child` crate with `std::net` + bounded `std::thread` worker pool, surgical line/span TOML module, and minimal audited dependencies (`serde`, `serde_json`, `sha2`, `hmac`, `base64`, Unix `libc`, Windows `windows-sys`) matching `native/remote-workspace-helper` and `desktop/src-tauri`. Reject `tokio`/`hyper`/`rustls`/`toml_edit` because `ocx-child` speaks loopback HTTP/1.1 only (never TLS), keeping dependency count minimal and stripped release binary ~1-2 MB.
+ - **`ARCH-02` (Amended)**: Adopt strict HTTP/1.1 framing + `linkRouteAllowed` / `linkManagementRouteAllowed` + `CALLER_CREDENTIAL_HEADERS` stripping + `/healthz` + `/readyz` + `child-parity` reporter. Reject upstream socket pooling: Home's `src/server/index/link-relay-sessions.ts:105` consumes each `nonce` once (`consume(nonce)`), so each relayed request opens one `TcpStream` and runs `preflight 204 -> single data request` on that exact socket, matching `SingleConnectionAgent` (`src/client/link-relay-transport.ts:41`).
+ - **`ARCH-03` (Amended)**: Adopt `0600` journal + ordered locks (`$OPENCODEX_HOME/child/lifecycle.lock` -> `$CODEX_HOME/.opencodex-write.lock`) + fail-closed checks (full-`ocx` runtime/journal, foreign listener, external `model_provider`, user-owned root `openai_base_url`) + two-tier offline detach (byte-exact restore when `untouchedByExternalWriter` holds across attaches/syncs; surgical value-matched strip surviving `#1798` comment-stripping rewrites; fail-closed exit `4` on ambiguous/conflicting edits). Reject `[model_providers.opencodex]` table injection: scope strictly to Design B loopback root keys + Home `codex-settings` (`[features]`, `[agents]`, `web_search`) + `models_cache.json` refresh with websockets disabled.
+ - **`ARCH-04` (Amended)**: Adopt unified native (`attach`, `sync`, `detach`, `serve`, `status`, `port`) and `ocx`-compatible (`--version` -> `opencodex 2.98.0`, `link port`, `config show --json`, `connect --link --key-stdin`, `restart`, `disconnect`, `sync [--expect-link ] [--json]`, `status [--json]`, `connect status [--json]`) CLI verbs. Enforce exact `src/cli/sync-expectation.ts:55` stderr mismatch JSON `{"phase":"child_link_mismatch","reason":"..."}` with exit `5` (including `pending_operation` on lock contention) and post-sync live daemon rollover + immediate `child-parity` POST.
+ - **`ARCH-05` (Amended)**: Adopt ordered remote candidate fallback (all full-`ocx` paths first, then `ocx-child` in `~/.local/bin`, `~/.cache/opencodex/bin`, Windows `%LOCALAPPDATA%\opencodex\bin`, and `PATH`, keeping Windows encoded PowerShell `< 8000` chars) and detached `ocx-child` preflight in `src/server/management/link-enrollment.ts`. Reject adding new fields to persisted `LinkRecord` in `links.json` (`src/link/store.ts:16` enforces closed `exactKeys`, and `remoteOcxArgv(["disconnect"])` + `remotePinnedSyncArgv` already operate uniformly once `ocx-child` is in candidate discovery).
+ - **`ARCH-06` (Accepted)**: Shared golden conformance fixture `tests/fixtures/link/ocx-child-conformance.json` (`schema: "ocx.child.conformance/1"`) executed by both `bun test` and `cargo test`.
+ - **`ARCH-07` (Accepted)**: Place new TS helper at `src/link/ephemeral-child.ts`, keep zero edits in `src/cli/index.ts` or any `file-size-baseline.json` file, keep `core-link-boundary` and `core-lab-boundary` untouched, register all new TS tests in `scripts/test-layout/layout-clients.json` + `tests/fixtures/test-layout-expected.json`, and update `structure/remote-link.md` + `docs-site/`.
+- **Same-architect reflection (`01a1b18b-a2f3-7b53-971a-9b2c9cccecf7`)**:
+ - Submitted concrete plan revision: `devlog/_plan/261107_ocx_child_ephemeral_link/{000_roadmap.md,010_wp1_conformance_contract.md,020_wp2_rust_ocx_child.md,030_wp3_home_link_integration.md,040_wp4_docs_gates_and_pr.md}` (recorded in Section 3.1 immediately after reflection response).
+
+### 3.1 Reflection Record
+
+- **Handle `01a1b18b-a2f3-7b53-971a-9b2c9cccecf7` reflection verdict**: `ALIGNED`.
+- **Decision-ID mapping verified**: `ARCH-01` -> `000` §3 + `020` §1.1–§1.2; `ARCH-02` -> `000` §3 + `020` §1.2.3–§1.2.4; `ARCH-03` -> `000` §3, §5 + `020` §1.2.5; `ARCH-04` -> `000` §3, §5 + `020` §1.2.6; `ARCH-05` -> `000` §3, §8 + `030` §1.1–§1.3; `ARCH-06` -> `000` §5 + `010` §1.2–§1.3 + `020` §1.3; `ARCH-07` -> `000` §3, §7 + `010` §1.1 + `030` §1.5 + `040` §1.1–§1.2.
+- **Minor notes folded into `020_wp2_rust_ocx_child.md`**:
+ - `Gap-1` (Accepted): `ChildJournalV1` snapshots `preAttachModelsCacheExisted` / `preAttachModelsCacheBytesB64` / `latestModelsCacheSha256` so offline `detach` restores pre-attach `models_cache.json` (or unlinks an unchanged cache `ocx-child` created) and never leaves stale Home slugs in Codex's model picker.
+ - `Gap-2` (Accepted): `native/ocx-child/Cargo.toml` includes `Win32_Security_Authorization` on `windows-sys` and `write_private_atomic` on Windows hardens `$OPENCODEX_HOME/child` and its `0600`-equivalent secret/journal files with an owner-only protected DACL.
+
+## 4. Dependency-Ordered Work-Phase Map (`PHASE-SPLIT-01`)
+
+Phases follow strict dependency order (shared wire/golden contracts -> standalone Child binary -> Home orchestration & E2E integration -> SoT documentation & publication):
+
+| Work Phase | Decade PRD | Depends On | Deliverable & Independent Verification Closure |
+|---|---|---|---|
+| `wp0` | `000_roadmap.md` + `010..040` | — | Complete roadmap and diff-level PRDs audited and closed with zero production edits (`LOOP-DOCS-FIRST-01`) |
+| `wp1` | `010_wp1_conformance_contract.md` | `wp0` | `src/link/ephemeral-child.ts` pure helpers + `tests/fixtures/link/ocx-child-conformance.json` + `tests/clients/link-ocx-child-conformance.test.ts` registered in dual test-layout maps; passes `bun test` + `bun run typecheck` |
+| `wp2` | `020_wp2_rust_ocx_child.md` | `wp1` | `native/ocx-child/` crate (`Cargo.toml`, `Cargo.lock`, `src/**`, `tests/conformance.rs`, `tests/relay_and_lifecycle.rs`) passing `cargo test --locked`, `cargo clippy --locked --all-targets -- -D warnings`, and `cargo build --release --locked` (~1-2 MB stripped binary) |
+| `wp3` | `030_wp3_home_link_integration.md` | `wp2` | Home candidate discovery (`src/link/ssh-argv.ts`, `src/link/remote-windows.ts`), detached `ocx-child` enrollment preflight (`src/link/remote-probe.ts`, `src/server/management/link-enrollment.ts`), `package.json` scripts/files, and focused + real-binary E2E tests (`tests/clients/link-ephemeral-child-discovery.test.ts`, `tests/server/link-enrollment-ephemeral-child.test.ts`, `tests/clients/ocx-child-e2e.test.ts`) |
+| `wp4` | `040_wp4_docs_gates_and_pr.md` | `wp3` | `structure/manifest.json` + `structure/remote-link.md` + `structure/overview.md` (`INV-LINK-01` or bound invariant) + `docs-site/` updates, full verification matrix, branch push, unmerged PR targeting `dev` with Security Review Summary, `goat_pull_request_link`, green checks, and `cxc loop pr-record` |
+
+Single-PR delivery decision (`DEV-STACK-01`): Delivered as a single focused PR targeting `dev` because `native/ocx-child`, its golden conformance vectors, and the ~60-line Home discovery/preflight integration form one cohesive feature slice within the lane's authorized branch `feat/ocx-child-ephemeral-link`.
+
+## 5. Cross-Boundary Field & Type Chains (`PLAN-FIELD-CHAIN-01`)
+
+| Field / Schema | Creation | Serialization | Deserialization | Consumers |
+|---|---|---|---|---|
+| `ephemeralChild: true` on Child `config show --json` (`EphemeralChildConfigProjection`) | `native/ocx-child/src/cli.rs` (`cmd_config_show`) | `serde_json::to_string` on `ocx-child` stdout over SSH | `JSON.parse` in `src/server/management/link-enrollment.ts` -> `isDetachedEphemeralChildConfig` in `src/link/ephemeral-child.ts` | `src/server/management/link-enrollment.ts` (`apply` preflight branching between live standalone `ocx` vs clean detached `ocx-child`); full `ocx` omits field -> defaults to existing standalone branch |
+| `ephemeralChild: true` on Child `GET /healthz` | `native/ocx-child/src/relay.rs` (`handle_healthz`) | `serde_json::to_vec` HTTP `200` body | `parseChildHttp` (`src/link/remote-probe.ts:30`) / `Response.json()` in `src/cli/link-apply-restart.ts:36` & `src/server/proxy-liveness.ts` | `isChildHealth(..., "client", childPort)` (`src/link/remote-probe.ts:35` ignores extra fields and verifies `service`, `port`, `pid`, `role === "client"`), `readClientRuntimeStartedAt`, E2E/diagnostic callers |
+| `ChildStateV1` (`schema: "ocx.child.state/1"`) | `native/ocx-child/src/state.rs` (`write_state_atomic`) | `serde_json::to_vec_pretty` into `$OPENCODEX_HOME/child/state.json` (`0600`) | `serde_json::from_slice` (`#[serde(deny_unknown_fields)]`) in `native/ocx-child/src/state.rs` | Internal to `ocx-child` (`serve`, `sync`, `detach`/`disconnect`, `restart`, `status`, `config show`); N/A outside `ocx-child` because full `ocx` uses `config.json` and fail-closed guards forbid mixing |
+| `ChildJournalV1` (`schema: "ocx.child.journal/1"`) | `native/ocx-child/src/journal.rs` (`create_or_update_journal`) | `serde_json::to_vec_pretty` into `$OPENCODEX_HOME/child/codex-journal.json` (`0600`) | `serde_json::from_slice` (`#[serde(deny_unknown_fields)]`) in `native/ocx-child/src/journal.rs` | `ocx-child` `sync` (updates `latestInjectedConfigSha256`, `ledger`, `latestCatalogSha256`) and offline `detach`/`disconnect` (Tier-1 hash restore or Tier-2 surgical value strip); also conformance harness in `native/ocx-child/tests/conformance.rs` |
+| `ConformanceSuite` (`schema: "ocx.child.conformance/1"`) | Checked-in JSON fixture `tests/fixtures/link/ocx-child-conformance.json` verified against TS oracle functions | Static UTF-8 JSON file in git | `JSON.parse` in `tests/clients/link-ocx-child-conformance.test.ts` AND `serde_json::from_str` in `native/ocx-child/tests/conformance.rs` | Both `bun test` and `cargo test` conformance runners asserting identical outputs across route admission, framing/stripping, HMAC v2, OCX-6 digest, child-parity validation, and TOML/catalog round-trips |
+| `ClientSyncMismatchReason` stderr JSON (`{"phase":"child_link_mismatch","reason":"..."}`) | Existing closed union in `src/client/sync-expectation.ts:17` (no new enum members added); emitted by `native/ocx-child/src/cli.rs` on `--expect-link` mismatch/busy lock | `serde_json::to_string` as final stderr line with exit code `5` | `JSON.parse(last ?? "")` in `src/cli/sync-fanout.ts:62` checking `mismatchReasons` set | Home `ocx sync [--json]` fan-out classifier (`classifySyncResult` -> `skipped("child-link-mismatch:")`) |
+
+## 6. Verification scope
+
+The work packages record implementation and conformance-test status.
+
+## 7. Grounded Verifiers (`PLAN-VERIFIER-REAL-01`)
+
+Every existing repository gate below was executed on the clean worktree before authoring this roadmap:
+
+| Verifier Command | Baseline Exit Today | How It Reads This Unit's Change Target |
+|---|---|---|
+| `bun run typecheck` (`bun check --no-script --noEmit`) | `0` (`2,318 files`) | `tsconfig.json` includes `src/**/*.ts`, `scripts/**/*.ts`, and `tests/**/*.ts`, directly typechecking `src/link/ephemeral-child.ts`, `src/server/management/link-enrollment.ts`, and all new `tests/**/*.test.ts` |
+| `bun scripts/structure-ssot.ts` (`bun run structure:check`) | `0` | Reads `structure/manifest.json`, `structure/INDEX.md`, `structure/overview.md`, `structure/remote-link.md`, and `git ls-files` (`scripts/structure-ssot.ts:315-635`), validating `src/link/` & `native/ocx-child/` path references and invariant bindings |
+| `bun test tests/test-layout.test.ts tests/test-layout-tooling.test.ts` | `0` (`28 pass`) | Walks `tests/**/*.test.ts`, `scripts/test-layout/layout.json` + `explicitShards` (`scripts/test-layout/layout-clients.json`), and `tests/fixtures/test-layout-expected.json` (`tests/test-layout-tooling.test.ts:28,287`) |
+| `bun test tests/lab/core-lab-boundary.test.ts tests/lab/core-link-boundary.test.ts` | `0` | Walks static imports from `src/router.ts`, `src/server/lifecycle.ts`, and `src/server/responses/core.ts` (`tests/lab/core-link-boundary.test.ts:9-48`) |
+| `bun test tests/clients/link-pinned-sync-argv.test.ts tests/clients/link-ssh-argv.test.ts tests/clients/link-ssh-argv-discovery.test.ts` | `0` | Directly imports `src/link/ssh-argv.ts` and `src/link/remote-windows.ts` and executes `REMOTE_OCX_SCRIPT`, `REMOTE_PINNED_SYNC_SCRIPT`, `windowsOcxArgv`, and `windowsPinnedSyncArgv` (`POWERSHELL_ENCODED_MAX = 8000`) |
+| `bun run privacy:scan` (`bun scripts/privacy-scan.ts`) | `0` (verified in `wp0` Check) | Scans tracked git files across `devlog/`, `src/`, `native/`, `tests/`, and `docs-site/` |
+| `cargo test --locked --manifest-path native/ocx-child/Cargo.toml` & `cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml` | Phase-created in `wp2` (`rustc 1.99.0` / `cargo 1.99.0` verified present) | Compiles and executes `native/ocx-child/src/**` and `native/ocx-child/tests/**` directly from `native/ocx-child/Cargo.toml` |
+| `cd docs-site && bun install --frozen-lockfile && bun run build` | Executed in `wp4` when `docs-site/` changes | Astro/Starlight build reading `docs-site/src/content/docs/guides/remote-link.md` and `reference/cli/lifecycle.md` |
+
+## 8. Non-Goals, Alternatives Rejected, & Consequences
+
+- **Non-goals**:
+ - SCP/SFTP binary auto-upload from Home during `ocx link add` (`ocx-child` is discovered on the remote host like `ocx`).
+ - Child-initiated (`POST /api/link/join` / local `ssh -L` tunnel supervisor) or local web dashboard on `ocx-child`.
+ - Provider-table (`[model_providers.opencodex]`) auth-header injection on `ocx-child` (loopback Design B needs no local bearer token).
+- **Alternatives rejected**:
+ - *Compiling `src/` with `bun build --compile` for Children*: Still ~100-217 MB and embeds the full proxy/GUI/provider runtime.
+ - *Async `tokio` + `hyper` + `toml_edit` Rust stack*: Adds ~60+ crates and megabytes of binary weight for loopback-only HTTP/1.1 streams and line-oriented TOML edits.
+ - *Adding `childKind` to `links.json`*: Breaks strict `exactKeys` compatibility in `src/link/store.ts:16` on downgrade while providing zero benefit once `remoteOcxArgv` and `remotePinnedSyncArgv` discover `ocx-child` in fallback order.
+- **Consequences**:
+ - Ephemeral SSH hosts need only a single ~1-2 MB static-friendly Rust binary (`ocx-child`) and `curl` (POSIX) / PowerShell (Windows).
+ - Both `bun test` and `cargo test` must keep `tests/fixtures/link/ocx-child-conformance.json` green whenever Link routes, relay-auth v2, or OCX-6 settings digests evolve.
diff --git a/devlog/_plan/261107_ocx_child_ephemeral_link/001_audit_and_status.md b/devlog/_plan/261107_ocx_child_ephemeral_link/001_audit_and_status.md
new file mode 100644
index 00000000000..8e60f63a076
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_ephemeral_link/001_audit_and_status.md
@@ -0,0 +1,27 @@
+# `261107_ocx_child_ephemeral_link` — Audit Disclosures & Phase Status Ledger
+
+> Reader contract: Maintainer and reviewer ledger tracking PABCD audit rounds, model-inheritance disclosures (`REVIEW-DECORRELATE-01`), and per-phase verification evidence (`LOOP-PESSIMIST-01`).
+
+## 1. Model Inheritance Disclosure (`REVIEW-DECORRELATE-01`)
+
+Per explicit user directive (`"너 상속 으로 해서"`), all PABCD subagents (`explorer`, `architect`, `reviewer`) inherit the coordinator's model (`google-antigravity/gemini-4-argon`) without mutating shared `.codexclaw` subagent configuration files. Cross-family decorrelation was therefore intentionally waived by user instruction and compensated by fresh zero-conclusion prompts and real command-backed verification.
+
+## 2. `wp0` (Docs-Only Roadmap Cycle) Audit & Closure
+
+- **Architect consultation**: Handle `01a1b18b-a2f3-7b53-971a-9b2c9cccecf7` returned `ALIGNED` on `ARCH-01..ARCH-07`; `Gap-1` (`models_cache.json` snapshot/restore in `ChildJournalV1`) and `Gap-2` (Windows owner-only DACL via `Win32_Security_Authorization`) folded into `020_wp2_rust_ocx_child.md`.
+- **Independent A-gate review**: Handle `01a1b197-8e50-7722-bf0d-d13bb303f767` Round 1 returned `VERDICT: GO-WITH-FIXES (blockers=2)`; all 5 findings (omitting `restartCapability` on `ocx-child` client `/healthz`, `ephemeralChild`-specific `apply()` failure recovery string + fail-closed detached `restart`, `010` header dependencies + exact `isAllowedRequestOrigin` loopback Host/Origin check, `sync --json` stderr redirection + attached `serve` start rationale, and clean `corrupt_managed_span_refused` vector) were folded into `010`, `020`, and `030`. Round 2 re-audit returned `VERDICT: PASS` (`blocking_issues: []`).
+- **`LOOP-PESSIMIST-01` notes (`wp0`)**:
+ - *Dead hypothesis*: Upstream socket pooling across relayed requests (`ARCH-02` alternative) died against `src/server/index/link-relay-sessions.ts:105` (`consume(nonce)` is strictly single-use), confirming one-shot `SingleSocketUpstream` per relayed request.
+ - *Dead hypothesis*: Advertising `restartCapability: "v1"` on `ocx-child` `GET /healthz` died in Round-1 audit because `ocx-child` does not serve `POST /api/system/restart` and `isChildHealth(..., "client", port)` does not require it.
+ - *Refutation trigger for `wp1..wp3`*: Any divergence between TypeScript and Rust outputs on `tests/fixtures/link/ocx-child-conformance.json`, or Windows encoded PowerShell exceeding `8000` chars.
+
+## 3. Work-Phase Status Table
+
+| Work Phase | Status | Summary |
+|---|---|---|
+| `wp0` | Completed | Docs-first roadmap (`000_roadmap.md`, `001_audit_and_status.md`, `010..040` decade PRDs) audited and locked with zero production edits |
+| `wp1` | Completed | Shared golden conformance fixture (`ocx-child-conformance.json`), `src/link/ephemeral-child.ts`, and `tests/clients/link-ocx-child-conformance.test.ts` (65 pass, 0 fail; typecheck clean) |
+| `wp2` | Completed | Standalone Rust crate `native/ocx-child` (stripped release binary; conformance, relay, and process-level lifecycle suites in `native/ocx-child/tests/`; `clippy -D warnings` & `fmt --check` clean). Current test counts are in the PR #8237 Verification section |
+| `wp3` | Completed | Home discovery (`ssh-argv.ts`, `remote-windows.ts`), detached enrollment preflight (`link-enrollment.ts`), `package.json` scripts/files, and focused + real-binary E2E suites (`link-ocx-child-e2e.test.ts` 811ms pass) |
+| `wp4` | Completed | `structure/manifest.json` + `structure/remote-link.md` + `structure/overview.md` (`INV-LINK-CHILD-01`) + `structure/INDEX.md`, `docs-site/` build (641 pages, 93,153 links), full gate matrix, and PR targeting `dev` |
+| `wp5` | In Progress | PR #8237 review rounds: write-ahead recovery journal, multiline TOML structural scanner, overwritten user `[features]` surgical restoration, shared `CODEX_HOME` ownership, per-step home identity rechecks during restoration, committed daemon generations, bounded relay shutdown, and `models_cache.json` slug ownership across sync and detach, each with shared golden or Rust regression tests |
diff --git a/devlog/_plan/261107_ocx_child_ephemeral_link/010_wp1_conformance_contract.md b/devlog/_plan/261107_ocx_child_ephemeral_link/010_wp1_conformance_contract.md
new file mode 100644
index 00000000000..085eb40623f
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_ephemeral_link/010_wp1_conformance_contract.md
@@ -0,0 +1,124 @@
+# `wp1` — Shared Golden Conformance Vectors & Ephemeral Child Contract Helpers
+
+> Continuity handoff (`LOOP-CONTINUITY-01` from `wp0` D at `a5c605fafc`): `wp0` locked the docs-first roadmap with zero production edits (`VERDICT: PASS` / `GO-WITH-FIXES (blockers=0)`). Revalidated `010_wp1_conformance_contract.md` against current HEAD: `isAllowedRequestOrigin` (`src/server/auth-cors.ts:118`) is added to `Header dependencies` and `routeCases` so DNS-rebinding `Host` and cross-origin `Origin` `403` rejections are locked in the shared golden fixture alongside route admission, smuggling, HMAC v2, OCX-6 digests, child-parity, and `#1798` journal round-trips. Architect consultation: covered by `ARCH-06` & `ARCH-07` (`ALIGNED` on handle `01a1b18b-a2f3-7b53-971a-9b2c9cccecf7`, `000_roadmap.md` §3.1).
+
+- **Phase ID**: `wp1` (depends on `wp0`; satisfies `c-2`)
+- **Header dependencies**:
+ - Existing types/functions consumed: `linkRouteAllowed` (`src/link/routes.ts:15`), `linkManagementRouteAllowed` (`src/link/management-routes.ts:72`), `isAllowedRequestOrigin` (`src/server/auth-cors.ts:118`), `computeLinkRelayProof`, `computeLinkConnectionProof`, `verifyLinkRelayProof`, `verifyLinkConnectionProof` (`src/link/relay-auth.ts:45-95`), `HomeCodexSettings`, `codexSettingsDigestPayload`, `codexSettingsDigest` (`src/link/codex-settings.ts:30-182`), `parseLinkChildParityReport` (`src/link/child-parity.ts:67`), `validateHubRelayRequestHeaders` (`src/client/hub-relay.ts:49`), `classifySyncResult` (`src/cli/sync-fanout.ts:52`), `externalCodexModelProvider` & `ensureRootWebSearchDisabled` (`src/codex/inject/config-toml.ts:30,366`), and `stripJournaledOpenaiBaseUrl` (`src/codex/injected-marker.ts:106`).
+ - Shared types exported/validated in this phase: `EphemeralChildConfigProjection`, `isDetachedEphemeralChildConfig`, `isEphemeralChildHealth` (`src/link/ephemeral-child.ts`), and `ConformanceSuite` (`tests/fixtures/link/ocx-child-conformance.json`).
+- **Scope**:
+ - **IN**: `src/link/ephemeral-child.ts` (NEW), `tests/fixtures/link/ocx-child-conformance.json` (NEW), `tests/clients/link-ocx-child-conformance.test.ts` (NEW — named `link-*` to satisfy `seeds.json` vs `cli` prefix rule), `scripts/test-layout/layout-clients.json` (MODIFY), `tests/fixtures/test-layout-expected.json` (MODIFY).
+ - **OUT**: `native/ocx-child/**` (`wp2`), `src/server/management/**` (`wp3`), `structure/**` & `docs-site/**` (`wp4`).
+
+## 1. Exact File Changes
+
+### 1.1 NEW `src/link/ephemeral-child.ts`
+
+Pure, zero-side-effect TypeScript contract helper module in `src/link/` (starts no timers, sockets, or processes on import; unreachable from `src/router.ts`, `src/server/lifecycle.ts`, and `src/server/responses/core.ts`):
+
+```ts
+/**
+ * Contract predicates for the lightweight `ocx-child` ephemeral Remote Link Child runtime.
+ *
+ * Kept pure inside `src/link/` so enrollment and diagnostics can distinguish a clean detached
+ * `ocx-child` host from a full standalone `ocx` daemon without changing `LinkStore` on disk.
+ */
+
+export interface EphemeralChildConfigProjection {
+ readonly port: number;
+ readonly runtimeRole: "standalone" | "client";
+ readonly ephemeralChild: true;
+}
+
+function isValidPort(value: unknown): value is number {
+ return typeof value === "number" && Number.isInteger(value) && value >= 1024 && value <= 65535;
+}
+
+/**
+ * True when `config show --json` came from a clean, detached `ocx-child` binary ready for
+ * Home-initiated enrollment (`runtimeRole: "standalone"`, `ephemeralChild: true`, valid port).
+ * An already-attached `ocx-child` reports `runtimeRole: "client"` and returns `false`.
+ */
+export function isDetachedEphemeralChildConfig(value: unknown): value is EphemeralChildConfigProjection {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
+ const record = value as Record;
+ return record.ephemeralChild === true
+ && (record.runtimeRole ?? "standalone") === "standalone"
+ && isValidPort(record.port);
+}
+
+/** True when a `/healthz` payload identifies a live `ocx-child` client relay on `expectedPort`. */
+export function isEphemeralChildHealth(value: unknown, expectedPort: number): boolean {
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
+ const record = value as Record;
+ return record.service === "opencodex"
+ && record.role === "client"
+ && record.ephemeralChild === true
+ && record.port === expectedPort
+ && typeof record.pid === "number"
+ && Number.isInteger(record.pid)
+ && record.pid > 0
+ && typeof record.startedAt === "string"
+ && Number.isFinite(Date.parse(record.startedAt));
+}
+```
+
+### 1.2 NEW `tests/fixtures/link/ocx-child-conformance.json`
+
+Deterministic JSON document (`"schema": "ocx.child.conformance/1"`) containing 6 golden vector arrays shared verbatim between `bun test` and `cargo test`:
+
+1. `routeCases` (24 vectors):
+ - Every allowed `POST` data-plane route (`/v1/responses`, `/v1/responses/compact`, `/v1/messages`, `/v1/messages/count_tokens`, `/v1/chat/completions`, `/v1/audio/transcriptions`, `/v1/alpha/search`, `/v1/images/generations`, `/v1/images/edits`, `/v1/live`, `/v1/realtime/calls`, plus all 10 `CONTEXT_PATHS` under `/v1/alpha/{history,notes}/v2/*`) -> `expectedAction: "relay_data"`, `expectedAuthHeader: "bearer"`.
+ - `GET /v1/catalog`, `HEAD /v1/catalog`, `GET /v1/hub-state`, `HEAD /v1/hub-state`, `GET /v1/models`, `GET /v1/opencodex/artifacts/item-1` -> `expectedAction: "relay_data"`, `expectedAuthHeader: "bearer"`.
+ - `GET /v1/usage` -> `expectedAction: "relay_data"`, `expectedAuthHeader: "x-opencodex-api-key"`.
+ - `GET /v1/link-management/codex-settings` (empty query -> `relay_management`; `?foo=1` -> `reject_404`), `GET /v1/link-management/api/settings` (clean query -> `relay_management`; `?refresh=1` -> `reject_404`).
+ - `GET /healthz` -> `local_healthz`; `GET /readyz` -> `local_readyz`; `HEAD /readyz` -> `reject_404`.
+ - `GET /v1/responses` with `Upgrade: websocket` (loopback `Origin`) -> `reject_426_websocket`; `GET /v1/responses` with `Upgrade: websocket` + `Origin: https://evil.example` -> `reject_403_origin`; `POST /v1/responses` with `Host: evil.example:10100` -> `reject_403_origin`; `POST /v1/responses` with `Origin: https://evil.example` -> `reject_403_origin`; `GET /v1/catalog` with `Upgrade: h2c` -> `reject_404`; `GET /api/config` & `DELETE /v1/responses` -> `reject_404`.
+2. `framingCases` (10 vectors):
+ - Valid `Content-Length: 42` with caller credentials (`Authorization: Bearer sk-user`, `x-api-key: ant-user`, `x-goog-api-key: g-user`, `api-key: az-user`, `x-opencodex-api-key: ocx-user`, `chatgpt-account-id: acc-1`, `cookie: sid=1`, `Connection: keep-alive, x-custom-secret`, `x-custom-secret: drop-me`, `x-request-trace: keep-me`) -> `allow` and exact `strippedHeaderNames` leaving only `x-request-trace`.
+ - Valid `Transfer-Encoding: chunked` without `Content-Length` -> `allow`.
+ - Simultaneous `Content-Length: 10` + `Transfer-Encoding: chunked` -> `reject_400_smuggling`.
+ - Duplicate `Content-Length` (`10`, `10` or `10`, `20`) -> `reject_400_smuggling`.
+ - Non-digit `Content-Length: +10` / `10abc` -> `reject_400_smuggling`.
+ - Non-chunked `Transfer-Encoding: gzip, chunked` -> `reject_400_smuggling`.
+ - `Upgrade: websocket` in relay header validator -> `reject_400_smuggling`.
+ - Header value containing `\r` or `\n` -> `reject_400_invalid`.
+3. `relayAuthCases` (3 deterministic vectors using synthetic test-only tokens like `"ocx_data_0000000000000000000000000000000000000001"` so `privacy:scan` sees obvious zero-entropy test fixtures):
+ - Locks `tokenSha256Hex`, `expectedCallerProof`, `expectedListenerProof`, and `expectedConnectionProof` computed over raw 32-byte `Buffer.from(tokenSha256Hex, "hex")`.
+4. `settingsDigestCases` (4 vectors):
+ - All-null defaults (`fastMode: null, multiAgentMode: null, webSearchSidecarEnabled: true, subagentDefaults: null`).
+ - Populated `fastMode: true, multiAgentMode: "v1", webSearchSidecarEnabled: false, subagentDefaults: { model: "gpt-5.6-sol", reasoningEffort: "high" }`.
+ - Populated `fastMode: false, multiAgentMode: "v2", webSearchSidecarEnabled: true, subagentDefaults: { model: "anthropic/claude-sonnet-4-5", reasoningEffort: null }`.
+ - `multiAgentMode: "default"` with `subagentDefaults: null`.
+5. `childParityCases` (6 vectors):
+ - Valid pre-apply (`lastApplyAt: null, appliedCodexSettingsDigest: null`), valid post-apply, mismatched nullability (`lastApplyAt: null` with non-null digest -> invalid), extra unknown property -> invalid, out-of-range timestamp -> invalid, negative `sequence` -> invalid.
+6. `journalRoundtripCases` (8 vectors):
+ - `clean_absent_config`: `preAttachToml: null` -> attach -> unchanged detach -> `expectedFinalToml: null` (file unlinked), `expectedDetachOutcome: "exact_restore"`.
+ - `preserve_comments_and_crlf`: CRLF `config.toml` with `# user comment` and `[mcp_servers.git]` -> attach -> sync with updated `fastMode` + `subagentDefaults` -> unchanged detach -> byte-exact CRLF restoration (`"exact_restore"`).
+ - `app_rewrite_1798_comment_strip`: attach -> Codex App strips all `# Auto-injected by opencodex...` comments and adds `model = "gpt-5.6-sol"` + `[projects."/work"]` -> detach -> strips `openai_base_url`, `experimental_realtime_ws_base_url`, `model_catalog_json`, and managed settings by exact journaled value while preserving `[projects."/work"]` (`"surgical_restore"`).
+ - `web_search_disabled_and_restored`: user had `web_search = "live"` -> attach with `webSearchSidecarEnabled: false` writes `web_search = "disabled"` -> `#1798` strips marker + user adds `approval_policy = "never"` -> detach restores `web_search = "live"` and keeps `approval_policy = "never"` (`"surgical_restore"`).
+ - `user_changed_base_url_preserved`: attach -> user edits `openai_base_url = "https://custom.gateway.example/v1"` -> detach preserves user's custom `openai_base_url` while removing remaining owned keys (`"surgical_restore"`).
+ - `corrupt_managed_span_refused`: attach wrote Design B root keys -> external edit corrupts `config.toml` with an unterminated quoted assignment on a managed root key (`openai_base_url = "http://127.0.0.1:10100/v1`) -> detach refuses (`expectedDetachOutcome: "refuse_conflict"`) leaving `config.toml` and `codex-journal.json` untouched.
+ - `conflict_tampered_catalog_refused`: attach wrote `opencodex-catalog.json` -> user hand-edits `opencodex-catalog.json` -> detach refuses (`expectedDetachOutcome: "refuse_conflict"`) rather than deleting user-modified catalog.
+ - `refuse_external_model_provider_on_attach`: `preAttachToml` has `model_provider = "openrouter"` + `[model_providers.openrouter]\nbase_url = "https://openrouter.ai/api/v1"` -> `expectedAttachOk: false`, `expectedDetachOutcome: "not_attached"`.
+
+### 1.3 NEW `tests/clients/link-ocx-child-conformance.test.ts` & Test-Layout Shards
+
+- Resolves fixture via `fixturePath("link", "ocx-child-conformance.json")` from `../helpers/repo-root`.
+- Verifies every vector in `routeCases`, `framingCases`, `relayAuthCases`, `settingsDigestCases`, `childParityCases`, and `journalRoundtripCases` against OpenCodex's live TypeScript implementations (`linkRouteAllowed`, `linkManagementRouteAllowed`, `validateHubRelayRequestHeaders`, `computeLinkRelayProof`, `computeLinkConnectionProof`, `codexSettingsDigestPayload`, `codexSettingsDigest`, `parseLinkChildParityReport`, `externalCodexModelProvider`, `stripJournaledOpenaiBaseUrl`, `ensureRootWebSearchDisabled`), plus unit tests for `isDetachedEphemeralChildConfig` and `isEphemeralChildHealth`.
+- Registers `"link-ocx-child-conformance.test.ts": "clients"` in BOTH:
+ - `scripts/test-layout/layout-clients.json`
+ - `tests/fixtures/test-layout-expected.json`
+
+## 2. Conditional-Path Activation Scenarios (`C-ACTIVATION-GROUNDING-01`)
+
+1. **WebSocket `Upgrade` vs generic `Upgrade` rejection**: Driven in `routeCases` (`Upgrade: websocket` on `/v1/responses` -> `reject_426_websocket`; `Upgrade: h2c` on `/v1/catalog` -> `reject_404`) and asserted against `linkRouteAllowed` + ingress rules.
+2. **Smuggling header combinations (`CL+TE`, duplicate `CL`)**: Driven in `framingCases` and asserted to return `reason: "smuggling"` from `validateHubRelayRequestHeaders`.
+3. **Raw 32-byte vs ASCII-hex HMAC key**: Driven in `relayAuthCases` and asserted against `computeLinkRelayProof` / `verifyLinkRelayProof` (plus negative assertion that HMAC over ASCII hex differs).
+4. **Detached vs already-attached `ocx-child` config**: Driven against `isDetachedEphemeralChildConfig` (`runtimeRole: "client"` returns `false`; `ephemeralChild` missing returns `false`; port `< 1024` returns `false`).
+
+## 3. Phase Verifiers
+
+- `bun test tests/clients/link-ocx-child-conformance.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/lab/core-link-boundary.test.ts tests/lab/core-lab-boundary.test.ts` (exit `0`)
+- `bun run typecheck` (exit `0`)
+- `bun run privacy:scan` (exit `0`)
diff --git a/devlog/_plan/261107_ocx_child_ephemeral_link/020_wp2_rust_ocx_child.md b/devlog/_plan/261107_ocx_child_ephemeral_link/020_wp2_rust_ocx_child.md
new file mode 100644
index 00000000000..fd2d5da1a63
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_ephemeral_link/020_wp2_rust_ocx_child.md
@@ -0,0 +1,124 @@
+# `wp2` — Standalone Lightweight Rust Child Crate (`native/ocx-child`)
+
+> Continuity handoff (`LOOP-CONTINUITY-01` from `wp1` D at `07f50076e5`): `wp1` landed the 74-vector `tests/fixtures/link/ocx-child-conformance.json` and `tests/clients/link-ocx-child-conformance.test.ts` (`65 pass, 0 fail`). Revalidated `020_wp2_rust_ocx_child.md` against `07f50076e5` and folded the `wp0` non-blocking Medium note: `Cargo.toml` sets crate `version = "0.1.0"` (matching `native/remote-workspace-helper/Cargo.toml:3`) while `pub const OCX_PROTOCOL_COMPAT_VERSION: &str = "2.98.0"` drives `--version` (`opencodex 2.98.0`) and peer headers, preventing `dev-version-bump.yml` drift on `package.json` bumps. Architect consultation: covered by `ARCH-01..ARCH-04` (`ALIGNED` on `01a1b18b-a2f3-7b53-971a-9b2c9cccecf7`, `000_roadmap.md` §3.1).
+
+- **Phase ID**: `wp2` (depends on `wp1`; satisfies `c-3`, `c-6`)
+- **Header dependencies**:
+ - Consumes shared fixture `ConformanceSuite` (`tests/fixtures/link/ocx-child-conformance.json`) and wire/schema contracts `EphemeralChildConfigProjection` (`src/link/ephemeral-child.ts`), `HomeCodexSettingsDocument` / `HomeCodexSettings` (`src/link/codex-settings.ts:30`), `LinkChildParityReport` (`src/link/child-parity.ts:28`), `ClientSyncMismatchReason` (`src/client/sync-expectation.ts:17`), and `FanoutDocument` (`src/cli/sync-fanout.ts:86`).
+ - Introduces Rust types `ChildStateV1`, `ChildJournalV1`, `ChildInjectedLedger`, `RelayServer`, `SingleSocketUpstream`, and binary `ocx-child` (`2.98.0`).
+- **Scope**:
+ - **IN**: `native/ocx-child/Cargo.toml` (NEW), `native/ocx-child/Cargo.lock` (NEW), `native/ocx-child/src/**` (NEW), `native/ocx-child/tests/**` (NEW).
+ - **OUT**: `src/**`, `package.json` (`wp3`), `structure/**` & `docs-site/**` (`wp4`).
+
+## 1. Exact File Map & Diff-Level Specification
+
+### 1.1 NEW `native/ocx-child/Cargo.toml` & `Cargo.lock`
+
+```toml
+[package]
+name = "opencodex-child"
+version = "0.1.0"
+edition = "2024"
+rust-version = "1.90"
+license = "MIT"
+publish = false
+
+[[bin]]
+name = "ocx-child"
+path = "src/main.rs"
+
+[lib]
+name = "ocx_child"
+path = "src/lib.rs"
+
+[dependencies]
+base64 = "=0.22.1"
+hmac = "=0.12.1"
+serde = { version = "=1.0.229", features = ["derive"] }
+serde_json = "=1.0.151"
+sha2 = "=0.10.9"
+
+[target.'cfg(unix)'.dependencies]
+libc = "=0.2.191"
+
+[target.'cfg(windows)'.dependencies]
+windows-sys = {
+ version = "=0.61.2",
+ features = [
+ "Win32_Foundation",
+ "Win32_Security",
+ "Win32_Security_Authorization",
+ "Win32_Security_Cryptography",
+ "Win32_Storage_FileSystem",
+ "Win32_System_Threading",
+ ],
+}
+
+[profile.release]
+codegen-units = 1
+lto = "thin"
+opt-level = "s"
+strip = "symbols"
+
+[profile.release.build-override]
+strip = false
+```
+
+### 1.2 NEW `native/ocx-child/src/` Module Layout
+
+All modules set `#![deny(unsafe_op_in_unsafe_fn)]` at crate root (`lib.rs` / `main.rs`):
+
+1. `src/lib.rs` & `src/main.rs`:
+ - Exports testable library modules (`routes`, `http_wire`, `relay_auth`, `codex_settings`, `parity`, `toml_surgeon`, `journal`, `state`, `platform`, `relay`, `daemon`, `cli`).
+ - `pub const OCX_PROTOCOL_COMPAT_VERSION: &str = "2.98.0";` (`>= 2.93.0`, decoupled from crate `0.1.0` so automated `package.json` bumps never cause version-lock drift).
+2. `src/platform/mod.rs`, `src/platform/unix.rs`, `src/platform/windows.rs`:
+ - `fill_random_bytes(&mut [u8])`: reads `/dev/urandom` on Unix and `BCryptGenRandom(..., BCRYPT_USE_SYSTEM_PREFERRED_RNG)` on Windows.
+ - `volatile_zero_bytes(&mut [u8])`: `core::ptr::write_volatile` byte loop + `core::sync::atomic::compiler_fence(Ordering::SeqCst)` (`SecretBytes` zeroes on `Drop`).
+ - `FileLock::acquire_blocking(path)` & `FileLock::try_acquire_nonblocking(path)`: opens lockfile with `0600` mode, acquires `libc::flock(fd, LOCK_EX)` / `LOCK_EX | LOCK_NB` on Unix and `LockFileEx(LOCKFILE_EXCLUSIVE_LOCK)` / `LOCKFILE_FAIL_IMMEDIATELY` on Windows.
+ - `write_private_atomic(path, bytes)`: ensures parent directory (`0700` on Unix; owner-only protected DACL via `SetNamedSecurityInfoW` / `PROTECTED_DACL_SECURITY_INFORMATION` on Windows), creates sibling temp file (`.tmp..`, `O_CREAT | O_EXCL | O_WRONLY` mode `0600` on Unix and owner-only DACL on Windows), writes all bytes, `sync_all()`, and atomically `fs::rename`s into `path`.
+ - `spawn_detached_serve(exe, opencodex_home, codex_home, listen_port)`: Unix `setsid` + `Stdio::null()`; Windows `CREATE_BREAKAWAY_FROM_JOB | DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP` breaking out of Home's `OcxJob` (`JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK` in `src/link/remote-windows.ts:69`).
+ - `terminate_verified_daemon(pid, expected_port)`: verifies `GET http://127.0.0.1:/healthz` reports `service == "opencodex"`, `ephemeralChild == true`, and `pid == expected_pid` before sending `SIGTERM` / attested shutdown, polling up to 5 s until exit and port release.
+3. `src/routes.rs` & `src/http_wire.rs`:
+ - `classify_route(method, path, query, headers) -> RouteDecision`: exact port of `src/link/routes.ts:15`, `src/link/management-routes.ts:72`, and `src/client/link-ingress.ts:88-96`.
+ - `GET /healthz` -> `LocalHealthz` returning `{ "service": "opencodex", "version": "2.98.0", "role": "client", "ephemeralChild": true, "uptime": , "pid": , "port": , "startedAt": "" }` (`restartCapability` / `restartBudgetMs` are deliberately omitted because `ocx-child` does not serve `POST /api/system/restart`, and `isChildHealth(..., "client", port)` at `src/link/remote-probe.ts:35` checks only `port`, `service`, `pid`, and `role === "client"`).
+ - `GET /readyz` (without `Upgrade`) -> `LocalReadyz`.
+ - Loopback `Host` & `Origin` anti-rebinding gate (matching `isAllowedRequestOrigin` in `src/server/auth-cors.ts:83-125` & `src/client/link-ingress.ts:89-96`): on every relayed route and `/v1/responses` WebSocket `Upgrade`, `Host` (when parseable) must be a loopback hostname (`127.0.0.1`, `localhost`, `[::1]`) and `Origin` (when present) must be `http(s)://(:port)`; otherwise return `403 origin_rejected`.
+ - `/v1/responses` with `Upgrade: websocket` (after passing `Host`/`Origin` check) -> `Reject426Websocket` (`426 upgrade_required`, `"Responses WebSocket transport is disabled; use HTTP"`).
+ - Any other `Upgrade` header -> `Reject404`.
+ - Exact `POST` routes + 10 `CONTEXT_PATHS`, `GET|HEAD` `/v1/catalog` & `/v1/hub-state`, `GET /v1/models`, `GET /v1/opencodex/artifacts/*` -> `RelayData { auth: Bearer }`.
+ - `GET /v1/usage` -> `RelayData { auth: ApiKeyHeader }`.
+ - `GET /v1/link-management/codex-settings` (empty query) and `GET|HEAD /v1/link-management/api/...` (20 allowlisted paths without `refresh|reload|force|quota` query params) -> `RelayManagement` (16 MiB response cap).
+ - Everything else -> `Reject404`.
+ - `parse_and_sanitize_request_headers`: enforces `64 KiB` (`65_536` B) cap (`431`), rejects CR/LF in names/values (`400`), rejects `CL+TE` / duplicate `CL` / non-digit `CL` / non-`chunked` `TE` / `Upgrade` (`400 {"error":"link relay request headers refused"}`), strips `HOP_BY_HOP_HEADERS`, `REQUEST_OMITTED_HEADERS`, `CALLER_CREDENTIAL_HEADERS`, and `Connection`-listed tokens.
+ - Enforces `256 MiB` (`268_435_456` B) inbound/non-SSE outbound body cap (`413` / `502`); streams chunked and SSE (`text/event-stream`) bodies chunk-by-chunk with immediate socket `flush()` and `300 s` header / SSE idle timeout.
+4. `src/relay_auth.rs`, `src/codex_settings.rs`, & `src/parity.rs`:
+ - Raw 32-byte `SHA-256(token)` HMAC key; exact `opencodex-link-relay-v2` caller/listener proofs and `opencodex-link-connection-v1` generation proofs with constant-time 32-byte comparison.
+ - `SingleSocketUpstream::open_authenticated(tunnel_port, creds, probe_only)`: opens one `TcpStream` to `127.0.0.1:` (5 s timeout), sends `GET /.well-known/opencodex/link-relay-auth?version=2&...` with peer version/features headers (plus `x-opencodex-link-probe: 1` when `probe_only`), verifies `204` + 0-byte body + `x-opencodex-link-proof` (+ optional `x-opencodex-link-connection-proof`), and returns that **exact `TcpStream`** to carry the single data request with `x-opencodex-link-session: `.
+ - Exact OCX-6 `codex_settings_digest` and `LinkChildParityReport` (`<= 1024` B, immediate send on start / post-sync rollover + `[2s, 5s, 15s, 30s, 60s]` backoff then `60s` period, halting on `404`).
+5. `src/toml_surgeon.rs` & `src/journal.rs`:
+ - Ordered locks: `$OPENCODEX_HOME/child/lifecycle.lock` -> `$CODEX_HOME/.opencodex-write.lock`.
+ - Fail-closed attach checks: refuses if `$OPENCODEX_HOME/runtime-port.json`, `ocx.pid`, full-`ocx` `config.json`, or `$CODEX_HOME/.opencodex-journal.json` exists, if a foreign listener occupies `listen_port`, or if `config.toml` has an external `model_provider` (except `base_url`-less placeholder), unmarked user `openai_base_url` / `experimental_realtime_ws_base_url`, or non-`opencodex-catalog.json` `model_catalog_json`.
+ - Design B apply + `0600` `ChildJournalV1`: snapshots `preAttachConfig*`, `preAttachCatalog*`, and `preAttachModelsCacheExisted` / `preAttachModelsCacheBytesB64` / `latestModelsCacheSha256`; writes root `openai_base_url`, `experimental_realtime_ws_base_url`, `model_catalog_json`, optional `web_search = "disabled"` (saving `replacedUserWebSearchLine`), `[features]` `fast_mode` / `multi_agent_v2`, and `[agents]` subagent defaults; writes `$CODEX_HOME/opencodex-catalog.json` and refreshes `$CODEX_HOME/models_cache.json`.
+ - Two-tier 100% offline `detach`: Tier 1 exact byte restore when `untouchedByExternalWriter && currentSha256 == latestInjectedConfigSha256`; Tier 2 surgical value-matched strip surviving `#1798` comment stripping while preserving user edits; fails closed (`exit 4`) on corrupt managed spans or tampered `opencodex-catalog.json`; restores pre-attach `models_cache.json` (or unlinks an unchanged cache `ocx-child` created so stale Home slugs never linger); stops owned `serve` daemon and leaves zero running process.
+6. `src/cli.rs`:
+ - Native & `ocx`-compatible verbs: `--version` (`opencodex 2.98.0`), `port` / `link port`, `config show --json`, `attach` / `connect --link --key-stdin --tunnel-port --link-id `, `serve` / `restart` (rolls over `ocx-child serve` when attached; fails closed `exit 1` without spawning any daemon when detached so zero orphan daemon can ever start), `sync [--expect-link ] [--json]` (under `--json`, all human/diagnostic lines write strictly to `stderr` so `stdout` emits only the single `ocx.sync.fanout/1` JSON object; validates `--expect-link` under non-blocking lock with exit-5 `child_link_mismatch` / `pending_operation` stderr JSON before any network/disk I/O; intentionally starts `ocx-child serve` if absent on an attached VM or rolls it over when `daemonStartedAtMs < appliedAtMs` so `/healthz` `startedAt >= appliedAt`, and immediately POSTs `child-parity`), `detach` / `disconnect`, `status [--json]` / `connect status [--json]` / `link status [--json]`.
+
+### 1.3 NEW `native/ocx-child/tests/conformance.rs` & `tests/relay_and_lifecycle.rs`
+
+- `tests/conformance.rs`: executes all 6 vector arrays from `../../tests/fixtures/link/ocx-child-conformance.json`.
+- `tests/relay_and_lifecycle.rs`: integration suite with a real mock Home `hub-link` HTTP/1.1 + SSE server verifying same-socket peer binding, single-use `nonce`, SSE streaming, smuggling/Upgrade rejection, caller credential stripping, `attach -> serve -> sync -> rollover + parity -> #1798 offline detach`, busy-lock/mismatch `--expect-link` exit `5`, and fail-closed attach/detach conflicts.
+
+## 2. Conditional-Path Activation Scenarios (`C-ACTIVATION-GROUNDING-01`)
+
+1. **Same-socket binding**: Mock Home verifies preflight and data request share identical client `SocketAddr`.
+2. **Spoofed Home listener proof**: Mock Home returns bad `x-opencodex-link-proof` -> `ocx-child` returns `503 link_relay_auth_failed` without sending `ocx_data_*`.
+3. **`#1798` comment-stripped offline detach**: Strip all `#` comments while Home is stopped -> `ocx-child detach` strips proxy keys by value and preserves user keys.
+4. **Busy-lock & mismatch `--expect-link` exit `5`**: Hold `lifecycle.lock` while invoking `sync --expect-link` -> observe exit `5` and `{"phase":"child_link_mismatch","reason":"pending_operation"}` on stderr.
+5. **Fail-closed attach**: Plant `runtime-port.json` or external `model_provider` -> observe non-zero refusal with `config.toml` untouched.
+
+## 3. Phase Verifiers
+
+- `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`)
+- `cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml` (exit `0`, stripped binary size recorded)
diff --git a/devlog/_plan/261107_ocx_child_ephemeral_link/030_wp3_home_link_integration.md b/devlog/_plan/261107_ocx_child_ephemeral_link/030_wp3_home_link_integration.md
new file mode 100644
index 00000000000..95496d62c74
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_ephemeral_link/030_wp3_home_link_integration.md
@@ -0,0 +1,125 @@
+# `wp3` — Home-Side Ephemeral Child Discovery, Enrollment, Sync Fan-Out, & E2E Integration
+
+> Continuity handoff (`LOOP-CONTINUITY-01` from `wp2` D at `072d491771`): `wp2` delivered `native/ocx-child` (878 KiB stripped release binary, 6/6 Rust conformance + live lifecycle suites passing). Revalidated `030_wp3_home_link_integration.md` against `072d491771`: all paths, hunks, `link-*` test-layout shard entries, and real-binary E2E steps match the compiled `ocx-child` contract. Architect consultation: covered by `ARCH-05` & `ARCH-07` (`ALIGNED` on `01a1b18b-a2f3-7b53-971a-9b2c9cccecf7`, `000_roadmap.md` §3.1).
+
+- **Phase ID**: `wp3` (depends on `wp2`; satisfies `c-4`, `c-6`)
+- **Header dependencies**:
+ - Consumes `isDetachedEphemeralChildConfig` (`src/link/ephemeral-child.ts`), `POWERSHELL_ENCODED_MAX` (`src/link/remote-windows.ts:8`), `isChildHealth`, `remoteChildHttpArgv`, `REMOTE_CURL_PREFLIGHT` (`src/link/remote-probe.ts:20-40`), `classifySyncResult` (`src/cli/sync-fanout.ts:52`), and compiled `ocx-child` binary.
+- **Scope**:
+ - **IN**: `src/link/ssh-argv.ts` (MODIFY), `src/link/remote-windows.ts` (MODIFY), `src/server/management/link-enrollment.ts` (MODIFY), `package.json` (MODIFY), `tests/clients/link-ephemeral-child-discovery.test.ts` (NEW), `tests/server/link-enrollment-ephemeral-child.test.ts` (NEW), `tests/clients/ocx-child-e2e.test.ts` (NEW), `scripts/test-layout/layout-clients.json` (MODIFY), `tests/fixtures/test-layout-expected.json` (MODIFY).
+ - **OUT**: `src/cli/index.ts` (untouched — 1999/2000 cliff), `src/link/store.ts` (untouched — `LinkRecord` stays backwards-compatible), `structure/**` & `docs-site/**` (`wp4`).
+
+## 1. Exact File Changes & Before/After Hunks
+
+### 1.1 MODIFY `src/link/ssh-argv.ts`
+
+Extend `REMOTE_OCX_CANDIDATES` so every full-`ocx` candidate is probed first, and only when all full-`ocx` candidates miss does discovery probe `ocx-child` in `$HOME/.local/bin/ocx-child`, `$HOME/.cache/opencodex/bin/ocx-child`, and `$PATH`:
+
+```diff
+ const REMOTE_OCX_CANDIDATES = `for candidate in "$HOME/.opencodex-desktop/bin/ocx" "/Applications/OpenCodex.app/Contents/MacOS/ocx" "/usr/bin/ocx"; do
+ try_ocx "$candidate" "$@"
+ done
+ remaining=$PATH
+ while :; do
+ directory=\${remaining%%:*}
+ try_ocx "\${directory:-.}/ocx" "$@"
+ case "$remaining" in *:*) remaining=\${remaining#*:};; *) break;; esac
+-done`;
++done
++for candidate in "$HOME/.local/bin/ocx-child" "$HOME/.cache/opencodex/bin/ocx-child"; do
++ try_ocx "$candidate" "$@"
++done
++remaining=$PATH
++while :; do
++ directory=\${remaining%%:*}
++ try_ocx "\${directory:-.}/ocx-child" "$@"
++ case "$remaining" in *:*) remaining=\${remaining#*:};; *) break;; esac
++done`;
+```
+
+### 1.2 MODIFY `src/link/remote-windows.ts`
+
+Append `ocx-child.exe` fallback candidates after all `ocx.exe` candidates in `OCX_CANDIDATES`, keeping both `windowsOcxArgv` and `windowsPinnedSyncArgv` strictly below `POWERSHELL_ENCODED_MAX = 8000`:
+
+```diff
+ const OCX_CANDIDATES = String.raw`$u=$env:USERPROFILE;$c=@("$u\.opencodex-desktop\bin\ocx.exe","$u\.local\share\goat\opencodex\current\ocx.exe","$u\.local\bin\ocx.exe")
+ if($env:LOCALAPPDATA){$c+="$env:LOCALAPPDATA\Programs\OpenCodex\ocx.exe"}
+ if($env:ProgramFiles){$c+="$env:ProgramFiles\OpenCodex\ocx.exe"}
+ $c+=@((Get-Command ocx.exe -Type Application -EA 0).Source)
++$c+=@("$u\.local\bin\ocx-child.exe","$u\.cache\opencodex\bin\ocx-child.exe")
++if($env:LOCALAPPDATA){$c+="$env:LOCALAPPDATA\opencodex\bin\ocx-child.exe"}
++$c+=@((Get-Command ocx-child.exe -Type Application -EA 0).Source)
+ `;
+```
+
+### 1.3 MODIFY `src/server/management/link-enrollment.ts`
+
+In `apply()` (`src/server/management/link-enrollment.ts:133-158`), import `isDetachedEphemeralChildConfig` from `../../link/ephemeral-child`: when `isDetachedEphemeralChildConfig(childConfig)` is `true`, require `before.code !== 0` (refusing `409 child_restart_unsupported` if any foreign/existing listener occupies `childPort`) and tailor the `remoteMayBeConnected` failure `recovery` hint to `"Run ocx-child detach (or ocx disconnect) on the Child before retrying."` so operators are never told to run `ocx restart` after `disconnect`; otherwise keep the exact existing `isChildHealth(beforeHealth, "standalone", childPort)` gate and recovery string for full `ocx`:
+
+```diff
++import { isDetachedEphemeralChildConfig } from "../../link/ephemeral-child";
+ ...
++ let ephemeralChild = false;
+ let remoteMayBeConnected = false, phase = "remote_port";
+ const failed = async (response: Response): Promise => {
+ const compensation = record ? await compensateNewLink(ctx, state, record) : null;
+ const outcome = compensation ?? response;
+ if (!remoteMayBeConnected) return outcome;
+ const payload = await outcome.json() as Record;
+- return Response.json({ ...payload, residual: true, recovery: "Run ocx disconnect and ocx restart on the Child before retrying." }, { status: outcome.status });
++ const recovery = ephemeralChild
++ ? "Run ocx-child detach (or ocx disconnect) on the Child before retrying."
++ : "Run ocx disconnect and ocx restart on the Child before retrying.";
++ return Response.json({ ...payload, residual: true, recovery }, { status: outcome.status });
+ };
+ ...
+ if (remotePlatform === "posix" && (await run(REMOTE_CURL_PREFLIGHT)).code !== 0) return fail("child_curl_missing", "Install curl on the Child before connecting a Remote Link.", 409);
+ const before = await run(remoteChildHttpArgv(childPort, "healthz", LINK_RELAY_VERIFY_MS, remotePlatform));
+- const beforeHealth = before.code === 0 ? parseChildHttp(before.stdout) : null;
+- if (!isChildHealth(beforeHealth, "standalone", childPort)) return fail("child_restart_unsupported", "The Child must run a standalone OpenCodex with bound restart support; update or disconnect it before linking.", 409);
++ ephemeralChild = isDetachedEphemeralChildConfig(childConfig);
++ if (ephemeralChild) {
++ if (before.code === 0) return fail("child_restart_unsupported", "The Child's target port is already in use; disconnect or free the port before linking.", 409);
++ } else {
++ const beforeHealth = before.code === 0 ? parseChildHttp(before.stdout) : null;
++ if (!isChildHealth(beforeHealth, "standalone", childPort)) return fail("child_restart_unsupported", "The Child must run a standalone OpenCodex with bound restart support; update or disconnect it before linking.", 409);
++ }
+```
+
+Add `native/ocx-child` source artifacts to `"files"` and add scripts `build:ocx-child` and `test:ocx-child`:
+
+```diff
+ "native/remote-workspace-helper/Cargo.toml",
+ "native/remote-workspace-helper/Cargo.lock",
+ "native/remote-workspace-helper/src",
++ "native/ocx-child/Cargo.toml",
++ "native/ocx-child/Cargo.lock",
++ "native/ocx-child/src",
+ "LICENSE"
+ ...
+ "build:remote-workspace-helper": "cargo build --release --locked --manifest-path native/remote-workspace-helper/Cargo.toml",
+ "test:remote-workspace-helper": "cargo test --locked --manifest-path native/remote-workspace-helper/Cargo.toml",
++ "build:ocx-child": "cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml",
++ "test:ocx-child": "cargo test --locked --manifest-path native/ocx-child/Cargo.toml",
+```
+
+### 1.5 NEW Tests & Dual Test-Layout Registrations
+
+Register all three new test files in `scripts/test-layout/layout-clients.json` and `tests/fixtures/test-layout-expected.json`:
+
+1. `tests/clients/link-ephemeral-child-discovery.test.ts` (`"clients"`): POSIX & Windows candidate discovery precedence (`ocx` beats `ocx-child`; fallback selects `ocx-child`), Windows encoded PowerShell `< 8000` chars, and `OCX_PROTOCOL_COMPAT_VERSION` (`>= 2.93.0`) compatibility check via `classifyVersionProbe`.
+2. `tests/server/link-enrollment-ephemeral-child.test.ts` (`"server"`): Home `probe -> confirm-host -> apply` and `DELETE /api/link/:id` across clean detached `ocx-child`, already-attached `ocx-child` (`409`), busy-port foreign listener (`409`), and full-`ocx` standalone Child (`202`).
+3. `tests/clients/link-ocx-child-e2e.test.ts` (`"clients"` — `link-*` prefix per `seeds.json`): End-to-end integration driving the real compiled `ocx-child` Rust binary against a real in-process Home `hub-link` + management server (`RUSTUP_HOME` / `CARGO_HOME` preserved from `OCX_REAL_HOME` per `#7884`): enroll -> SSE inference relay with caller credential stripping -> Home `ocx sync` fan-out updating catalog & Home Codex settings -> daemon rollover (`startedAt >= appliedAt`) + immediate `child-parity` POST verified on Home `GET /api/link/status` -> Child `status --json` -> `--expect-link` mismatch exit `5` (`child-link-mismatch:other_link`) -> `#1798` comment-stripped `DELETE /api/link/:id` & offline `detach` -> byte-accurate restoration and zero running `ocx-child` process.
+
+## 2. Conditional-Path Activation Scenarios (`C-ACTIVATION-GROUNDING-01`)
+
+1. **Foreign listener occupying detached `ocx-child` port**: Triggered in `link-enrollment-ephemeral-child.test.ts` -> observed `409 child_restart_unsupported` before `issueKey`.
+2. **Full `ocx` precedence over `ocx-child`**: Triggered in `link-ephemeral-child-discovery.test.ts` -> observed `ocx` selected when runnable, `ocx-child` selected when `ocx` missing/broken.
+3. **Post-sync runtime rollover & OCX-6 parity**: Triggered in `ocx-child-e2e.test.ts` -> observed `startedAt >= appliedAt` and Home `/api/link/status` `parity.appliedCodexSettingsDigest` matching Home's new digest.
+4. **Wrong `--expect-link` fan-out skip**: Triggered in `ocx-child-e2e.test.ts` -> observed `classifySyncResult` returning `{ state: "skipped", reason: "child-link-mismatch:other_link" }`.
+
+## 3. Phase Verifiers
+
+- `bun test tests/clients/link-ephemeral-child-discovery.test.ts tests/server/link-enrollment-ephemeral-child.test.ts tests/clients/link-ocx-child-e2e.test.ts tests/server/link-enrollment.test.ts tests/clients/link-pinned-sync-argv.test.ts tests/clients/link-ssh-argv.test.ts tests/clients/link-ssh-argv-discovery.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts tests/lab/core-link-boundary.test.ts tests/lab/core-lab-boundary.test.ts` (exit `0`)
+- `bun run typecheck` (exit `0`)
+- `bun run privacy:scan` (exit `0`)
diff --git a/devlog/_plan/261107_ocx_child_ephemeral_link/040_wp4_docs_gates_and_pr.md b/devlog/_plan/261107_ocx_child_ephemeral_link/040_wp4_docs_gates_and_pr.md
new file mode 100644
index 00000000000..f52661835f4
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_ephemeral_link/040_wp4_docs_gates_and_pr.md
@@ -0,0 +1,52 @@
+# `wp4` — Source-of-Truth Documentation, Full Verification Matrix, & PR Publication
+
+> Continuity handoff (`LOOP-CONTINUITY-01` from `wp3` D at `a49f471b98`): `wp3` completed Home discovery, detached `ocx-child` enrollment preflight, `package.json` scripts, and the compiled-binary E2E suite (`104 pass, 0 fail`). Revalidated `040_wp4_docs_gates_and_pr.md` against `a49f471b98`: all referenced repository paths are tracked in git and `INV-LINK-CHILD-01` is present in `tests/clients/link-ocx-child-conformance.test.ts:1`. Architect consultation: covered by `ARCH-07` (`ALIGNED` on `01a1b18b-a2f3-7b53-971a-9b2c9cccecf7`, `000_roadmap.md` §3.1).
+
+- **Phase ID**: `wp4` (depends on `wp3`; satisfies `c-5`)
+- **Header dependencies**:
+ - References implemented paths `native/ocx-child/Cargo.toml`, `native/ocx-child/src/lib.rs`, `src/link/ephemeral-child.ts`, `src/link/ssh-argv.ts`, `src/link/remote-windows.ts`, `src/server/management/link-enrollment.ts`, `tests/fixtures/link/ocx-child-conformance.json`, `tests/clients/link-ocx-child-conformance.test.ts`, and `tests/clients/link-ocx-child-e2e.test.ts`.
+- **Scope**:
+ - **IN**: `structure/manifest.json` (MODIFY), `structure/remote-link.md` (MODIFY), `structure/overview.md` (MODIFY), `structure/INDEX.md` (REGENERATE via `bun run structure:index`), `docs-site/src/content/docs/guides/remote-link.md` (MODIFY), `docs-site/src/content/docs/reference/cli/lifecycle.md` (MODIFY), `devlog/_plan/261107_ocx_child_ephemeral_link/STATUS.md` (NEW/UPDATE), git push + GitHub PR targeting `dev` (unmerged).
+ - **OUT**: Merging the PR (`authority.merge = false`), force-pushing, manual `lane=all` CI dispatch.
+
+## 1. Exact File Changes
+
+### 1.1 MODIFY `structure/manifest.json`, `structure/remote-link.md`, `structure/overview.md`, & Regenerate `structure/INDEX.md`
+
+- In `structure/manifest.json` (`remote-link.md` entry): add `"native/ocx-child/"` and `"src/server/management/link-enrollment.ts"` to `documents`.
+- In `structure/remote-link.md` (185/600 lines): add present-tense contract section `## Ephemeral Rust Child (ocx-child)` naming `native/ocx-child/Cargo.toml`, `native/ocx-child/src/lib.rs`, `src/link/ephemeral-child.ts`, `src/server/management/link-enrollment.ts`, `tests/fixtures/link/ocx-child-conformance.json`, `tests/clients/link-ocx-child-conformance.test.ts`, and `tests/clients/link-ocx-child-e2e.test.ts` (all staged in git before running `bun run structure:check`).
+- In `structure/overview.md` (319/600 lines): register bound invariant `INV-LINK-CHILD-01` with `Enforced by \`tests/clients/link-ocx-child-conformance.test.ts\`` and include `// INV-LINK-CHILD-01` in `tests/clients/link-ocx-child-conformance.test.ts`.
+- Run `bun run structure:index` and verify `bun run structure:check` exits `0`.
+
+### 1.2 MODIFY `docs-site/src/content/docs/guides/remote-link.md` & `reference/cli/lifecycle.md`
+
+- Document lightweight ephemeral Child hosts (`ocx-child`), candidate discovery fallback order, clean detached enrollment, `ocx sync` fan-out parity, and 100% offline `ocx-child detach` / `disconnect` with zero background daemon.
+- Validate `cd docs-site && bun install --frozen-lockfile && bun run build` exits `0`.
+
+### 1.3 Full Verification Matrix, Push, & PR Publication
+
+1. Run fresh verification suite (serializing heavy Bun runs with `/usr/bin/lockf -k /tmp/ocx-heavy-test.lock`):
+ - `cargo fmt --manifest-path native/ocx-child/Cargo.toml --check`
+ - `cargo clippy --locked --manifest-path native/ocx-child/Cargo.toml --all-targets -- -D warnings`
+ - `cargo test --locked --manifest-path native/ocx-child/Cargo.toml`
+ - `cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml` (record exact byte size)
+ - `bun run typecheck`
+ - `bun run structure:check`
+ - `bun run skill:surface:check`
+ - `bun run privacy:scan`
+ - Focused Bun suites + `bun run test:changed`
+ - `cd docs-site && bun install --frozen-lockfile && bun run build`
+2. Push `feat/ocx-child-ephemeral-link` to `origin`.
+3. Write PR body to a `mktemp` file filling every section of `.github/PULL_REQUEST_TEMPLATE.md` (`## Summary`, `## Security Review Summary`, `## Verification`, `## Checklist`) and open PR targeting `dev` via `gh pr create --base dev --head feat/ocx-child-ephemeral-link --title ... --body-file ...`.
+4. Link PR immediately via `goat_call { path: "pull_request.link", args: { url } }`.
+5. Inspect exact-head checks; if pending/running, arm `goat_call { path: "pull_request.watch", args: { url } }` and yield cleanly without sleep-polling; once green, run `cxc loop pr-record --session 01a1b16f-bd9a-7a92-b78e-0ed532a2a346 --pr `, call `goat_call { path: "pull_request.unwatch", args: { url } }`, and leave PR unmerged.
+
+## 2. Conditional-Path Activation Scenarios (`C-ACTIVATION-GROUNDING-01`)
+
+1. **`structure:check` git-index verification**: Stage all new files before `bun run structure:check` and verify `INV-LINK-CHILD-01` binding passes.
+2. **Privacy scan on push range**: Run `bun run privacy:scan` across all committed files and verify zero findings.
+
+## 3. Phase Verifiers
+
+- All 10 commands in Section 1.3 exit `0`.
+- Exact-head GitHub PR checks succeed and `cxc loop pr-record` succeeds.
diff --git a/devlog/_plan/261107_ocx_child_ephemeral_link/050_wp5_pr_review_fixes.md b/devlog/_plan/261107_ocx_child_ephemeral_link/050_wp5_pr_review_fixes.md
new file mode 100644
index 00000000000..e9e695064f1
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_ephemeral_link/050_wp5_pr_review_fixes.md
@@ -0,0 +1,38 @@
+# `wp5` — PR #8237 Codex Review Fixes (Write-Ahead Journal, Multiline TOML Tracking, & Feature Restoration)
+
+> Continuity handoff (`LOOP-CONTINUITY-01` from `wp4` D at `fec8761583`): PR #8237 opened against `dev` with 13 CI checks green (`gates`, `structure gate`, `docs site build`, `docker smoke`, `npm-global`, `enforce-target`, `hygiene`, etc.) and received 3 valid automated Codex review findings on `native/ocx-child/` (`PRRT_kwDOS-0Gi860orZz`, `PRRT_kwDOS-0Gi860orZ4`, `PRRT_kwDOS-0Gi860orZ7`). Architect consultation: preserves `ARCH-03` & `ARCH-06` (`000_roadmap.md` §3).
+
+- **Phase ID**: `wp5` (depends on `wp4`; satisfies `c-5`)
+- **Header dependencies**:
+ - Updates `ChildJournalV1` & `apply_attach_or_sync` & `offline_detach` (`native/ocx-child/src/journal.rs`), `ChildInjectedLedger` & `mark_structural_lines` & `upsert_table_bool` & `strip_features_ledger` (`native/ocx-child/src/toml_surgeon.rs`), and adds shared conformance vectors in `tests/fixtures/link/ocx-child-conformance.json` + `tests/clients/link-ocx-child-conformance.test.ts` + `native/ocx-child/tests/conformance.rs`.
+- **Scope**:
+ - **IN**: `native/ocx-child/src/journal.rs` (MODIFY), `native/ocx-child/src/toml_surgeon.rs` (MODIFY), `native/ocx-child/tests/conformance.rs` (MODIFY), `tests/fixtures/link/ocx-child-conformance.json` (MODIFY), `tests/clients/link-ocx-child-conformance.test.ts` (MODIFY), `devlog/_plan/261107_ocx_child_ephemeral_link/001_audit_and_status.md` (MODIFY).
+ - **OUT**: Merging PR #8237.
+
+## 1. Exact Fixes
+
+1. **Finding #1 (`journal.rs:296`, P1 — Write-ahead recovery journal before overwriting Codex files)**:
+ - Split `prepare_codex_models_cache(cache_path, catalog_models) -> Result<(Option>, String), String>` so the target `models_cache.json` bytes and SHA-256 digest are computed *before* mutating any file in `$CODEX_HOME`.
+ - Call `write_journal(&paths.journal_file, &journal)?` **first** (before writing `catalog_path`, `catalog_backup_file`, `cache_path`, and `config_path`).
+ - Preserve each file's last known-owned on-disk SHA-256 (`previous_injected_config_sha256`, `previous_catalog_sha256`, `previous_models_cache_sha256`) across consecutive interrupted syncs without adopting external edits as owned, and treat `pre_attach_config_sha256` / `previous_*_sha256` under `untouched_by_external_writer` as `ExactRestore` during `offline_detach` (removing Child-created `models_cache.json` even if a sync interrupted before the next cache write).
+2. **Finding #2 (`toml_surgeon.rs:199`, P1 — Multiline TOML string/collection boundary tracking)**:
+ - Port `markStructuralLines` from `src/codex/toml-source-lines.ts:158-223` into `mark_structural_lines(lines: &[&str]) -> Vec` tracking `"""..."""` (with backslash escapes and 3–5 closing quotes), `'''...'''` (3–5 closing quotes), `[...]` depth, and `{...}` depth.
+ - Gate every structural scan (`root_end_index`, `detect_unowned_root_routing_conflict`, `external_codex_model_provider`, `insert_root_lines`, `strip_owned_root_urls`, `strip_owned_web_search`, `ensure_root_web_search`, `set_root_catalog_path`, `strip_opencodex_catalog_path`, `upsert_table_bool`, `apply_subagent_defaults`, `strip_subagent_defaults`, `strip_features_ledger`, `surgical_detach_config`, and blank-line collapsing) on `structural[i] == true` so `[Important]` or `openai_base_url = ...` inside multiline `developer_instructions` is untouched data.
+3. **Finding #3 (`toml_surgeon.rs:709`, P2 — Restore overwritten user `[features]` values on surgical detach)**:
+ - Add `#[serde(default)] pub prior_user_fast_mode_line: Option` and `#[serde(default)] pub prior_user_multi_agent_v2_line: Option` to `ChildInjectedLedger`.
+ - When `upsert_table_bool` overwrites an existing `fast_mode` or `multi_agent_v2` line in `[features]`, return `Option` of the overwritten line; on first write (`prior_ledger` had `wrote_fast_mode == None`), store that line in `prior_user_fast_mode_line` (preserving it across subsequent `sync` calls).
+ - In `strip_features_ledger`, when the current line in `[features]` still matches `wrote_fast_mode` / `wrote_multi_agent_v2`, replace it with `prior_user_*_line` if `Some(...)` (and drop it only when `None`), preserving any post-attach user modification.
+
+## 2. Activation Scenarios (`C-ACTIVATION-GROUNDING-01`)
+
+- Add 2 new shared golden vectors to `tests/fixtures/link/ocx-child-conformance.json` (verified in both `bun test` and `cargo test`):
+ 1. `multiline_developer_instructions_bracket_line`: `developer_instructions = """\n[Important]\nKeep literal\n"""\n` -> attach inserts root keys *after* the closing `"""` (not inside the string!), and surgical detach after `#1798` comment strip restores cleanly.
+ 2. `restore_overwritten_user_features_on_surgical_detach`: pre-attach `[features]\nfast_mode = false\nmulti_agent_v2 = true\n` -> attach sets `fastMode: true, multiAgentMode: "v1"` -> user appends `[extra]\nk = 1\n` -> surgical detach restores `fast_mode = false` and `multi_agent_v2 = true` while keeping `[extra]`.
+- Add `multiline_features_agents_and_blank_lines_surgical` to shared conformance vectors (`[features]`, `fast_mode = true`, repeated blank lines, and `[agents]` inside multiline string with `fastMode` and `subagentDefaults` active).
+- Add deterministic `JournalWriteFault` (`AfterJournalWrite` and `AfterCatalogWrite`) consecutive-failed-sync tests in `native/ocx-child/tests/conformance.rs` verifying `ExactRestore` of `config.toml` and `pre_attach_catalog` plus removal of previous injected `models_cache.json`.
+
+## 3. Verifiers
+
+- `cargo fmt --manifest-path native/ocx-child/Cargo.toml --check` & `cargo clippy --locked --manifest-path native/ocx-child/Cargo.toml --all-targets -- -D warnings`
+- `cargo test --locked --manifest-path native/ocx-child/Cargo.toml` & `cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml`
+- `bun test tests/clients/link-ocx-child-conformance.test.ts tests/clients/link-ocx-child-e2e.test.ts` & `bun run typecheck` & `bun run privacy:scan`
diff --git a/devlog/_plan/261107_ocx_child_plant_ux/000_roadmap.md b/devlog/_plan/261107_ocx_child_plant_ux/000_roadmap.md
new file mode 100644
index 00000000000..b541177ad66
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_plant_ux/000_roadmap.md
@@ -0,0 +1,136 @@
+# Zero-Preinstall SSH Planting, Two-Tier `--purge` Detach, and `#remote` UX Complete Lightweight Ephemeral Child Onboarding
+
+> Reader contract: For OpenCodex maintainers and reviewers evaluating stacked PR delivery on `feat/ocx-child-plant-ux` (based on `feat/ocx-child-ephemeral-link`, #8237). Document type: Decision & Roadmap Specification (`000`-range, zero implementation diffs per `LEXICO-SPLIT-01`).
+
+PR #8237 introduced `native/ocx-child` (~0.9 MB standalone Rust binary) and Home discovery/enrollment support, yet operators connecting a bare SSH devbox, container, or VM still hit `502 remote_ocx_missing` unless they manually copy `ocx-child` onto the Child first, have no one-click or `--purge` option to remove the planted binary on disconnect, receive generic conflict errors when port `10100` or `~/.codex/config.toml` is already owned externally, and see no mode or settings-parity badges in `#remote`. How can OpenCodex deliver zero-preinstall SSH Child onboarding, clean two-tier disconnect (`fast detach` vs `--purge`), actionable conflict diagnostics, and full CLI + 11-locale `#remote` UX without touching release workflows or file-size ratchets? We extend Home host inspection to classify `runtimeKind` (`full` | `ephemeral` | `missing`) and `targetTriple`, verify local `ocx-child` artifacts by SHA-256 (`src/link/child-binary-source.ts`), stream atomic SSH `stdin` plants into the Child's user cache bin directory when authorized (`plant: true`), add safe managed-path `--purge` self-removal and structured conflict markers to `native/ocx-child`, persist `childKind` on `LinkRecord`, and redesign `#remote` + `ocx link add|revoke` across all 11 locales.
+
+## Contents
+
+1. [Loop Specification & HOTL Bounds](#1-loop-specification--hotl-bounds)
+2. [Threat Model & Security Controls (`SEC-THREAT-01`)](#2-threat-model--security-controls-sec-threat-01)
+3. [Architect Consultation & Design Dispositions](#3-architect-consultation--design-dispositions)
+4. [Dependency-Ordered Work-Phase Map (`PHASE-SPLIT-01`)](#4-dependency-ordered-work-phase-map-phase-split-01)
+5. [Cross-Boundary Field & Type Chains (`PLAN-FIELD-CHAIN-01`)](#5-cross-boundary-field--type-chains-plan-field-chain-01)
+6. [Early-Warning & Boundary Guard Disclosure (`PLAN-BYPASS-NAMED-01`)](#6-early-warning--boundary-guard-disclosure-plan-bypass-named-01)
+7. [Grounded Verifiers (`PLAN-VERIFIER-REAL-01`)](#7-grounded-verifiers-plan-verifier-real-01)
+8. [Non-Goals, Alternatives Rejected, & Consequences](#8-non-goals-alternatives-rejected--consequences)
+
+## 1. Loop Specification & HOTL Bounds
+
+| Field | Specification |
+|---|---|
+| Loop archetype | `satisfy-spec` (Class C4 multi-cycle HOTL PABCD with docs-first `wp0` entry per `LOOP-DOCS-FIRST-01`) |
+| Trigger | Dispatched GOAT task lane `lane-ocx-child-plant-ux` on branch `feat/ocx-child-plant-ux` stacked on open PR #8237 (`feat/ocx-child-ephemeral-link` at `c4d4d5d6311905bea62f065d7d04b9de7cdb5a86`) |
+| Goal | Deliver zero-preinstall SSH binary planting, `--purge` managed-path removal, actionable `child_port_busy` / `child_external_provider_conflict` diagnostics, `LinkRecord.childKind` persistence/projection, CLI `--plant` / `--purge` flags, `#remote` GUI redesign across all 11 locales, `pr-assets` UI screenshots, and an unmerged stacked PR |
+| Non-goals | Merging the PR (`authority.merge = false`), editing `.github/workflows/` or `scripts/release.ts`, touching `src/lab/` or `core-lab-boundary` protected files (`src/router.ts`, `src/server/lifecycle.ts`, `src/server/responses/core.ts`), raising file-size ratchets, committing PNGs onto `feat/ocx-child-plant-ux`, dispatching `lane=all` CI, mutating shared `.codexclaw` subagent configs, or calling `web_search` |
+| Verifier | Grounded commands in [Section 7](#7-grounded-verifiers-plan-verifier-real-01): `cargo test` + `cargo build --release --locked`, `bun run typecheck`, `bun run test:changed`, `cd gui && bun run lint:i18n && bun run lint && bun test tests && bun run build`, `bun run build:gui`, `bun run structure:check`, `cd docs-site && bun run build`, `bun run privacy:scan`, and `gh pr checks` |
+| Stop condition | All work-phases `wp0..wp4` and criteria `c-1..c-5` met with fresh receipts, UI screenshots pushed only to `origin/pr-assets`, stacked PR opened with `DEV-STACK-03` table, exact-head PR checks green and recorded via `cxc loop pr-record`, `goat_pull_request_unwatch` called, and `update_goal({ status: "complete" })` accepted |
+| Memory artifact | `devlog/_plan/261107_ocx_child_plant_ux/` (`000_roadmap.md` + `010..040` PRDs) and `.codexclaw/goalplans/loop-archetype-satisfy-spec-class-c4-hotl-multi/goalplan.json` |
+| Expected terminal outcomes | `DONE`: all `c-1..c-5` verified + green unmerged stacked PR recorded; `NOOP`: N/A; `BLOCKED`: reproducible CI/policy/toolchain refusal reported with `file:line` and command output without weakening checks; `UNSAFE`: unmitigated binary integrity, symlink, or path-deletion hazard; `NEEDS_HUMAN`: out-of-scope authority required; `BUDGET_EXHAUSTED`: host limit reached |
+| Escalation condition | Any requirement to edit outside `writeScope` (`native/ocx-child`, `src/link`, `src/server/management`, `src/cli`, `gui`, `structure`, `docs-site`, `scripts`, `tests`, `devlog`), weaken a check, commit images to the PR branch, or merge. Subagents inherit `google-antigravity/gemini-4-argon` per user directive |
+| HOTL resource bounds | Token/cost/wall-clock bounds: `none stated; host limits apply`. Tool/credential scope: local git/bun/cargo, headless browser for local `#remote` screenshot capture, `origin/pr-assets` upload, `gh pr create` / `gh pr view` / `gh pr checks`, GOAT PR link/watch/unwatch |
+
+## 2. Threat Model & Security Controls (`SEC-THREAT-01`)
+
+1. **Assets**:
+ - Remote Child host filesystem integrity (`$HOME/.cache/opencodex/bin/ocx-child`, `%LOCALAPPDATA%\opencodex\bin\ocx-child.exe`, `$OPENCODEX_HOME/child/*`, `~/.codex/*`).
+ - Integrity and authenticity of the `ocx-child` binary streamed from Home to Child over SSH `stdin`.
+ - Operator consent before mutating a bare remote host (`plant: true` / `--plant` / explicit confirmation).
+ - Existing SSH host-key pinning (`StrictHostKeyChecking=yes`, private `known_hosts`, `BatchMode=yes`, `ClearAllForwardings=yes`) and Windows `OcxJob` process containment.
+2. **Entrypoints & Trust Boundaries**:
+ - Home binary artifact resolution (`src/link/child-binary-source.ts`) reading `OPENCODEX_CHILD_BIN_DIR`, sidecar directories, local `target/release`, or `$OPENCODEX_HOME/cache/ocx-child//`.
+ - Home `POST /api/link/probe`, `POST /api/link/confirm-host`, `POST /api/link/apply`, and `DELETE /api/link/:id` (`src/server/management/link-enrollment.ts`, `link-routes.ts`).
+ - SSH `stdin` binary write & execution on POSIX (`sh -c`) and Windows (`powershell.exe -EncodedCommand` under `OcxJob`).
+ - `ocx-child detach --purge` / `disconnect --purge` self-removal (`native/ocx-child/src/cli.rs`, `platform.rs`).
+3. **Attacker Capabilities & Hazard Scenarios**:
+ - Corrupted, truncated, symlinked, or unverified `ocx-child` binary in cache/override directory streamed onto a Child.
+ - Pre-existing symlink at `$HOME/.cache/opencodex/bin` or `$HOME/.cache/opencodex/bin/ocx-child` (or Windows ReparsePoint) tricking plant or `--purge` into overwriting or deleting an unintended file.
+ - `ocx-child detach --purge` invoked on a system/package-manager/user path (`/usr/local/bin/ocx-child`, `~/.local/bin/ocx-child`, Cargo target dir) deleting a binary Home did not plant.
+ - Non-interactive CLI automation running `ocx link add --expect-fingerprint ` against a bare host without intending to install software on it.
+4. **Controls**:
+ - **SHA-256 artifact verification & regular-file checks**: `child-binary-source.ts` rejects symlinks (`lstatSync`), group/world-writable binaries on POSIX, empty/oversized (> 32 MiB) files, and requires exact `SHA256SUMS` verification for cached/distributed artifacts (computing SHA-256 over the exact read `Buffer` streamed to `stdin` to eliminate TOCTOU between hash and stream).
+ - **Explicit `plant` authorization**: `POST /api/link/apply` never plants unless `body.plant === true`; non-TTY `ocx link add` refuses bare hosts unless `--plant` is passed alongside `--expect-fingerprint`; GUI shows an explicit Ready-to-Plant card with architecture and byte size.
+ - **Symlink-refusing atomic plant & Windows `OcxJob` containment**: POSIX plant runs under `umask 077`, refuses symlinked parent/target (`[ ! -L ... ]`), writes to `.ocx-child.tmp.$$.`, verifies exact byte count and `chmod 0700`, atomically `mv -f`s into `$HOME/.cache/opencodex/bin/ocx-child`, and verifies `--version` exit `0`. Windows plant runs inside packed PowerShell + `OcxJob` (`<= 8000` UTF-16LE base64 chars), rejects `ReparsePoint` attributes on parent/target, verifies exact byte length and SHA-256, moves atomically into `%LOCALAPPDATA%\opencodex\bin\ocx-child.exe`, and verifies `[OcxJob]::Ok`.
+ - **Post-plant rollback**: If `apply` fails after planting a fresh binary in that enrollment attempt (`plantedFreshBinary === true`), Home runs bounded cleanup (`ocx-child detach --purge` / managed-path removal) before returning the error so failed enrollments leave zero orphan binaries or daemons.
+ - **Managed-path-only `--purge`**: `native/ocx-child` inspects `current_exe` without following a final symlink (`symlink_metadata`), verifies the parent directory matches ONLY the managed plant location (`*/.cache/opencodex/bin/ocx-child` on POSIX; `*\opencodex\bin\ocx-child.exe` or `*/.cache/opencodex/bin/ocx-child.exe` on Windows), refuses symlinks/reparse points and foreign paths (treating non-managed paths as a safe no-op binary keep with a notice), removes `$OPENCODEX_HOME/child` only via `fs::remove_dir` (which fails safely if non-empty), and on Windows renames `ocx-child.exe` aside in the same directory before spawning a short detached `cmd.exe /d /c` deleter.
+
+## 3. Architect Consultation & Design Dispositions
+
+- **Proposals consulted**:
+ - Initial read-only architect handle `01a1b2ea-432d-7001-8efc-89edb28881db` (retired after 145s timeout with zero workspace mutations per `DISPATCH-RETIRE-01`) and live read-only architect handle `01a1b2ed-664f-7180-99b1-fd1091864296` (both inherited `google-antigravity/gemini-4-argon`).
+- **Main dispositions on `ARCH-01..ARCH-06`**:
+ - **`ARCH-01` (Amended)**: Adopt `allowMissing?: boolean` on `POST /api/link/probe` (when `includeVersion: true`) and `POST /api/link/confirm-host`, returning `runtimeKind: "full" | "ephemeral" | "missing"`, `targetTriple: "darwin-arm64" | "darwin-x64" | "linux-x64" | "linux-arm64" | "windows-x64" | null`, `ocxVersion: string | null`, `plantable: boolean`, `binarySizeBytes?: number`, and `plantUnavailableReason?: string`. **Amendment**: When `allowMissing !== true`, `confirm-host` preserves the exact legacy `remoteOcxArgv(["--version"], remotePlatform)` call and `{ alias, fingerprint, ocxVersion }` response shape so `tests/server/link-enrollment.test.ts:90` and Find Home (`role === "child"`) remain 100% unchanged; when `allowMissing === true`, a single framed inspection command (`src/link/remote-plant.ts`) reports OS/arch, selected candidate kind (`full` vs `ephemeral` vs `missing`), and `--version` output in one SSH round-trip.
+ - **`ARCH-02` (Accepted)**: Implement `src/link/child-binary-source.ts` resolving `(1)` `OPENCODEX_CHILD_BIN_DIR`, `(2)` sidecar directories (`native-bin//` and beside `process.execPath`), `(3)` local `native/ocx-child/target/release/ocx-child(.exe)` when `targetTriple === hostTriple`, and `(4)` `$OPENCODEX_HOME/cache/ocx-child//` verified against `SHA256SUMS`. Read bytes once into memory and return `{ bytes, sizeBytes, sha256, targetTriple, sourceKind }`.
+ - **`ARCH-03` (Amended)**: Implement POSIX and Windows SSH `stdin` atomic plant and post-plant failure rollback in `POST /api/link/apply` (`plant?: boolean`). **Amendment**: Reject reordering `REMOTE_OCX_CANDIDATES` / `OCX_CANDIDATES` ahead of full `ocx` when `child/state.json` is absent — `structure/remote-link.md:187` and `tests/clients/link-ephemeral-child-discovery.test.ts` lock the invariant that full `ocx` wins when detached while `child/state.json` pins `ocx-child` once attached. On a bare host no full `ocx` exists, so `$HOME/.cache/opencodex/bin/ocx-child` / `%LOCALAPPDATA%\opencodex\bin\ocx-child.exe` is already discovered first and then pinned by `child/state.json`.
+ - **`ARCH-04` (Accepted)**: Implement `--purge` on `ocx-child detach` and `ocx-child disconnect` (`cli.rs` + `platform.rs`) with strict managed-path and non-symlink verification, empty `$OPENCODEX_HOME/child` directory removal, and Windows rename-aside + detached `cmd.exe /d /c` self-delete. Add deterministic conflict markers (`ocx-child-conflict:child_port_busy`, `ocx-child-conflict:child_external_provider_conflict`) and map both Home preflight and `connect` failures to `409 child_port_busy` and `409 child_external_provider_conflict` with zero Child file mutations.
+ - **`ARCH-05` (Amended)**: Persist optional `childKind?: "full" | "ephemeral"` on `LinkRecord` (`src/link/store.ts`), project `childKind` on `LinkStatusDto["links"][number]` (`src/link/status-projection.ts`, omitting when undefined on legacy records so legacy exact-object fixtures stay untouched while GUI defaults legacy hub-initiated rows to `"full"`), expose `homeCodexSettingsDigest` on `GET /api/link/status` for `gui-session` callers (`src/server/management/link-routes.ts:300`), and extend `DELETE /api/link/:id` body to `{ force?: boolean, purge?: boolean }` (rejecting `force: true` combined with `purge: true` as `400 invalid_body`, and running `remoteOcxArgv(["disconnect", "--purge"], platform)` when `purge === true` on `record.childKind === "ephemeral"`).
+ - **`ARCH-06` (Accepted)**: Extend `ocx link add [--plant]` and `ocx link revoke [--purge]`, extract modular GUI components (`gui/src/pages/RemoteLinkSheet.tsx` and `gui/src/pages/RemoteLinkDialogs.tsx`) so `RemoteLink.tsx` shrinks cleanly, style via `gui/src/styles-remote-link.css` (leaving baseline-capped `gui/src/styles.css` untouched), and update all 11 locale catalogs (`en`, `ko`, `ja`, `zh`, `zh-TW`, `de`, `fr`, `pt`, `ru`, `tr`, `vi`) in small 2–3 file batches.
+
+### 3.1 Same-Architect Reflection Record
+
+- **Submitted concrete plan revision**: `devlog/_plan/261107_ocx_child_plant_ux/{000_roadmap.md,010_wp1_rust_purge_and_conflicts.md,020_wp2_home_plant_store_and_api.md,030_wp3_cli_and_gui_remote_ux.md,040_wp4_docs_screenshots_and_pr.md}`.
+- **Reflection handle `01a1b2ed-664f-7180-99b1-fd1091864296` verdict**: `ALIGNED` (zero material gaps).
+- **Decision-ID mapping verified**: `ARCH-01` -> `000` §3, §5 + `010` §2.3 + `020` §2.1–§2.2; `ARCH-02` -> `000` §2–§3 + `010` §2.3; `ARCH-03` -> `000` §2–§3 + `010` §2.3 + `020` §2.2; `ARCH-04` -> `000` §3, §5 + `010` §2.1–§2.2 + `020` §2.2; `ARCH-05` -> `000` §3, §5 + `010` §2.3 + `020` §2.3; `ARCH-06` -> `000` §3 + `030` §1–§3 + `040` §1–§2.
+
+## 4. Dependency-Ordered Work-Phase Map (`PHASE-SPLIT-01`)
+
+| Work Phase | Decade Doc | Architectural Layer | Depends On | Key Deliverables & Verifiable Close |
+|---|---|---|---|---|
+| `wp0` | `000_roadmap.md`, `010..040` | Specification & Audit | — | Diff-level PRDs, architect reflection, independent A-gate review, verifier grounding; closes `D -> IDLE` with zero production edits |
+| `wp1` | `010_wp1_rust_purge_and_conflicts.md` | Rust `ocx-child` & `src/link/` Foundations | `wp0` | `native/ocx-child` `--purge` + conflict markers; `LinkRecord.childKind`; `child-binary-source.ts`; `remote-plant.ts` + `remote-windows.ts` inspect/plant argv builders; Cargo + Bun unit tests green |
+| `wp2` | `020_wp2_home_plant_store_and_api.md` | Home Management API & Status Projection | `wp1` | `probe` / `confirm-host` (`allowMissing`), `apply` (`plant` + rollback + `childKind` + `409` conflict codes), `status` (`childKind` + `homeCodexSettingsDigest`), `DELETE /api/link/:id` (`purge`); server & E2E plant/purge tests green |
+| `wp3` | `030_wp3_cli_and_gui_remote_ux.md` | CLI & `#remote` Dashboard UX + 11 Locales | `wp2` | `ocx link add --plant`, `ocx link revoke --purge`, `RemoteLink.tsx` + sibling components, Ready-to-Plant card, mode/parity badges, conflict notices, Disconnect `--purge` modal, 11 locales; CLI + GUI `lint:i18n`, `lint`, `test`, `build` green |
+| `wp4` | `040_wp4_docs_screenshots_and_pr.md` | SoT Docs, `pr-assets` Screenshots & Stacked PR | `wp3` | `structure/remote-link.md` + `docs-site/` sync, full gates, real `#remote` PNGs pushed ONLY to `origin/pr-assets`, stacked PR opened against `feat/ocx-child-ephemeral-link` (#8237), CI/review watch to green, `cxc loop pr-record` |
+
+## 5. Cross-Boundary Field & Type Chains (`PLAN-FIELD-CHAIN-01`)
+
+1. **`LinkChildKind` (`"full" | "ephemeral"`)**:
+ - **Creation**: `src/server/management/link-enrollment.ts` (`apply` sets `childKind: ephemeralChild ? "ephemeral" : "full"` on hub-initiated `LinkRecord`). Client-initiated `/api/link/issue` sets `childKind: "full"` (legacy records omit).
+ - **Serialization**: `src/link/store.ts` (`writeLinkStore` writes `links.json` with `"childKind"` added to `RECORD_KEYS`).
+ - **Deserialization**: `src/link/store.ts` (`parseRecord` validates `raw.childKind === undefined || raw.childKind === "full" || raw.childKind === "ephemeral"`).
+ - **Consumers**: `src/link/status-projection.ts` (`projectLinkStatus` copies optional `childKind` onto `LinkStatusDto["links"][number]`); `src/server/management/link-routes.ts` (`remove` checks `purge === true && record.childKind === "ephemeral"`); `src/cli/link.ts` (`validateStatus` accepts optional `"childKind"`, `runRevoke` uses status/store `childKind` to print `~/.cache/opencodex/bin/ocx-child detach` recovery on `--force`); `gui/src/remote-link-api.ts` (`parseRemoteLinkStatus` parses `childKind`); `gui/src/pages/RemoteLink.tsx` & `RemoteLinkDialogs.tsx` (Connected row badge, Disconnect `--purge` checkbox visibility, Force-remove command chip).
+2. **`RemoteRuntimeKind` (`"full" | "ephemeral" | "missing"`) & `ChildTargetTriple` (`"darwin-arm64" | "darwin-x64" | "linux-x64" | "linux-arm64" | "windows-x64"`)**:
+ - **Creation**: `src/link/remote-plant.ts` (`inspectRemoteChildRuntime` parses framed SSH stdout + `resolveChildBinarySource`).
+ - **Serialization**: `src/server/management/link-enrollment.ts` (`probe` and `confirmHost` JSON responses when `allowMissing === true`, plus `ConfirmedLinkHost` in-memory state).
+ - **Deserialization**: `src/cli/link-enrollment.ts` (`offered` & confirm validator) and `gui/src/remote-link-api.ts` (`parseConfirmation`).
+ - **Consumers**: `src/server/management/link-enrollment.ts` (`apply` requires `plant === true` when `confirmed.runtimeKind === "missing"`, streams `targetTriple` binary); `src/cli/link-enrollment.ts` (TTY plant prompt vs non-TTY `--plant` gate); `gui/src/pages/RemoteLinkSheet.tsx` (Ready-to-Plant card vs Cached Ephemeral card vs Full OpenCodex card).
+3. **Conflict Error Codes (`"child_port_busy"` | `"child_external_provider_conflict"` | `"remote_plant_failed"`)**:
+ - **Creation**: `native/ocx-child/src/daemon.rs` (`ensure_port_available_for_attach`), `native/ocx-child/src/toml_surgeon.rs` (`detect_unowned_root_routing_conflict`), and `src/server/management/link-enrollment.ts` (`apply` preflight/plant/connect error mapping).
+ - **Serialization**: `src/server/management/link-route-state.ts` (`fail(code, message, 409 | 502, hint)`).
+ - **Deserialization**: `src/cli/link-enrollment.ts` (`PHASES` / specific error messages) and `gui/src/remote-link-api.ts` (`LINK_ERROR_CODES` + `LinkApiError`).
+ - **Consumers**: `gui/src/pages/RemoteLink.tsx` (`ERROR_TKEY` -> localized `Notice` across all 11 `gui/src/i18n/*.ts` files) and `src/cli/link-enrollment.ts` (human-readable stderr recovery).
+4. **Home Parity Digest on GUI Status (`homeCodexSettingsDigest` + `appliedCodexSettingsDigest`)**:
+ - **Creation**: `src/server/management/link-routes.ts` (`GET /api/link/status` computes `codexSettingsDigest(projectHomeCodexSettings(ctx.config, "0.0.0").settings)` for `ctx.principal === "gui-session"`, alongside existing `LinkChildParity.appliedCodexSettingsDigest`).
+ - **Serialization**: `Response.json(body)` on `GET /api/link/status` (omitted on `admin-token` responses so strict CLI `validateStatus` top-level keys stay unchanged).
+ - **Deserialization**: `gui/src/remote-link-api.ts` (`parseRemoteLinkStatus` extracts `homeCodexSettingsDigest: string | null` and per-row `appliedCodexSettingsDigest: string | null`).
+ - **Consumers**: `gui/src/pages/RemoteLink.tsx` (renders "Home settings synced" / "Sync needed" badge when both digests are non-null on a connected Child row).
+
+## 6. Early-Warning & Boundary Guard Disclosure (`PLAN-BYPASS-NAMED-01`)
+
+| Guard / Control | Tier | Executing Surface | Known Bypass Path | Residual Risk | Wording Policy | Final Layer |
+|---|---|---|---|---|---|---|
+| Managed-path check on `ocx-child detach --purge` | E2 (Runtime check) | `native/ocx-child/src/platform.rs` (`purge_managed_executable`) | Local Child user can manually `rm` any file they own over SSH | None for foreign paths: `ocx-child` refuses to unlink symlinks or paths outside `.cache/opencodex/bin` / `AppData\Local\opencodex\bin` | Described as a safety guard on `--purge`, never OS-level immutability | `native/ocx-child` `symlink_metadata` + parent path suffix match |
+| `SHA256SUMS` check in `child-binary-source.ts` | E2 (Runtime check) | `src/link/child-binary-source.ts` | Same local OS user setting `OPENCODEX_CHILD_BIN_DIR` or rebuilding `native/ocx-child/target/release` locally | Local Home process user already owns SSH keys to Child; protects against corrupted/tampered downloads or cache files | Called artifact integrity verification against `SHA256SUMS` / local build | Exact byte-count + SHA-256 check on the Child plant script |
+| Non-TTY `--plant` requirement in `ocx link add` | E2 (CLI check) | `src/cli/link-enrollment.ts` + `POST /api/link/apply` (`plant === true`) | Caller with admin token or paired GUI session explicitly passing `"plant": true` | Intentional: `plant: true` is the explicit opt-in contract | Called explicit plant opt-in guard | `POST /api/link/apply` server check (`body.plant === true`) |
+
+## 7. Grounded Verifiers (`PLAN-VERIFIER-REAL-01`)
+
+Every verifier below was executed or verified against the repository scripts on `2026-11-07`:
+
+1. `cargo test --locked --manifest-path native/ocx-child/Cargo.toml` — baseline ran (`exit 0`, 8/8 tests passed); directly compiles and tests `native/ocx-child/src/**/*.rs` and `native/ocx-child/tests/*.rs`.
+2. `cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml` — builds the real `native/ocx-child/target/release/ocx-child` binary consumed by `child-binary-source.ts` and E2E SSH plant/purge tests.
+3. `bun run typecheck` (`package.json` -> `bun check --no-script --noEmit`) — baseline ran (`exit 0`, 2,319 files checked); directly typechecks `src/**/*.ts`, `tests/**/*.ts`, and `gui/src/**/*.tsx` including all 11 `gui/src/i18n/*.ts` `TKey` exhaustiveness checks.
+4. `cd gui && bun run lint:i18n` (`oxlint src/pages src/components src/App.tsx src/main.tsx src/ui.tsx src/provider-workspace-data.ts`) — baseline ran (`exit 0`); enforces zero hardcoded UI strings in `gui/src/pages/RemoteLink*.tsx` and locale catalog parity.
+5. `cd gui && bun run lint && bun test tests && bun run build` (plus root `bun run build:gui`) — directly lints, runs `gui/tests/remote-link.test.tsx` + `remote-link-route.test.tsx`, and bundles `gui/dist`.
+6. `bun run test:changed` + explicit focused suites (`bun test tests/clients/link-*.test.ts tests/server/link-*.test.ts tests/cli/cli-link*.test.ts tests/ci-workflows/file-size-ratchet.test.ts tests/ci-workflows/repo-hygiene.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts`) — exercises module-graph + subprocess/layout/ratchet/hygiene gates.
+7. `bun run skill:surface:check` — baseline ran (`exit 0`); verifies `src/cli/capabilities*.ts` and `skills/ocx/` stay synchronized.
+8. `bun run structure:check` — baseline ran (`exit 0`); verifies `structure/remote-link.md`, `structure/manifest.json`, and `structure/INDEX.md`.
+9. `cd docs-site && bun run build` — Astro + Starlight build verifying `docs-site/` remote-link user docs.
+10. `bun run privacy:scan` — baseline ran (`exit 0`); scans tracked `src/`, `gui/`, `tests/`, `docs-site/`, and `devlog/` for accidental credentials or PII.
+
+## 8. Non-Goals, Alternatives Rejected, & Consequences
+
+- **Rejected SFTP/SCP upload for `ocx-child` planting**: OpenSSH `scp -O` vs SFTP protocol availability varies across minimal Alpine/BusyBox containers and restricted Windows OpenSSH setups, whereas `ssh -T ... sh -c` / `-EncodedCommand` with piped `stdin` uses the exact transport already proven by `probe` and `connect --key-stdin` and works 100% air-gapped.
+- **Rejected modifying `.github/workflows/` or `scripts/release.ts`**: Out of lane write scope and requires maintainer release-pipeline security sign-off; `child-binary-source.ts` cleanly supports `OPENCODEX_CHILD_BIN_DIR`, packaged sidecars, local `target/release`, and `SHA256SUMS`-verified `$OPENCODEX_HOME/cache/ocx-child//` today.
+- **Rejected committing UI screenshots onto `feat/ocx-child-plant-ux`**: Prohibited by `AGENTS.md` and `tests/ci-workflows/repo-hygiene.test.ts`; screenshots are committed strictly to `origin/pr-assets` and referenced by immutable commit SHA in the PR description.
+
diff --git a/devlog/_plan/261107_ocx_child_plant_ux/001_audit_and_status.md b/devlog/_plan/261107_ocx_child_plant_ux/001_audit_and_status.md
new file mode 100644
index 00000000000..675a92bd818
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_plant_ux/001_audit_and_status.md
@@ -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 |
+
diff --git a/devlog/_plan/261107_ocx_child_plant_ux/010_wp1_rust_purge_and_conflicts.md b/devlog/_plan/261107_ocx_child_plant_ux/010_wp1_rust_purge_and_conflicts.md
new file mode 100644
index 00000000000..338b39a1f9b
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_plant_ux/010_wp1_rust_purge_and_conflicts.md
@@ -0,0 +1,197 @@
+# Work-Phase 1 (`wp1`): `native/ocx-child` `--purge` & Conflict Markers + `src/link/` Foundations
+
+> Depends on: `000_roadmap.md` (`ARCH-01..ARCH-05`). Consumed types: `LinkRecord`, `SshRunner`, `packedPowerShellArgv`, `ChildPaths`. Exported types: `LinkChildKind`, `ChildTargetTriple`, `RemoteRuntimeKind`, `ChildBinarySource`, `RemoteRuntimeInspection`.
+
+## 1. File Change Map
+
+| Action | Path | Summary |
+|---|---|---|
+| MODIFY | `native/ocx-child/src/platform.rs` | Add `PurgeExecutableOutcome`, `is_managed_plant_executable(exe_path, user_home, local_appdata)`, and `purge_managed_executable(exe_path, user_home, local_appdata)` (refusing symlinks/reparse points and foreign paths; POSIX `fs::remove_file`; Windows rename-aside + detached `cmd.exe /d /c` cleanup) |
+| MODIFY | `native/ocx-child/src/daemon.rs` | Prefix `ensure_port_available_for_attach` conflict errors with `ocx-child-conflict:child_port_busy:` so Home maps foreign port ownership deterministically to `409 child_port_busy` |
+| MODIFY | `native/ocx-child/src/toml_surgeon.rs` | Prefix `detect_unowned_root_routing_conflict` external provider / user `openai_base_url` errors with `ocx-child-conflict:child_external_provider_conflict:` |
+| MODIFY | `native/ocx-child/src/cli.rs` | Extend `cmd_detach` (`detach` / `disconnect`) to accept `--purge` (reject unknown flags with exit `2`), drop `FileLock` and remove `lock_file` + empty `$OPENCODEX_HOME/child` dir, and run `purge_managed_executable` when `--purge` is passed |
+| MODIFY | `native/ocx-child/tests/relay_and_lifecycle.rs` | Add Rust integration tests for `detach --purge` (managed path removed + empty `child` dir removed; foreign path preserved; symlink refused) and structured `child_port_busy` / `child_external_provider_conflict` stderr prefixes |
+| MODIFY | `src/link/store.ts` | Add `export type LinkChildKind = "full" | "ephemeral"` and optional `childKind?: LinkChildKind` to `LinkRecord` + `"childKind"` in `RECORD_KEYS` |
+| NEW | `src/link/child-binary-source.ts` | Resolve `ocx-child` binary for `ChildTargetTriple` across `OPENCODEX_CHILD_BIN_DIR`, sidecar dirs, `native/ocx-child/target/release` (when `targetTriple === hostChildTargetTriple()`), and `$OPENCODEX_HOME/cache/ocx-child//` with `SHA256SUMS` verification |
+| MODIFY | `src/link/remote-windows.ts` | Add `windowsInspectRuntimeArgv()` and `windowsPlantChildArgv(expectedBytes, expectedSha256)` + `windowsPurgePlantedChildArgv()` staying strictly below `POWERSHELL_ENCODED_MAX = 8000` inside `OcxJob` |
+| NEW | `src/link/remote-plant.ts` | Implement `ChildTargetTriple`, `RemoteRuntimeKind`, `parseTargetTriple`, `remoteInspectRuntimeArgv`, `inspectRemoteChildRuntime`, `remotePlantChildArgv`, `remotePurgePlantedChildArgv`, and `classifyChildAttachConflict` |
+| NEW | `tests/clients/link-child-plant-foundations.test.ts` | Unit tests for `child-binary-source.ts` (`SHA256SUMS` pass/mismatch, symlink refusal, `OPENCODEX_CHILD_BIN_DIR`, host release fallback), `LinkRecord.childKind` roundtrip, and POSIX/Windows inspect & plant argv budgets (`<= 8000`) |
+| MODIFY | `scripts/test-layout/layout-clients.json` & `tests/fixtures/test-layout-expected.json` | Register `tests/clients/link-child-plant-foundations.test.ts` |
+
+## 2. Diff-Level Specifications
+
+### 2.1 `native/ocx-child/src/platform.rs` & `cli.rs` (`--purge`)
+
+```diff
+--- a/native/ocx-child/src/platform.rs
++++ b/native/ocx-child/src/platform.rs
+@@ -876,5 +876,82 @@ pub fn resolve_codex_home() -> PathBuf {
+ if let Some(custom) = std::env::var_os("CODEX_HOME").filter(|v| !v.is_empty()) {
+ return PathBuf::from(custom);
+ }
+ resolve_user_home().join(".codex")
+ }
++
++#[derive(Debug, Clone, PartialEq, Eq)]
++pub enum PurgeExecutableOutcome {
++ Removed,
++ SkippedUnmanagedPath,
++}
++
++pub fn is_managed_plant_executable(
++ exe_path: &Path,
++ user_home: &Path,
++ local_appdata: Option<&Path>,
++) -> Result {
++ reject_symlink(exe_path)?;
++ let Some(parent) = exe_path.parent() else { return Ok(false); };
++ reject_symlink(parent)?;
++ let expected_name = if cfg!(windows) { "ocx-child.exe" } else { "ocx-child" };
++ if exe_path.file_name().and_then(|n| n.to_str()) != Some(expected_name) {
++ return Ok(false);
++ }
++ let canon_parent = fs::canonicalize(parent)
++ .map_err(|e| format!("failed canonicalizing {}: {e}", parent.display()))?;
++ let cache_bin = user_home.join(".cache").join("opencodex").join("bin");
++ if fs::canonicalize(&cache_bin).ok().as_ref() == Some(&canon_parent) {
++ return Ok(true);
++ }
++ if let Some(appdata) = local_appdata {
++ let win_bin = appdata.join("opencodex").join("bin");
++ if fs::canonicalize(&win_bin).ok().as_ref() == Some(&canon_parent) {
++ return Ok(true);
++ }
++ }
++ Ok(false)
++}
++
++pub fn purge_managed_executable(
++ exe_path: &Path,
++ user_home: &Path,
++ local_appdata: Option<&Path>,
++) -> Result {
++ if !is_managed_plant_executable(exe_path, user_home, local_appdata)? {
++ return Ok(PurgeExecutableOutcome::SkippedUnmanagedPath);
++ }
++ #[cfg(unix)]
++ {
++ fs::remove_file(exe_path)
++ .map_err(|e| format!("failed removing planted ocx-child {}: {e}", exe_path.display()))?;
++ Ok(PurgeExecutableOutcome::Removed)
++ }
++ #[cfg(windows)]
++ {
++ use std::os::windows::process::CommandExt;
++ const CREATE_NO_WINDOW: u32 = 0x0800_0000;
++ const DETACHED_PROCESS: u32 = 0x0000_0008;
++ let parent = exe_path.parent().ok_or_else(|| "missing parent".to_owned())?;
++ let trashed = parent.join(format!(".ocx-child.purge.{}.old", std::process::id()));
++ let _ = fs::remove_file(&trashed);
++ fs::rename(exe_path, &trashed)
++ .map_err(|e| format!("failed renaming planted ocx-child for purge: {e}"))?;
++ let cmd_line = format!(
++ "ping -n 2 127.0.0.1 >nul & del /f /q "{}"",
++ trashed.display()
++ );
++ let _ = Command::new("cmd.exe")
++ .args(["/d", "/c", &cmd_line])
++ .stdin(Stdio::null())
++ .stdout(Stdio::null())
++ .stderr(Stdio::null())
++ .creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
++ .spawn();
++ Ok(PurgeExecutableOutcome::Removed)
++ }
++}
+```
+
+In `native/ocx-child/src/cli.rs` (`cmd_detach`):
+- Parse `--keep-catalog`, `--json`, `--purge`; reject unknown flags with exit `2`.
+- Hold `FileLock` during daemon stop, `offline_detach`, and removal of `key_file` + `state_file`; then explicitly `drop(lock)`, remove `paths.lock_file` and attempt `fs::remove_dir(&paths.child_dir)` (ignoring non-empty `Err`, plus `fs::remove_dir(&paths.opencodex_home)` only when empty) so a detached ephemeral Child leaves zero stale files in `$OPENCODEX_HOME/child/`.
+- When `purge` is true, resolve `current_exe` (using `OCX_CHILD_SELF_EXE_FOR_TEST` override only under explicit test env or `std::env::current_exe()` — better: factor `detach_with_exe(args, current_exe_override: Option<&Path>)` for Rust library integration tests without any env-var backdoor in production!) and call `purge_managed_executable`, reporting `"purged": true | false` in JSON output.
+
+### 2.2 Conflict Markers in `daemon.rs` and `toml_surgeon.rs`
+
+```diff
+--- a/native/ocx-child/src/daemon.rs
++++ b/native/ocx-child/src/daemon.rs
+@@ -80,14 +80,14 @@ pub fn ensure_port_available_for_attach(port: u16, owned_pid: Option) -> Re
+ return Err(format!(
+- "attach refused: 127.0.0.1:{port} is already served by foreign listener (pid {})",
++ "ocx-child-conflict:child_port_busy: attach refused: 127.0.0.1:{port} is already served by foreign listener (pid {})",
+ health.pid
+ ));
+ }
+ if TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port))).is_err() {
+ return Err(format!(
+- "attach refused: 127.0.0.1:{port} is already in use"
++ "ocx-child-conflict:child_port_busy: attach refused: 127.0.0.1:{port} is already in use"
+ ));
+ }
+```
+
+```diff
+--- a/native/ocx-child/src/toml_surgeon.rs
++++ b/native/ocx-child/src/toml_surgeon.rs
+@@ -399,7 +399,7 @@ pub fn detect_unowned_root_routing_conflict(content: &str) -> Result<(), String>
+ if let Some(ext) = external_codex_model_provider(content) {
+ return Err(format!(
+- "attach refused: an external Codex model_provider ({ext}) owns config.toml"
++ "ocx-child-conflict:child_external_provider_conflict: attach refused: an external Codex model_provider ({ext}) owns config.toml"
+ ));
+ }
+```
+(Apply the exact `ocx-child-conflict:child_external_provider_conflict:` prefix to all three conflict branches in `detect_unowned_root_routing_conflict` — `external_codex_model_provider` at `toml_surgeon.rs:399`, unmarked `openai_base_url` / `experimental_realtime_ws_base_url` at `toml_surgeon.rs:421`, and unmarked user-owned `model_catalog_json` at `toml_surgeon.rs:433` — preserving `"attach refused:"` so all existing tests stay green.)
+
+### 2.3 `src/link/store.ts`, `src/link/child-binary-source.ts`, and `src/link/remote-plant.ts`
+
+1. `src/link/store.ts`:
+ ```diff
+ +export type LinkChildKind = "full" | "ephemeral";
+ +
+ export interface LinkRecord {
+ ...
+ createdAt: string;
+ remotePlatform?: "posix" | "windows";
+ + childKind?: LinkChildKind;
+ }
+ -const RECORD_KEYS = new Set(["id", "alias", "direction", "hostKeyFingerprint", "tunnelPort", "apiKeyId", "createdAt", "remotePlatform"]);
+ +const RECORD_KEYS = new Set(["id", "alias", "direction", "hostKeyFingerprint", "tunnelPort", "apiKeyId", "createdAt", "remotePlatform", "childKind"]);
+ ```
+2. `src/link/child-binary-source.ts`:
+ - `export type ChildTargetTriple = "darwin-arm64" | "darwin-x64" | "linux-x64" | "linux-arm64" | "windows-x64"`
+ - `export type PlantUnavailableReason = "unsupported_target" | "artifact_not_found" | "checksum_mismatch"`
+ - `resolveChildBinarySource(targetTriple: ChildTargetTriple | null, options?: { env?: NodeJS.ProcessEnv; hostPlatform?: NodeJS.Platform; hostArch?: string; opencodexHome?: string; version?: string })`:
+ - Returns `{ ok: true, artifact: { targetTriple, bytes: Uint8Array, sizeBytes: number, sha256: string, source: "env" | "sidecar" | "local-release" | "cache" } }` or `{ ok: false, reason: PlantUnavailableReason }`.
+ - Artifact naming: `ocx-child-${targetTriple}${isWin ? ".exe" : ""}` (and in `OPENCODEX_CHILD_BIN_DIR` also accepts `/${targetTriple}/ocx-child[.exe]` when `SHA256SUMS` lists either).
+ - `SHA256SUMS` requirement: for `env`, `sidecar`, and `cache` sources, requires a sibling `SHA256SUMS` file in the candidate directory containing `<64hex> ` (or `*`), verifies `lstatSync` is a regular non-symlink file (`<= 32 MiB`, `> 0` bytes, mode `& 0o022 === 0` on POSIX), reads the file once into `Uint8Array`, computes SHA-256 over those exact bytes, and returns `checksum_mismatch` if it diverges! For `local-release` (`native/ocx-child/target/release/ocx-child[.exe]` when `targetTriple === hostChildTargetTriple()`), verifies regular non-symlink file & permissions and hashes the read bytes directly.
+3. `src/link/remote-plant.ts` & `src/link/remote-windows.ts`:
+ - `remoteInspectRuntimeArgv(platform)`:
+ - POSIX `sh -c`: emits `ocx-inspect:uname::`, evaluates candidate order identical to `REMOTE_OCX_SCRIPT`, distinguishes `full` vs `ephemeral` (selected path basename starting with `ocx-child` or `"$selected" --help` containing `(ocx-child`), and emits `ocx-inspect:kind:` + `ocx-inspect:version:` with exit `0`.
+ - Windows `packedPowerShellArgv`: emits `ocx-inspect:arch:<$env:PROCESSOR_ARCHITECTURE>`, checks `OCX_CANDIDATES`, and emits `ocx-inspect:kind:` + `ocx-inspect:version:` with exit `0`.
+ - `remotePlantChildArgv(platform, expectedBytes, expectedSha256)`:
+ - POSIX `sh -c`: `umask 077`, checks `[ ! -L "$HOME/.cache/opencodex" ] && [ ! -L "$HOME/.cache/opencodex/bin" ] && [ ! -L "$HOME/.cache/opencodex/bin/ocx-child" ]`, `mkdir -p "$HOME/.cache/opencodex/bin"`, `chmod 0700 "$HOME/.cache/opencodex/bin"`, writes stdin to `$tmp`, verifies `[ "$actual_bytes" = "$expected_bytes" ]`, verifies SHA-256 via `sha256sum` or `shasum -a 256` when available, `chmod 0700 "$tmp"`, `mv -f "$tmp" "$dest"`, and runs `"$dest" --version`.
+ - Windows `packedPowerShellArgv` (`< 8000` chars): inside `NATIVE_PROCESS` (`OcxJob`), resolves `$dir = if($env:LOCALAPPDATA){"$env:LOCALAPPDATA\opencodex\bin"}else{"$env:USERPROFILE\.cache\opencodex\bin"}`, refuses `ReparsePoint` on `$dir` or `$dest`, reads `[Console]::OpenStandardInput()` bounded to `expectedBytes + 1`, checks exact length and `[Security.Cryptography.SHA256]` lowercase hex digest against `expectedSha256`, moves atomically to `$dest`, and runs `[OcxJob]::Run($dest, '"'+$dest+'" --version')`.
+
+## 3. Conditional Activation Scenarios (`C-ACTIVATION-GROUNDING-01`)
+
+- `--purge` on managed path vs foreign path vs symlink: Rust integration test invokes `detach_with_exe` on `.cache/opencodex/bin/ocx-child` (deletes binary + empty `child` dir), `/tmp/custom/ocx-child` (preserves binary), and symlinked `.cache/opencodex/bin/ocx-child` (refuses unlink).
+- `SHA256SUMS` mismatch & symlink in `child-binary-source.ts`: unit test writes tampered `SHA256SUMS` -> observes `{ ok: false, reason: "checksum_mismatch" }`; writes symlink -> refused.
+- `child_port_busy` & `child_external_provider_conflict`: Rust test binds a dummy TCP listener on `listen_port` or writes `model_provider = "custom"` -> observes exact `ocx-child-conflict:...` stderr prefix and untouched `config.toml`.
+
+## 4. `wp1` P-Phase Continuity & Architect Reflection Amendments (`01a1b2fe-fb7d-7f00-b3c9-756fbf5e0e0c`)
+
+- **Continuity from `wp0` D**: Resumed from `wp0` D conclusion ("Verified wp0 docs-only roadmap; next cycle wp1 starts foundations").
+- **Architect `01a1b2fe-fb7d-7f00-b3c9-756fbf5e0e0c` verdict**: `ALIGNED` on `W1-ARCH-01..W1-ARCH-03`.
+- **Folded refinements for `B` implementation**:
+ 1. `src/link/store.ts` (`parseRecord`): validate `raw.childKind === undefined || raw.childKind === "full" || raw.childKind === "ephemeral"` and preserve `childKind` on the returned `LinkRecord` so `writeLinkStore` roundtrips it cleanly.
+ 2. `native/ocx-child/src/platform.rs`: verify `fs::symlink_metadata(exe_path)` is an existing regular file (`file_type().is_file()`), and on Windows pass `OCX_PURGE_TARGET` via `.env("OCX_PURGE_TARGET", &trashed)` to `cmd.exe /d /c "ping -n 2 127.0.0.1 >nul & del /f /q \"%OCX_PURGE_TARGET%\""` with `CREATE_BREAKAWAY_FROM_JOB | CREATE_NO_WINDOW | DETACHED_PROCESS`.
+ 3. `src/link/child-binary-source.ts`: verify `lstatSync` non-symlink status on candidate directory, `SHA256SUMS` (`<= 256 KiB`), and binary (`<= 32 MiB`, `> 0`, POSIX `(mode & 0o022) === 0`).
+ 4. `src/link/remote-plant.ts` & `src/link/remote-windows.ts`: include `remotePurgePlantedChildArgv(platform)` (invoking `detach --purge` when runnable and unlinking the managed binary after non-symlink/non-ReparsePoint checks) and `classifyChildAttachConflict(stderr)` matching `ocx-child-conflict:child_port_busy:` and `ocx-child-conflict:child_external_provider_conflict:`.
+
+## 5. `wp1` A-Phase Audit Fold-Backs (`01a1b309-5460-7c93-855b-d59ef8182224`, `GO-WITH-FIXES`)
+
+1. **`B1` (Folded — Windows `OcxJob` silent breakaway & `.old` sweep)**: Because `OcxJob` sets `JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK` without `JOB_OBJECT_LIMIT_BREAKAWAY_OK`, `purge_managed_executable` on Windows spawns `cmd.exe` with `CREATE_NO_WINDOW | DETACHED_PROCESS` (silent breakaway succeeds automatically; if a caller has explicit breakaway only, fallback is automatic), and both `windowsOcxArgv` (on `disconnect --purge`) and `windowsPurgePlantedChildArgv` sweep `.ocx-child.purge.*.old` in the managed bin dir after `Invoke-Native` exits.
+2. **`B2` (Folded — Managed ancestor symlink/reparse checks)**: Canonicalize `user_home` / `local_appdata` root once, then reject symlinks/reparse points on every managed subpath component (`.cache`, `.cache/opencodex`, `.cache/opencodex/bin`, `ocx-child` and `opencodex`, `opencodex/bin`, `ocx-child.exe`) in Rust `is_managed_plant_executable` as well as POSIX and Windows plant/purge scripts.
+3. **`B3` (Folded — `SHA256SUMS` fail-closed precedence)**: In `resolveChildBinarySource`, when a candidate binary exists in `env`, `sidecar`, or `cache` and fails `SHA256SUMS` (`checksum_mismatch`) or symlink/permission checks (including POSIX `(mode & 0o022) === 0` on `SHA256SUMS`), return `{ ok: false, reason: "checksum_mismatch" }` immediately without falling through to `local-release`. Also include `windowsPurgePlantedChildArgv()` in the `<= 8000` encoded-command budget test.
+
diff --git a/devlog/_plan/261107_ocx_child_plant_ux/020_wp2_home_plant_store_and_api.md b/devlog/_plan/261107_ocx_child_plant_ux/020_wp2_home_plant_store_and_api.md
new file mode 100644
index 00000000000..2ac68ff1277
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_plant_ux/020_wp2_home_plant_store_and_api.md
@@ -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.
+
diff --git a/devlog/_plan/261107_ocx_child_plant_ux/030_wp3_cli_and_gui_remote_ux.md b/devlog/_plan/261107_ocx_child_plant_ux/030_wp3_cli_and_gui_remote_ux.md
new file mode 100644
index 00000000000..d8c42f9edb1
--- /dev/null
+++ b/devlog/_plan/261107_ocx_child_plant_ux/030_wp3_cli_and_gui_remote_ux.md
@@ -0,0 +1,124 @@
+# Work-Phase 3 (`wp3`): CLI (`--plant`, `--purge`) & Dashboard `#remote` UX Across All 11 Locales
+
+> Depends on: `000_roadmap.md`, `010_wp1_rust_purge_and_conflicts.md`, `020_wp2_home_plant_store_and_api.md`. Consumed types: `LinkStatusDto`, `LinkChildKind`, `ChildTargetTriple`, `RemoteRuntimeKind`, `PlantUnavailableReason`.
+
+## 1. File Change Map
+
+| Action | Path | Summary |
+|---|---|---|
+| MODIFY | `src/cli/link-enrollment.ts` | Add `--plant` to `ocx link add`, probe/confirm with `allowMissing: true`, interactive TTY bare-host plant prompt (`Confirm — plant lightweight ocx-child () and connect? (type yes): `), non-TTY `--plant` requirement on bare hosts, and actionable messages for `child_port_busy` / `child_external_provider_conflict` / `remote_plant_*` |
+| MODIFY | `src/cli/link.ts` | Update `LINK_USAGE` (keeping `src/cli/capabilities-base.ts` untouched so `skills/ocx/references/*` outside `writeScope` stays current), accept optional `childKind` in `validateStatus`, add `--purge` to `ocx link revoke` (mutually exclusive with `--force`), and emit `~/.cache/opencodex/bin/ocx-child detach` recovery guidance on `--force` when `childKind === "ephemeral"` |
+| MODIFY | `gui/src/remote-link-api.ts` | Add `child_port_busy`, `child_external_provider_conflict`, `remote_plant_failed`, `remote_plant_unavailable` to `LINK_ERROR_CODES`; extend `LinkConfirmHostView`, `LinkRowWire` (`childKind`, `appliedCodexSettingsDigest`), and `RemoteLinkStatusWire` (`homeCodexSettingsDigest`) |
+| NEW | `gui/src/pages/RemoteLinkSheet.tsx` | Extracted Add Child / Find Home `