diff --git a/.gitignore b/.gitignore index ce10233dcc..f32218aafd 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,6 @@ tests/**/.tmp-* # `git add` three separate times and reached `dev` once — see # tests/ci-workflows/repo-hygiene.test.ts, which fails if any path here becomes tracked again. go/ + +# Rust native helpers keep their reproducible sources and lockfile in git, never local artifacts. +native/**/target/ diff --git a/.npmignore b/.npmignore index acf3a0c4d0..cfbe1d3750 100644 --- a/.npmignore +++ b/.npmignore @@ -19,6 +19,7 @@ gui/eslint.config.* gui/bun.lock # misc +native/remote-workspace-helper/target/ *.test.ts *.map .DS_Store diff --git a/devlog/_plan/260912_remote_workspace_carry/020_executor_runtime.md b/devlog/_plan/260912_remote_workspace_carry/020_executor_runtime.md index 550b2a5bb5..e20200c7c3 100644 --- a/devlog/_plan/260912_remote_workspace_carry/020_executor_runtime.md +++ b/devlog/_plan/260912_remote_workspace_carry/020_executor_runtime.md @@ -82,3 +82,25 @@ REMOTE-ARCH-003: Separate persisted enrollment capabilities from current connect REMOTE-ARCH-006: Use existing required private-file/Windows ACL primitives for new identity and bearer stores. Check permission setup failures and refuse loading/saving secrets when enforcement fails. Do not change global config-store behavior. Record exact selected existing helper in phase-2 P after reading the owner; no best-effort function is accepted as proof. REMOTE-ARCH-007: Codex real App Server tests depend on OCX_CODEX_BIN; Claude real integration on OCX_CLAUDE_BIN; Pi on OCX_PI_BIN. The Linux confinement case can return without execution unless OCX_REQUIRE_LINUX_REMOTE_WORKSPACE_CONFINEMENT=1 or bwrap is available. Current generic CI alone does not prove those paths. Mock tests prove lifecycle and tool-routing contracts only; native Hub isolation and executor confinement stay explicit final acceptance gaps when not activated. For each adapter separately record denied local tools, inherited plugins/hooks/config, offline refusal and teardown; inspect source plus hosted mocks, no claims of live CLI confinement from flags alone. + +## Phase-2 revalidation and exact owner choices + +Previous D: wp1 inactive foundation source cycle complete at 726ddc7fc0; final hosted proof remains wp4. Continue in child branch codex/260912-60plus-remote-runtime. Existing public exports and added host-negative coverage are retained. + +REMOTE-ARCH-004: storage modules import atomicWriteFile directly from src/config/atomic-write.ts and getConfigDir from src/config/paths.ts, avoiding the broad config.ts barrel. Device CLI orchestration retains explicit runner construction because it computes actual availability after root approval; no import-time probe exists. This is intentional sequential coupling. Server seams in phase 3 use narrow structural connection/session interfaces rather than pulling concrete remote classes into shared request types. No remote module imports server surfaces. + +REMOTE-ARCH-006 exact helpers: NEW src/remote-control/workspace-secret-store.ts owns prepareWorkspaceSecretDirectory(directory) and hardenWorkspaceSecretFile(path). On POSIX use chmodSync with propagated failure and lstat directory/file identity/type checks. On Windows call existing src/lib/windows-secret-acl.ts hardenSecretDir/hardenSecretPath with required:true. Reject symlink state targets. All three stores use this before reads and before atomicWriteFile. Existing atomic-write.ts already creates an empty private descriptor, hardens before writing bytes, and scrubs failures; retain it. Tests: NEW tests/clients/remote-workspace-secret-store.test.ts covers owner-only POSIX file mode, unexpected path types/symlinks and failed reads; hosted Windows ACL owner tests remain applicable. No global config behavior changes. + +src/lib/windows-atomic-replace.ts change is the new ReplacePublisher literal remote-workspace (the function is already exported). Use existing counter serialization/consumers unchanged: creation at executor write, diagnostic key serialization, dynamic record readers; no closed switch to extend. + +NEW tests/clients/remote-workspace-session-binding.test.ts covers session/device/root/capability mismatches with zero execution and a valid positive control, using encrypted messages and independent fixtures. MODIFY agent-wire, hub, sessions and device tests to assert subset negotiation and presence intersection. Platform runner source retains existing fail-closed native paths; remove stale comment claiming supported macOS commands. + +### Audit amendment: store-level failure propagation + +Hub/Device/Session file-store constructors accept an optional narrow permissions dependency containing prepareDirectory and hardenFile, defaulting to the required production helper. Load returns null for absent files; existing files require directory and file checks before secret reads. Save prepares directory, hardens an existing target, then invokes the existing private atomic writer. For each store, injected directory/file hardening throws must propagate, preserve existing bytes and prevent secret IO. New-state first-run controls return null then save/load valid fixtures. Add all three store cases to remote-workspace-secret-store.test.ts; this injection observes caller ordering rather than relying on ACL-owner tests alone. + +### Native containment amendment + +Independent source review requires a protected Linux bubblewrap executable outside writable roots, with identity revalidation before use. Custom executable files and their parent chain must not be writable by group/other; canonical system symlinks are resolved before checking. Workspace roots cannot contain the executable; every invocation rechecks. Add source/runner regression fixtures without claiming a local run. + +Windows command availability remains disabled in this carry: nativeRemoteWorkspaceCommandRunnerAvailable returns false before invoking the helper, and the official Windows helper rejects public probe/run without allocating OS resources. The candidate Windows implementation remains in original PR history; do not retain callable unverified entrypoints. This matches the fail-closed macOS policy and preserves independently authorized file tools. Update native denial tests and docs; Windows working-command acceptance stays OPEN. A future lifecycle owner and hosted cancellation/cleanup evidence are required before re-enablement. This is a safety limitation, not completion of Windows commands. diff --git a/devlog/_plan/260912_unimplemented_trio_stack/000_plan.md b/devlog/_plan/260912_unimplemented_trio_stack/000_plan.md new file mode 100644 index 0000000000..611dbac17c --- /dev/null +++ b/devlog/_plan/260912_unimplemented_trio_stack/000_plan.md @@ -0,0 +1,108 @@ +# Trio stack: WS stage instrumentation, native-main device reauth, paginated history recovery + +Unit 260912_unimplemented_trio_stack. HOTL loop goalplan slug +`implement-three-unimplemented-opencodex-backlog` (session +01a09616-38e6-72e0-b5bf-99eb10ce58a6). Bottom-up manual stacked-PR chain +against `dev` (lidge-jun/opencodex). No merges, no GitHub native-stack +registration. Every push uses `git push --no-verify`; local product +suite/build/typecheck/install NOT RUN; each PR relies on hosted exact-head +CI and says so in its Verification section. + +## Objective + +Close the three implementable unimplemented backlog items identified in the +2026-09-12 inventory: + +1. Issue #4191 — WS 1006 / response-prelude-timeout diagnosis has no durable + content-free evidence. Ship stage instrumentation only; no fix, no + auto-retransmit fallback. +2. Issue #3898 — headless hub cannot reauth native `__main__` because + deviceauth is pool-only. Ship the native-main device reauth API/CLI, then + the main-card Re-login GUI on top of it. +3. Issue #4311 residual — paginated history still has no writer support and + no recovery for ordinal-corrupted rollouts. Ship the offline recovery + tool with preservation proofs; live writes stay refused. + +## Sources + +- #4191 body: content-free stage diagnostics list; A/B evidence that the + failure is proxy-path-specific; related #2471, #4083, #3976. +- #3898 body: suggested contract (reuse OpenAI deviceauth, persist to native + main slot, keep `__main__` out of `/api/codex-auth/login`, no codex + binary/keyring requirement, secret-free DTOs). +- #4311 body: ordinal-0 clone defect (now guarded), incident recovery by + ordinal-digit rewrite while Codex was closed, prohibition of N+1 guessing + and live rewrites. +- devlog/_plan/260912_accounts/080_reauth_api.md and 090_reauth_ui.md — + accepted Accounts-lane design drafts this unit adopts for L2/L3. +- devlog/_plan/260912_history_containment/ — refusal contract this unit + must preserve. + +## Constraints (hard) + +- L1 logs stay content-free: create-frame byte count, send completion, + close code (numeric), elapsed/first-frame timings, frame counters, OCX and + Bun versions. No conversation text, no headers, no close-reason text, no + account identifiers in the new records. +- L1 adds no `responseCommitted === false` auto-retransmit: turn + duplication risk is documented in #4191 discussion. +- L2 keeps `/api/codex-auth/login` rejecting `__main__` (400), keeps pool + Add/Re-login semantics unchanged, and must not route the native flow + through `startLoginFlow("chatgpt")` (scratch-slot overwrite + pool + singleflight collision, src/oauth/index.ts:1899-1973). +- L2 commit to `$CODEX_HOME/auth.json` only under an exclusive claim with + path/hash/inode assertion and same-identity verification; never retains + old identity token beside new credentials; fails safe + (`native_main_unavailable`) when no fence can be established. +- L3 must not reuse `AddCodexAccountModal` or `openReauth("__main__")`; + dedicated hook and dedicated backend namespace only (the pool login route + rejects `__main__` at src/codex/account-id.ts:15-20). +- L4 must not invent last-ordinal+1, must not write to a live rollout, must + not weaken `history_paginated_requires_native_writer` refusal in + preflight/apply/restore paths, and must preserve every non-ordinal byte. +- All layers: focused tests land with the layer; every new test file gets + layout.json `explicit` + tests/fixtures/test-layout-expected.json + entries in the same PR. +- structure/ ownership: any owned source area changed by a layer updates + its structure doc in the same PR (structure/AGENTS.md). + +## Work-phase map (dependency order = stack order, bottom first) + +| WP | Layer | Branch | PR base | Decade doc | +|----|-------|--------|---------|------------| +| wp2 | L1 #4191 WS stage instrumentation | codex/260912-ws-stage-instrumentation | dev | 010 | +| wp3 | L2 #3898 native-main reauth API/CLI | codex/260912-native-main-reauth-api | wp2 branch | 020 | +| wp4 | L3 #3898 main-card Re-login GUI | codex/260912-native-main-reauth-ui | wp3 branch | 030 | +| wp5 | L4 #4311 paginated history recovery | codex/260912-native-paginated-writer | wp4 branch | 040 | + +Dependency logic: L2 and L3 are one feature split at the API/UI seam +(030 depends on 020's route). L1 is independent but touches the shared +request-log schema, so it sits at the bottom where later layers rebase onto +a stable log contract. L4 is the riskiest (user data) and rides on top so +lower layers can land without waiting for it. There is no functional +dependency between L1/L2 and L4; the chain exists to serialize review. + +## Verification policy per layer + +- Red-first focused tests, then implementation, then green. +- `bun test tests//` (or `cd gui && bun test tests/` + for L3) fresh at C, captured via `cxc receipt test`. +- Full local suite/build/typecheck/install: NOT RUN (standing rule); each + PR Verification section labels this and names the hosted exact-head CI + run as the integration evidence. Cancelled/skipped CI never counts as + passing. +- L4 additionally: privacy-relevant paths (rollout bytes) stay in tests + with synthetic fixtures only. + +## Open decisions carried to audit + +1. L2 hub fence: on a headless hub the native owner never activates + (src/server/index.ts:1026-1046 + src/codex/desired-state.ts:79-81). + 020 resolves how commit fencing works there without weakening the + exclusive-claim contract; audit must confirm the chosen fence. +2. L4 scope: true live-write support needs a Codex-owned writer API that + does not exist in this tree. This unit ships the offline recovery tool + and keeps live refusal; the PR description must say so explicitly. +3. L3 screenshot evidence: obtained from hosted CI artifacts or recorded + exemption, per repo PR gate (title/body mentions of gui need a + screenshot). diff --git a/devlog/_plan/260912_unimplemented_trio_stack/001_baseline_revalidation.md b/devlog/_plan/260912_unimplemented_trio_stack/001_baseline_revalidation.md new file mode 100644 index 0000000000..327dc4285c --- /dev/null +++ b/devlog/_plan/260912_unimplemented_trio_stack/001_baseline_revalidation.md @@ -0,0 +1,22 @@ +# Baseline revalidation (wp1 B-phase) + +Independent main-session spot check of the citations folded in by the wp1 +audit (0268727f82), re-run against the working tree at B. Every folded +reference was opened and read; results below. All verified TRUE. + +| Claim | Where verified | Result | +|-------|----------------|--------| +| `assertNativeMainOwner` throws without a held owner entry | src/codex/native-main-owner.ts:302-314 — throws NATIVE_MAIN_OWNER_UNAVAILABLE/BUSY (503) unless snapshot held | TRUE | +| Exclusive claim is owner-independent (FS/SQLite lock) | src/codex/native-main-claim.ts:167 — `withNativeMainExclusiveClaim(context, operation, options)`, claim/release around operation, no owner lookup | TRUE | +| `shouldSyncCodexOnStart` is composed at desired-state.ts:130 | src/codex/desired-state.ts:130 — exported function; comment names the hub rule | TRUE | +| Pool login writes `isMain: false` | src/codex/auth-api.ts:2934,2939 — both update and add paths set `isMain: false` | TRUE | +| Paginated guard throws the structured reason | src/codex/history-provider.ts:1172 — `CodexHistoryIntegrityError("history_paginated_requires_native_writer")` on `ordinal` key or `history_mode === "paginated"` | TRUE | +| State DB resolution | src/codex/paths.ts:106-109 — `resolveCodexStateDbPath` joins sqlite root + state_5.sqlite | TRUE | +| `startLoginFlow` location | src/oauth/index.ts:1899 — export begins | TRUE | +| GUI modal state | gui/src/components/CodexAccountPool.tsx:75 (`showAdd`), :94 (`reauthId`), :651-654 (modal mount) | TRUE | + +Consequence for implementation cycles: 020's hub-fence resolution stands +as amended — the native-main reauth fence MUST NOT call +`assertNativeMainOwner`; the owner-independent exclusive claim plus +path/hash/inode and recovery/admission rechecks is the whole fence, and +claim/admission failure alone maps to `native_main_unavailable`. diff --git a/devlog/_plan/260912_unimplemented_trio_stack/010_l1_ws_stage_instrumentation.md b/devlog/_plan/260912_unimplemented_trio_stack/010_l1_ws_stage_instrumentation.md new file mode 100644 index 0000000000..72cfd8c3c2 --- /dev/null +++ b/devlog/_plan/260912_unimplemented_trio_stack/010_l1_ws_stage_instrumentation.md @@ -0,0 +1,142 @@ +# L1: content-free Codex WS upstream stage instrumentation (#4191) + +Class C2. Stack bottom, base `dev`. Branch +`codex/260912-ws-stage-instrumentation`. Diagnosis instrumentation only: +no behavior change to success paths, no retry/fallback change. +Second revision: folds the wp2 A-audit FAIL (2 blockers, 2 majors, 1 +minor) into the design. First revision's `recordCodexWsStage`-on-context +design is retracted — the exchange has no `RequestLogContext` +(codex-ws-exchange.ts:11-18,85) and cannot get one without inverting +layers. + +## Problem + +#4191 fails as WS 1006 or "response prelude timed out" only through the +proxy. The content-free stage record already exists as +`CodexWsFailureStage` (src/server/responses/codex-ws-wire.ts:100-144) and +`failureStage()` (src/server/responses/codex-ws-exchange.ts:148-159), but +it is only interpolated into failure message strings. Durable logs keep +neither the message nor a typed code: the eager relay collapses stream +errors to `upstream_reset` + `streamAborted` (wire.ts:218-229 comment; +relay.ts:1417-1430), and the 504 pre-response JSON path never reaches the +relay at all. `/api/logs` and usage.jsonl are explicit per-field copies, +so a field added only to `RequestLogContext` is dropped on write and on +restart hydrate. + +## Contract (from #4191 + maintainer bounds) + +Record, per upstream exchange: create-frame bytes, send completion, +close code (numeric only), elapsed ms and first-frame ms, frame counters +(upstream/control/relayed, pings/pongs), pool reuse boolean, OCX version, +Bun runtime version. Never record conversation text, headers, close-reason +text, or account identifiers. No `responseCommitted === false` +auto-retransmit fallback. Client CLI version is not on the handshake +(`user-agent` is not in FORWARD_HEADERS, +src/adapters/openai-responses.ts:43-61) — the limitation is documented in +the PR, not worked around by parsing `frameText`. + +## Changes + +MODIFY `src/server/responses/codex-ws-wire.ts` +- New exported type `CodexWsStageRecord = + Omit & { + requestBytes: number | null; closeCode: number | null; reused: boolean; + ocxVersion: string; bunVersion: string }` (Omit, not an intersection — + an intersection cannot widen `requestBytes`). Extend the privacy + comment: numeric/boolean/semver fields only; close-reason text stays out + of every durable record. +- New `markCodexWsStage(response, record)` / `readCodexWsStage(response)` + over a `WeakMap` — the same + Response-marker seam `markCodexWsResponse` already uses. +- `ocxVersion` comes from a module-local package.json IIFE, the exact + pattern already duplicated in management-api.ts:87-93, gui-static.ts:6-9, + client/machine-listener.ts:21, update/index.ts:147. Do NOT import + management-api (layer inversion + cycle). + +MODIFY `src/server/responses/codex-ws-exchange.ts` +- `ExchangeOptions` gains optional `bunVersion?: string` and nothing + else; no context, no callback registry. +- Snapshot once in `failStream` (the funnel every failure site already + calls: armSilence :206, connect-deadline :256, onClose :426, onError + :437, and the onMessage sites :330-402) and once in `commitResponse` + (:160). After the existing settle decision, call + `markCodexWsStage(response, record)` on the Response being resolved — + both the SSE 200 and the `codexWsPreResponseFailure` JSON paths resolve + a Response, so one marker covers success and failure. +- `requestBytes`: computed at failure time only (current deferred + behavior). On the committed-success record it is `null` — the happy + path must not byte-count megabyte replay frames (the deferral comment at + :143-147 is the contract). +- `closeCode` is captured in `onClose` from the event (numeric only) and + carried into the `failStream` call it makes; other sites pass `null`. +- `reused` is `session.reused`; `bunVersion` from the new option. +- No control-flow change at any site: emissions happen after the settle + decision, never instead of it. + +MODIFY `src/server/responses/ws-upstream.ts` +- Pass `bunVersion: typeof runtime === "string" ? runtime : runtime.version` + (the gate input at :62-64 may be a plain string) through + `codexWsUpstreamFetch` into `codexWsExchange`. Signature gain is one + optional field. + +MODIFY `src/server/responses/core.ts` +- Adopt the stage onto the attempt at the handleResponses send path, not + only at `retryCodexPoolOnAlternateAccount` (:1532-1556 is the pool + retry, not the primary send): `readCodexWsStage(upstreamResponse)`; + when present assign `logCtx.activeAttempt.codexWsStage`. Apply at every + adopted `upstreamResponse`: the primary send (:5304-5320), the + post-retry assignment (:5824) — or once on the final response after the + ladder (~5758); B picks the single funnel that covers every adopted + response and tests it. This covers the 504/502 pre-response JSON path + that never reaches relay.ts, and needs no relay.ts change: the relay + collapse only sets `streamAborted` alongside the stage. (First + revision's relay.ts MODIFY is retracted.) + +MODIFY `src/usage/log.ts` +- `PersistedUsageAttempt` gains `codexWsStage?: CodexWsStageRecord` + with a comment naming #4191 and the content-free invariant. +- Attempt serializer allowlist (:445-480 region): carry `codexWsStage` + through a `normalizeCodexWsStageRecord` guard (numeric fields via + isNonNegativeFiniteNumber-style checks, booleans strictly, versions as + capped semver strings, `requestBytes: number | null`) so a hand-edited + row cannot inject strings into the DTO. +- `normalizeUsageEntry` (:527-612) carries it via the attempts + normalization above; no entry-level copy (stage is per-attempt). + +MODIFY `src/server/request-log.ts` +- `RequestLogEntry` needs no new field: `attempts` already projects. + `requestLogEntryFromPersistedUsage` (:280-330) keeps copying + `attempts` wholesale. Verify `addFinalRequestLog` (:1037-1086) passes + the attempt objects (with the stage) into `addLog` — if it re-derives + attempt rows field-by-field, add `codexWsStage` there instead. B + confirms which of the two attempt paths is authoritative and tests it. + +## Tests (red-first) + +MODIFY `tests/responses/ws-upstream.test.ts` +- Through `handleResponses` (the :399-408 pattern — the only path that + owns a logCtx): upstream 1006 persists `codexWsStage` on the logged + attempt with `closeCode: 1006` and `sent: true`; prelude-timeout + persists `firstFrameMs: null`, `upstreamFrames: 0`; a committed + success records exactly one stage with `requestBytes: null`. +MODIFY `tests/responses/ws-failure-stage.test.ts` +- Record carries closeCode/reused/versions; the serialized record never + contains reason text, header names, or body substrings. +NEW `tests/usage/usage-log-ws-stage.test.ts` +- Round trip: `normalizeUsageEntry` + attempt serializer keep a valid + stage; corrupt stage shapes (string frames, object closeCode) are + dropped, not passed through. layout.json explicit + + tests/fixtures/test-layout-expected.json entries (domain `usage`). + +## Docs / ownership + +L1 touches owned `src/server/responses/*`, `src/usage/log.ts`, and +`src/server/request-log.ts`: sync structure/transports/responses.md and +structure/runtime.md in this PR (structure:check must stay green). + +## Out of scope + +Any WS behavior fix, SSE-fallback policy change, prelude-timeout tuning +(#3976/#4083), pool policy, inbound client-socket metrics +(codexWebSocketAdmissionMetrics is the client side — do not touch), +auto-retransmit on `responseCommitted === false`. diff --git a/devlog/_plan/260912_unimplemented_trio_stack/020_l2_native_main_reauth_api.md b/devlog/_plan/260912_unimplemented_trio_stack/020_l2_native_main_reauth_api.md new file mode 100644 index 0000000000..bde091a235 --- /dev/null +++ b/devlog/_plan/260912_unimplemented_trio_stack/020_l2_native_main_reauth_api.md @@ -0,0 +1,138 @@ +# L2: native-main device reauth API/CLI (#3898) + +Class C4 (auth boundary). Stack layer 2, base the L1 branch. Branch +`codex/260912-native-main-reauth-api`. Adopts the accepted Accounts-lane +design devlog/_plan/260912_accounts/080_reauth_api.md; this doc is the +diff-level revalidation of that draft against current `dev` plus the +deltas the code map surfaced. 080 remains the contract source; anything +here overrides stale details of 080, not its invariants. + +## Problem + +Headless hub (`runtimeRole=hub`, `oauthOpenBrowser: false`, no codex +binary, no keyring) cannot reauth native `__main__`: +`/api/codex-auth/login` is pool-only and rejects `__main__` +(src/codex/account-id.ts:15-20; src/codex/auth-api.ts:2733-2748); +`ocx account main add` requires official `codex login` + OS keyring +(src/cli/account-main.ts:73-90,214-260). WHAM `token_revoked` on the main +grant is then unrecoverable from the hub. + +## Changes (080 contract, revalidated) + +MODIFY `src/oauth/chatgpt-device.ts` +- Factor the private grant exchange so a native-only result retains the + raw validated token payload: new `loginChatGPTNativeDevice` returns + `{ credential, idToken }` in-process only; reject missing + access/refresh/id token or mismatched account identity. Existing + `loginChatGPTDevice` behavior unchanged (still projects + OAuthCredentials, no id_token). +- Delta from 080 (explorer-confirmed gap): the usercode/poll/token fetches + (84-90, 121-127, 152-163) have no per-request timeout — only the 15-min + poll deadline and abort. Add a service-owned per-fetch deadline (fetch + + body) so a stuck TCP cannot hold the flow until TTL. This is the Kuhn + blocker "poll timer does not bound fetch/body deadlines". + Audit-folded: one FRESH 30s timeout per fetch attempt inside the poll + loop (AbortSignal.any([ctrl.signal, AbortSignal.timeout(30_000)]), the + main-account.ts:239-241 pattern) — a single 30s signal across the whole + poll would kill the 15-minute grant. Abort-timeout maps to + device_authorization_failed. The shared helper also bounds hung POOL + device logins at 30s per fetch — an intended improvement, called out in + the PR. + +MODIFY `src/codex/main-account.ts` +- New `beginNativeMainReauth`: captures the existing + `MainAuthJsonCredential` snapshot (103-136) into a private closure; + returned commit accepts complete native device tokens and, only after + human authorization, acquires `withNativeMainExclusiveClaim` + (src/codex/native-main-claim.ts:167), rechecks recovery/admission fence, + asserts original path/hash/inode before atomic rename, requires same + chatgpt account identity, writes access+refresh+id token + account_id + together, advances the mutation epoch, and reconciles runtime/quota + state. Old identity token is never retained beside new credentials. No + claim held during human polling. + Audit-folded: do NOT reuse persistRefreshedMainAuthJson (:190-195) — it + spreads expected.tokens and never writes id_token, so the old identity + token would survive beside the new grant. The commit uses a SIBLING + persist that sets access_token/refresh_token/id_token/account_id + together and overwrites any prior id_token (adding the key is safe: + readMainAuthJsonCredential :122 tolerates it and + native-profile-store.ts:476-481 expects it). + +NEW `src/codex/main-device-reauth.ts` +- One process-owned active flow (opaque UUID, AbortController, bounded + terminal retention 5 min, grant deadline 15 min). Start/status/cancel + return only flowId, status, verificationUrl, deviceCode, and closed safe + failure codes per the 080 `MainDeviceReauthStatus` union. Injectable + login/commit dependencies for tests. Superseded/cancelled completions + never publish. No tokens/emails/raw account ids in DTO/log/error. +- Dedicated abort controller and direct `loginChatGPTNativeDevice` call: + MUST NOT use `startLoginFlow("chatgpt")` (would overwrite the chatgpt + scratch slot and 409 against pool logins, src/oauth/index.ts:1899-1973). + +NEW `src/codex/main-device-reauth-api.ts` +- `POST/GET/DELETE /api/codex-auth/main/reauth-device` with exact opaque + flow query, strict request keys, safe 400/404/409. Registered at the + management dispatch boundary (src/server/management-api.ts:385-407 + region); existing management auth/origin/session controls stay + authoritative. No CLI direct account-file write. + +MODIFY `src/cli/account-main.ts` +- `ocx account main reauth --device [--no-wait]`, + `reauth status --flow `, `reauth cancel --flow ` via the + management API; reject extra args before start. Register capability/help; + regenerate skill surface with `bun run skill:surface` if the capability + registry changes (tests/ci-workflows/skill-ocx.test.ts gates this). + Audit-folded: the native-main CLI branch point is account-main.ts (:181 + region, beside add/switch) with USAGE in src/cli/account.ts:64; the + management route-registry (src/server/management/route-registry.ts + MANAGEMENT_ROUTES) must gain the POST/GET/DELETE rows or + management-route-registry.test.ts and the capabilities ratchet go red — + do NOT grow UNDECLARED_ROUTES_2026_08_28. + +## Hub fence resolution (open decision 1, resolved here for audit) + +On a headless hub the native owner lifecycle is a no-op +(src/server/index.ts:1026-1046 binds the no-op when +`shouldSyncCodexOnStart` is false; the gate is composed at +src/codex/desired-state.ts:130 — :79-81 is `localClientSyncAllowed`). +The reauth commit therefore MUST NOT depend on owner activation and MUST +NOT widen `shouldSyncCodexOnStart` (that gate covers client-config sync, +not credential rewrite). + +Audit-folded correction to 080: 080's `assertNativeMainOwner` at +preparation/commit is RETRACTED for this layer. That assert throws without +a held owner entry (src/codex/native-main-owner.ts:302-314), which would +make hub reauth always fail. The exclusive claim is owner-independent +(src/codex/native-main-claim.ts:167, FS/SQLite lock only). The fence is +pinned to: `withNativeMainExclusiveClaim` + in-process admission fence + +path/hash/inode assertion + recovery/admission snapshot recheck, exactly +as on workstations. Only claim/admission failure maps to +`native_main_unavailable`; no write occurs without the full fence — an +unfenced write is a C4 violation, not a fallback. + +## Tests (red-first; domain tests/codex-integration, tests/oauth, tests/cli) + +NEW `tests/codex-integration/main-device-reauth.test.ts` — same-account +success without codex/keyring; wrong identity refused; missing token +fields; cancelled/superseded late result cannot publish; concurrent file +replace/refresh/profile switch; atomic write failure; claim unavailable → +native_main_unavailable with zero writes; no pool-row mutation; DTO/log +secret scan. +NEW `tests/codex-integration/main-device-reauth-api.test.ts` — route +contract: strict keys, 400/404/409 shapes, unauthorized rejected, +`__main__` still refused by `/api/codex-auth/login`. +MODIFY `tests/oauth/chatgpt-device-auth.test.ts` — native result retains +idToken in-process; per-fetch deadline fires on a hung stub fetch. +Audit-folded: native-main CLI tests land in +tests/cli/cli-native-profile.test.ts (native-main CLI); the pool +cli-account.test.ts keeps only the __main__ login rejection cases. +MODIFY `tests/cli/cli-native-profile.test.ts` — reauth --device surface, +status, cancel, arg rejection. +All NEW files: layout.json explicit + expected-fixture entries. + +## Docs / ownership + +structure/ ownership docs for src/codex, src/oauth, src/cli, src/server +synced in this PR (structure:check must stay green). Headless recovery +instructions updated (docs-site) in the same PR. Security draft stays in +scratch; only the implementation + regression diff is published. diff --git a/devlog/_plan/260912_unimplemented_trio_stack/030_l3_main_card_relogin_ui.md b/devlog/_plan/260912_unimplemented_trio_stack/030_l3_main_card_relogin_ui.md new file mode 100644 index 0000000000..41c1196409 --- /dev/null +++ b/devlog/_plan/260912_unimplemented_trio_stack/030_l3_main_card_relogin_ui.md @@ -0,0 +1,104 @@ +# L3: main-card Re-login with device code (#3898 GUI) + +Class C3 (auth-adjacent GUI). Stack layer 3, base the L2 branch. Branch +`codex/260912-native-main-reauth-ui`. Adopts +devlog/_plan/260912_accounts/090_reauth_ui.md, revalidated against current +`dev` by the GUI code map. Depends on L2's +`/api/codex-auth/main/reauth-device` contract. + +## Problem + +The main card is a locked App-login identity: expired state shows only +`codexAuth.mainTokenExpired` ("sign in again via Codex App login", +gui/src/components/codex-account-pool-main-card.tsx:183-185) and no +Re-login control (props at 21-56 have no `onReauth`). Pool rows have the +full device-code modal; the main card has nothing. + +## Constraints (090 + code map) + +- MUST NOT reuse `AddCodexAccountModal` / `openReauth("__main__")` / + `reauthAccountId=__main__`: the pool login route rejects `__main__` + (src/codex/account-id.ts:15-20; src/codex/auth-api.ts:221-224,2736-2748) + and a successful pool login writes `isMain: false` rows + (src/codex/auth-api.ts:2934-2939) — wrong credential store. +- DTO field chain: backend DTO → hook-validated state → main card only; + no device code in browser storage; verification URL accepted only from + the backend contract, never from arbitrary payloads. +- New copy lands in ALL locale files (en, de, fr, ja, ko, ru, tr, zh, + zh-TW) per gui/AGENTS.md "Text and i18n". +- `tests/gui/provider-workspace-auth.test.ts:248` currently requires + `codexAuth.mainTokenExpired` on the main card; updating that copy is + part of this layer. + +## Changes + +NEW `gui/src/components/use-main-device-reauth.ts` +- Dedicated hook mirroring the pool OAuth hook's start/poll/cancel shape + (gui/src/components/use-add-codex-account-oauth.ts:27) against + `/api/codex-auth/main/reauth-device`: `start()` POST, `poll(flowId)` + with visibility polling (2s tick, 10s per-tick timeout, stop on terminal + status), `cancel(flowId)` DELETE, unmount/abort cleanup. +- Normalizes closed status/error payloads; ignores late responses from a + replaced flow (flowId ownership); never accepts token/account-id fields; + renders only verificationUrl + deviceCode + status. + +MODIFY `gui/src/components/codex-account-pool-main-card.tsx` +- New optional `onReauthDevice` prop. When `showReauth` (83) is true, + render a "Re-login with device code" CTA beside the existing copy; after + start, show verification URL + human code + pending status + cancel; + success triggers the existing parent refresh. +- Layout stays consistent with the current card; pool Add/Re-login and the + native profile picker are untouched. + +MODIFY `gui/src/components/CodexAccountPool.tsx` +- Own main-reauth modal state separate from `showAdd`/`reauthId` + (75,94; openReauth at 189-192); wire `onReauthDevice` at the main-card render (515-533); + pause pool refresh while the main flow is active, same as the existing + modal pause (174-178). + +MODIFY `gui/src/i18n/{en,de,fr,ja,ko,ru,tr,zh,zh-TW}.ts` +- New `codexAuth.*` keys: CTA label, pending status, cancel, terminal + failure copy (actionable, safe; no auto-retry wording). Revise + `mainTokenExpired` so it no longer claims App login is the only path. + +## Audit folds (wp4 A) + +- Start POSTs an EMPTY body (the route rejects any body with 400); poll + immediately until verificationUrl/deviceCode arrive (they are empty in the + start response), and keep the last url/code through the committing state. +- Map the full MainDeviceReauthStatus union + HTTP error shapes: committing + (no url/code), failed.code (identity_mismatch, credential_changed, + native_main_unavailable, device_authorization_failed, + publication_failed, reconciliation_failed), 409 flow_in_progress, 503 + native_main_unavailable; when credentialUpdated is true the copy never + claims the file was unchanged; the verification URL is allowlisted to + https://auth.openai.com/codex/device. +- structure claim lands in structure/gui-and-management-api.md (the + Codex-accounts row :312), not overview.md. + +## Tests (red-first) + +NEW `gui/tests/main-device-reauth.test.tsx` — happy-dom mount per +gui/tests convention: CTA starts the dedicated route (never +`/api/codex-auth/login`), code/URL display, cancel ownership, stale-poll +ignore, success refresh, keyboard and error states. +MODIFY `tests/gui/provider-workspace-auth.test.ts` — main-card contract +updated for the new CTA + copy. +MODIFY `tests/gui/codex-auth-modal-status.test.ts` if locale-key +assertions enumerate codexAuth keys. +The happy-dom file lives under `gui/tests/`, outside the `tests/` layout +map — layout.json explicit + expected-fixture entries are needed only for +any NEW `tests/gui/*` source-contract file, not for `gui/tests/*`. + +## Docs / ownership + +L3 touches owned `gui/`: sync structure/overview.md and +structure/gui-and-management-api.md in this PR (structure:check must stay +green). + +## Verification + +`cd gui && bun test tests/main-device-reauth.test.tsx` plus the touched +suites; `bun run lint:i18n` for copy. Local GUI build NOT RUN; PR +screenshot evidence comes from hosted CI built artifacts, or an explicit +recorded exemption (repo gate: gui-mentioning PRs need a screenshot). diff --git a/devlog/_plan/260912_unimplemented_trio_stack/040_l4_native_paginated_writer.md b/devlog/_plan/260912_unimplemented_trio_stack/040_l4_native_paginated_writer.md new file mode 100644 index 0000000000..0b6e879460 --- /dev/null +++ b/devlog/_plan/260912_unimplemented_trio_stack/040_l4_native_paginated_writer.md @@ -0,0 +1,91 @@ +# L4: paginated history — offline ordinal recovery, live refusal preserved (#4311) + +Class C4 (user data). Stack top, base the L3 branch. Branch +`codex/260912-native-paginated-writer`. + +## Problem and scope decision (open decision 2, resolved here for audit) + +#4311's live defect (ordinal-0 `session_meta` clone) is already guarded: +`updateSessionMeta` throws for paginated records before writing +(throw at src/codex/history-provider.ts:1172), and preflight refuses +`history_paginated_requires_native_writer` +(structured field src/codex/inject.ts:899; preflight closure +src/codex/inject.ts:1182-1194). The residual acceptance is +(a) native paginated writer support and (b) corrupted-rollout recovery. + +(a) needs a Codex-owned writer API/IPC. None exists in this tree: Codex +owns ordinals and the live projection cursor +(structure/codex-home.md:232-234), `appendRolloutLine` deliberately does +not allocate ordinals (src/codex/history-provider.ts:77,248), and H +serializes only OpenCodex writes (src/codex/history-lock.ts; +src/codex/internal/history-writer.ts:86,107). Inventing N+1 is explicitly +forbidden by the issue (concurrent native writer / stale cursor). This +layer therefore ships (b) the offline recovery tool, keeps (a) refused +with the same structured reason, and says so in the PR. A follow-up +native-writer integration needs a Codex-side write API first — reported, +not faked. + +## Changes + +NEW `src/codex/history-ordinal-recovery.ts` +- Offline repairer for the #4311 corruption shape: an unprojected suffix + whose ordinals regress (projector error `expected N, got 0`). +- Preconditions, all enforced before any write: + - Codex fully closed (no running Codex process holds the home; detect + via the same process/home inspection the service uses, fail safe when + undecidable). + - Target resolution follows `resolveCodexStateDbPath` and + `threads.rollout_path` (src/codex/paths.ts:107-108; the column is + read through history-provider, not paths.ts) — never assume + `~/.codex/sessions`. + - Suffix shape verified: ordinals strictly increase before the boundary, + regress at the boundary, and the suffix parses cleanly. Anything else + refuses. + - Byte-identical backup written before mutation (manifest beside the + existing backup convention, src/codex/history-provider.ts:30). +- Rewrite: only ordinal digits in the unprojected suffix, renumbered to + continue the pre-boundary sequence; message text, ids, timestamps, and + all earlier bytes preserved. Exact readback verification before + reporting success. Dry-run (verify-only) is the default; `--write` + applies. + +MODIFY `src/cli/` (doctor/dispatch surface per existing conventions) +- `ocx doctor history repair-paginated-ordinals [--thread ]` + [--write]: runs the recovery, prints boundary, counts, backup path, and + readback result. Register capability/help; regenerate skill surface if + the registry changes. + +MODIFY `structure/codex-home.md` +- Record the recovery tool's ownership of offline ordinal repair and + restate that live paginated writes stay refused (structure:check gate). + +Explicitly unchanged (regression-tested, not edited): +`preflightCodexHistoryInjection` (history-provider.ts:307), +`appendRolloutLine` (77), `updateSessionMeta` paginated guard (1172), +inject pre/postflight (inject.ts:1182,1295,1332), catalog-only sync +(src/codex/sync.ts:216). + +## Tests (red-first; tests/codex-integration) + +NEW `tests/codex-integration/history-ordinal-recovery.test.ts` +- Synthetic fixture: session_meta ordinal 0 followed by event ordinal 1 + (the issue's minimal shape) behind a healthy increasing prefix. +- Dry-run reports and writes nothing (byte-identical file). +- Applied repair renumbers only the suffix; every non-ordinal byte + identical; readback passes; backup exists and matches the original. +- Refusals: Codex process detected / undecidable; suffix shape mismatch + (no regression, gap, unparsable line); missing backup space; absolute + rollout_path outside CODEX_HOME via sqlite_home. +- Preservation invariants red-first: run the preservation assertions + against the unimplemented command first (red), then implement (green). +MODIFY `tests/codex-integration/codex-history-provider.test.ts` +- Assert preflight refusal reason unchanged for paginated rows (the + recovery tool must not become a live writer). +NEW files: layout.json explicit + expected-fixture entries. + +## Out of scope + +Live paginated writes, ordinal allocation, native-writer IPC, any change +to the authless/compaction relabel fork (inject.ts:1098), provider-table +lifetime policy (separate #4311 sub-thread, tracked by containment unit), +in-app repair while Codex runs. diff --git a/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md b/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md index b28642e0ca..b531e1b307 100644 --- a/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md +++ b/docs-site/src/content/docs/fr/reference/cli/providers-accounts.md @@ -315,6 +315,9 @@ ocx account main doctor [--json] ocx account main list [--json] ocx account main register