Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
480a9fe
docs(devlog): 260806 WP13/WP14 resume plan — stale-checked roadmap, P…
lidge-jun Aug 6, 2026
d69f05f
docs(devlog): 260806 plan r2 — audit blockers folded in (reads non-mu…
lidge-jun Aug 6, 2026
0cfeda0
docs(devlog): 260806 plan r3 — the enable path is a syncModelsToCodex…
lidge-jun Aug 6, 2026
813375f
docs(devlog): 010 amendments — write-lock revalidation replaces the p…
lidge-jun Aug 6, 2026
9971d2b
docs(devlog): 010 r2 — per-artifact serializers own revalidation; cat…
lidge-jun Aug 6, 2026
1d02e2c
feat(codex): expose native restore artifact outcomes
lidge-jun Aug 6, 2026
96191c6
feat(codex): distinguish skipped sync outcomes
lidge-jun Aug 6, 2026
e483171
feat(codex): revalidate desired state under artifact locks
lidge-jun Aug 6, 2026
b45f05d
feat(cli): persist Codex restore intent before mutation
lidge-jun Aug 6, 2026
3906440
test(codex): cover desired-state toggle outcomes
lidge-jun Aug 6, 2026
5788def
fix(codex): keep external-provider restore free of history work
lidge-jun Aug 6, 2026
d213731
fix(codex): serialize restore config writes under the write lock
lidge-jun Aug 6, 2026
134956b
fix(codex): re-read desired state inside the catalog commit
lidge-jun Aug 6, 2026
f11bb6d
test(codex): behavioral coverage for lost-transition and restore gating
lidge-jun Aug 6, 2026
9c6fc9b
fix(codex): gate the cache reacquisition write on desired state
lidge-jun Aug 6, 2026
4167b86
docs(devlog): 020 amendments — Desktop toggle adapted to the real thr…
lidge-jun Aug 6, 2026
d66e764
docs(devlog): 020 r2 — desiredEnabled widening, full inspector state …
lidge-jun Aug 6, 2026
4ce7c8d
docs(devlog): 020 r3 — owned-but-drifted credentials are unsafe, not …
lidge-jun Aug 6, 2026
bab425e
feat(claude): add Desktop desired-state schema
lidge-jun Aug 6, 2026
f1803c9
feat(claude): inspect and safely remove Desktop gateway profiles
lidge-jun Aug 6, 2026
7f1220b
feat(claude): add Desktop native integration toggle
lidge-jun Aug 6, 2026
9e7e699
feat(claude): gate Desktop auto-apply on persisted intent
lidge-jun Aug 6, 2026
b6378e1
feat(gui): expose Claude Desktop desired toggle state
lidge-jun Aug 6, 2026
0b97b91
test(claude): cover Desktop standard-mode removal
lidge-jun Aug 6, 2026
f4607d5
test(claude): close Desktop toggle regression coverage
lidge-jun Aug 6, 2026
c002b5f
fix(claude): restore the tri-state activeProfile contract on Desktop …
lidge-jun Aug 6, 2026
67ebfb7
fix(claude): gate Desktop cleanup by selected ownership
lidge-jun Aug 6, 2026
1208f46
fix(management): fence Desktop toggle writers
lidge-jun Aug 6, 2026
e53752e
fix(cli): fence the no-daemon Desktop apply behind a post-await inten…
lidge-jun Aug 6, 2026
bac8eb9
fix(gui): make the Desktop toggle build under tsc, not just bun test
lidge-jun Aug 6, 2026
90dee1a
docs(devlog): 030 amendments — workstation-only composed acceptance, …
lidge-jun Aug 6, 2026
4b7a572
docs(devlog): 030 r2 — per-case broken-change proof, observable CLI c…
lidge-jun Aug 6, 2026
21491e6
docs(devlog): 030 r3 — port safety is config-seeded, never flag-passed
lidge-jun Aug 6, 2026
def3ac9
fix(codex): freshen sync admission at HTTP boundary
lidge-jun Aug 6, 2026
4948645
fix(codex): keep disabled paths artifact-free
lidge-jun Aug 6, 2026
025d90a
fix(codex): disclose contended lock identity
lidge-jun Aug 6, 2026
1acf7bc
feat(cli): add machine-readable restore result
lidge-jun Aug 6, 2026
ae961e9
fix(codex): refuse foreign-home restore before native locks
lidge-jun Aug 6, 2026
d5a3845
test(codex): compose native toggle entry-point acceptance
lidge-jun Aug 6, 2026
1700fee
fix(test): unstick the held-provider race and let the fixture own its…
lidge-jun Aug 6, 2026
792b238
test(codex): isolate toggle regression fixtures
lidge-jun Aug 6, 2026
9715fc7
test(codex): persist retained-sync race inputs
lidge-jun Aug 6, 2026
5c9c275
fix(cli): restore --json emits an envelope on every outcome path
lidge-jun Aug 6, 2026
65a55a7
fix(cli): restore --json early exits reuse the full envelope schema
lidge-jun Aug 6, 2026
e2d50e0
Merge remote-tracking branch 'origin/dev' into codex/260806-wp13-toggles
lidge-jun Aug 6, 2026
7a875c2
docs(devlog): PR screenshot — integrations overview with the Desktop …
lidge-jun Aug 6, 2026
4d22be4
test(fixtures): make Linux service ownership explicit
lidge-jun Aug 6, 2026
39f452a
test(ci): the Copilot migration swapped models:read for copilot-reque…
lidge-jun Aug 6, 2026
8c231ea
Merge remote-tracking branch 'origin/dev' into codex/260806-wp13-toggles
lidge-jun Aug 6, 2026
bdd12a9
fix(claude): stop POST /apply from cancelling its own enable
lidge-jun Aug 6, 2026
d9d5554
fix(claude): keep the server snapshot coherent after a Desktop enable
lidge-jun Aug 6, 2026
38d3834
fix(gui,cli): surface the partial-success apply instead of a clean su…
lidge-jun Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions devlog/_plan/260806_wp13_toggles_resume/000_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# 260806 — WP13/WP14 resume: Codex CLI toggle truth, Claude Desktop toggle, composed acceptance

PR-ONLY unit: the branch `codex/260806-wp13-toggles` (from `origin/dev` @ `b3a1d90a8`)
is pushed and opened as PR(s) against `dev`, but **never merged** in this session —
that boundary is a user instruction, not a preference.

This unit resumes the paused tail of two prior campaigns:

- `devlog/_fin/260804_codex_write_substrate/` — WP13 (composed acceptance, issue
[#1048](https://github.com/lidge-jun/opencodex/issues/1048)) was deferred; WP14's PR
deliverable landed as PR #998, so "WP14" here means the *new* toggle work opened as a PR.
- `devlog/_plan/260803_codex_desktop_toggle/` — 040 (Codex toggle CLI truth) and 050
(Claude Desktop toggle) were written but never implemented.

## Stale-check verdicts (explorer audit, 2026-08-06, tree @ b3a1d90a8)

All three pre-written docs are **NEEDS AMENDMENT**, none is ALREADY LANDED, none is
implementable as written. What changed under them:

### Landed since the docs were written

- Durable `clientIntegrations` desired state exists for `codex`/`grok` only
(`src/types.ts:551-556`, `src/config.ts:986-1012`), with owner
`setIntegrationEnabled` (`src/codex/desired-state.ts:90`) and field-scoped
`mutatePersistedConfig` (`src/config.ts:2197`).
- Dashboard `PUT /api/native-integrations/codex` persists intent before artifact work
(`src/server/management/native-integration-routes.ts:58-87,199-293`); startup honors
Codex OFF via `syncCodexOnStartIfEnabled` (`src/cli/index.ts:320`,
`src/codex/desired-state.ts:160-176`). Tests: `tests/native-codex-toggle.test.ts:106-156`,
`tests/codex-desired-state.test.ts:167-223`.
- Production injection runs under `withCodexWriteLock` (`src/codex/inject.ts:871-956`);
the typed lock model exists (`src/codex/codex-write-lock.ts:67-125`) with a real
two-process contention test (`tests/codex-inject-write-lock.test.ts:56-127`).
- c24 (Grok OFF survives restart) is **landed at unit level**: persist-before-strip
(`native-integration-routes.ts:342-374`), startup predicate `shouldSyncGrokOnStart`
(`src/cli/index.ts:350-354`, `desired-state.ts:58-76,195-197`), covered by
`tests/codex-desired-state.test.ts:233-243`. No full-process E2E; the composed
acceptance phase may add it, but c24 is not a standalone work-phase.

### Still missing (the actual work)

1. **CLI restore/eject do not persist desired state.** `ocx restore`/`eject`
(dispatch `src/cli/index.ts:774-819`) call `restoreNativeCodexAsync` without writing
`clientIntegrations.codex=false`; `restore back`/`eject back` do not persist ON.
Startup would resurrect routing the CLI just removed (040's core defect, alive).
2. **No artifact-level restore truth.** `restoreNativeCodexAsync`
(`src/codex/inject.ts:1193-1217`) reports `inline.success` even when the history
worker fails; no per-artifact result envelope exists.
3. **`syncModelsToCodex` and `ocx ensure` are ungated** (`src/codex/sync.ts:49-129`,
`src/cli/index.ts:379-424`): they bypass the desired-state gate.
4. **Claude Desktop has no toggle at all**: no `claude-desktop` key in
`clientIntegrations`, no native-toggle route (union is claude|grok|codex,
`native-integration-routes.ts:31`), auto-apply calls the writer directly ignoring
desired state (`agent-settings-routes.ts:131-150`), status does not classify
standard/gateway/foreign/not_installed (`agent-settings-routes.ts:767-815`), and no
`removeDesktop3pConfig`/read-only inspector exists. Audit correction: the current
status GET reads via `existsSync`/`readFileSync` and never calls the writer, so
reads are non-mutating **today**; the risk 050 guards against is a *future* OFF or
status path routing through `writeDesktop3pConfig`, whose eager `mkdirSync`
(`src/claude/desktop-3p.ts:331-345`) would manufacture a library. 020 adds the
dedicated read-only inspector rather than fixing an active violation.
5. **No composed acceptance suite.** WP13's P01-P36 doc cites pre-substrate line
numbers and pre-substrate RED claims (lock absence, no production caller) that are
no longer true. The surviving target: compose real entry points — CLI
restore/eject/ensure/sync, management toggle routes, startup gate — against a temp
home, including refusal, foreign-home, and race paths.

### External evidence (Luna swarm, 3 lanes, all sources opened)

- Anthropic's official configuration reference (claude.com/docs/third-party/
claude-desktop/configuration, accessed 2026-08-06) now documents the configLibrary
(`~/Library/Application Support/Claude-3p/configLibrary/`, `_meta.json` + `<id>.json`),
gateway fields `inferenceGatewayBaseUrl`/`ApiKey`/`AuthScheme` (bearer|x-api-key),
`inferenceModels` (string or object entries; first entry is default),
`modelDiscoveryEnabled`, and `supports1m`/`prefer1m`. The schema-drift risk recorded
in memory (private fields) is RESOLVED: the fields 050 relies on are documented.
- No official spec for behavior when the selected `<id>.json` is missing — community
evidence shows "configuration needs attention" symptoms only (UNVERIFIED). 050's rule
stands: never leave `appliedId` pointing at a missing file; select the standard `{}`
profile before removing ours.
- No native 1P-restore control is documented; community tools restore standard mode by
selecting an official/empty profile then removing the 3P one — matching 050's pivot.
- Codex CLI reads config.toml at session start (restart-scoped); `model_provider`
selects from `model_providers`; no official restore-after-proxy runbook exists, so
our restore semantics remain artifact-based, not documented-contract-based.

## Phase map (one decade doc per PABCD cycle)

- **010 (WP-B)** Codex toggle completion, consuming existing `clientIntegrations`:
CLI restore/eject persist OFF, restore back/eject back persist ON, artifact-level
restore result (history failure classified, never silent), and desired-state gating
for **every** direct `syncModelsToCodex` caller with 040's discriminated skip
semantics (`040_codex_toggle.md:222-235`) and fresh checks at irreversible
boundaries (`:600-618`): `ocx ensure` (`src/cli/index.ts:379-424`), `ocx sync`
(`:856-871`), restore/eject dispatch (`:774-819`), `src/cli/models.ts:102-107`,
`src/cli/provider.ts:232-237`,
`src/server/management/config-routes.ts:261-268`, and the toggle enable path
itself (`src/server/management/native-integration-routes.ts:262`), which today
interprets only `applied.ok` and needs the same discriminated-skip handling.
Source doc:
`260803_codex_desktop_toggle/040_codex_toggle.md` with the line-map above; drop its
four-client-coordinator premise — extend the landed two-key schema instead.
- **020 (WP-C)** Claude Desktop toggle per 050's amended contract: add
`claude-desktop` to `clientIntegrations` and the native route union; a dedicated
read-only inspector for status classification (absent library = `not_installed`;
reads never write); OFF = write+select `{}` standard profile, then remove the
opencodex profile and its credential-bearing backup; OFF with no owned state =
successful no-op; GUI switch. Also gate the Desktop auto-apply path with 050's
before/after-await desired-state guards (`050_desktop_toggle.md:747-783`,
`agent-settings-routes.ts:131-150`) so a concurrent OFF cannot lose to an in-flight
apply.
- **030 (WP-D)** Composed acceptance, **reduced workstation-only scope**: one suite
through real entry points against temp homes — CLI process invocations, management
routes, startup gate — covering refusal/foreign-home/race/restore truth, including
the missing Grok E2E (disable → fresh start path → fence stays absent). This is a
deliberate subset of issue #1048's 36-entry two-execution-class program: the
disposable-host service-lifecycle class (`050_composed_acceptance.md:24-35,55-99,
108-177,568-580`) needs `ocx service` on a throwaway host, which this session's
safety boundary forbids. **#1048 therefore stays OPEN** after 030; the PR references
it without a closing keyword and states which entries remain.
- **WP-E** Push branch, open template-complete PR(s) against dev referencing #1048,
PR CI green. **No merge, no promotion.** dev/preview/main tips proven unchanged.

Verification per phase: `bun run typecheck`, `bun run test`, `bun run lint:gui` (gui
touched phases), `bun run privacy:scan`, temp-home live proof (`mktemp -d`; never the
real `~/.codex`/`~/.opencodex`; never `ocx start/stop/service` — launchd owns the live
proxy on :10100). Every new mechanism gets a broken-change check.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# 010 — Codex toggle: design amendments over 040

`040_codex_toggle.md` stays the diff-level source for messages, exit codes, the
seven-caller skip table, and the `CodexHistoryFailureReason` discriminator
(current anchor `src/codex/history-provider.ts:167-175`). Two of its structural
premises are replaced here; where this document and 040 disagree, this document
wins.

## Amendment 1 — no four-client coordinator; the write lock is the serializer

040 imports `runClientIntegrationFlight`, `requirePersistedClientIntent`, and
`mutateClientIntegrationEnabled` from a WP3 shared contract that was never built
(`040_codex_toggle.md:33-38,431-454,578-618`), and `000_plan.md` r3 drops that
premise. What actually exists:

- `setIntegrationEnabled` persists intent and explicitly does not linearize
(`src/codex/desired-state.ts:14-19,90`).
- The management route has a route-local single flight
(`native-integration-routes.ts:199-224`) that serializes toggles *within* the
server process only.
- `withCodexWriteLock` serializes Codex artifact writes *across* processes
(`src/codex/codex-write-lock.ts:67-125`; production caller `inject.ts:871-956`).

Replacement design, Codex-only:

1. Intent writes stay `setIntegrationEnabled` — one owner, no new mutation API.
2. The race 040 worried about (CLI OFF vs route ON, two processes) is closed by
**revalidation under each artifact's own serialization boundary**, not by a
cross-surface flight and not by one global lock. The three artifact families
already have distinct serializers, and the Codex lock is *released* before
the history worker launches (`inject.ts:951-966`), so "inside
withCodexWriteLock" cannot cover history. Concretely:
- config/profile writes re-read persisted desired state inside their
`withCodexWriteLock` transaction (`codex-write-lock.ts:315-345` — the
callback is synchronous, and the desired-state read is a synchronous file
read, so it fits);
- the history worker re-reads desired state inside
`withHistoryWriteSerialization` (`history-worker.ts:119-131`), returning
the existing `blocked`-style envelope with a new reason
`"desired_disabled"`/`"desired_enabled"` instead of mutating;
- catalog restore re-reads inside `withCatalogWriteSerialization`
(`inject.ts:1241-1246`).
A lost race becomes the discriminated skip (`status:"skipped"`,
`skippedReason:"desired_disabled"` or `"desired_enabled"` for the restore
direction). Each lock provides mutual exclusion for its artifact; the
re-read inside it provides the freshness 040's
`requirePersistedClientIntent` wanted (`040:600-618`). The small window
where different artifacts observe different intent is acceptable: each
artifact converges to the latest persisted intent, and the startup gate
re-converges the remainder on the next start.
3. The route keeps its local flight for HTTP idempotency; the CLI needs no
flight because the lock + revalidation is the correctness boundary.

## Amendment 2 — the OFF path stays on the async worker boundary

040's CLI diff wraps synchronous `restoreNativeCodex()` with a `beforeWrite`
hook (`040:184-205`). The CLI has since moved to `restoreNativeCodexAsync()`
with history in a Worker (`inject.ts:1193-1218`); reverting to the inline path
would regress the event-loop isolation the substrate campaign built. Instead:

- `restoreNativeCodexAsync` gains the artifact-level result 040 demands
(`040:294-329`): a per-artifact envelope `{ config, catalog, history }` —
catalog is a first-class member because restore performs it independently
(`inject.ts:1241-1246`) and a `completed`-vs-not outcome exists today that
the summary silently flattens. Profile restoration is reported inside the
`config` member (it rides the same journal transaction). `history` carries
`CodexHistoryFailureReason` (`"busy" | "permission"`) instead of being folded
into `inline.success` (defect at `inject.ts:1193-1217`). Aggregate `success`
is false if ANY member failed.
- Persist-OFF ordering for `ocx restore`/`eject`: `setIntegrationEnabled(false)`
FIRST (so a crash mid-restore leaves intent durable and startup will not
resurrect routing), then the async restore; the history job revalidates
desired state under the lock per Amendment 1 before mutating. `restore back`/
`eject back` persist ON first, then sync — and a sync skip caused by a
concurrent OFF prints 040's competing-OFF error with exit 2.
- `success` for the command means: config (incl. profile) AND catalog restored
AND history either restored or classified (`busy` → retry advice, exit 1;
`permission` → ACL advice, exit 1). No path reports success with an
unclassified hole in any artifact.

## Test impact (from the audit, folded in)

- `tests/codex-sync-api.test.ts:73-81` — expects the new `status:"applied"`.
- `tests/cli-restore-back.test.ts:11-35` — drop source-string assertions,
assert behavior through a temp-home process run.
- `tests/native-codex-toggle.test.ts:106-156` — new envelopes and seams.
- `tests/codex-desired-state.test.ts`, `tests/codex-inject-write-lock.test.ts`
— extend for revalidation-under-lock; not intrinsically broken.

## Commit order (typecheck green at every commit)

1. `CodexHistoryFailureReason` + artifact envelope in history-provider/inject
(additive, no callers change behavior yet).
2. Discriminated `status` on `syncModelsToCodex` + all seven callers updated in
the same commit (exit-code contract lands here).
3. Revalidation-under-lock in inject/restore/history job.
4. CLI restore/eject persist intent + new messages; process-level tests.
5. Route/context wiring + GUI, if any surface text changes.
Loading
Loading