-
Notifications
You must be signed in to change notification settings - Fork 1k
docs(devlog): plan cross-platform parity and the Windows identity decode fix #3436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # 000 - Problem model: what "macOS-only" actually means here | ||
|
|
||
| Unit: cross-platform parity + Windows compatibility fixes. | ||
| Branch base: `dev` at `072df52eb`. Date: 2026-09-04. | ||
|
|
||
| ## The request | ||
|
|
||
| Make the capabilities that only work on macOS work on Windows and Linux too, and | ||
| land the Windows-compatibility bug fixes the backlog already documents. Delivered | ||
| as a stacked pull-request chain against `dev`. | ||
|
|
||
| ## The claim that had to be tested first | ||
|
|
||
| "opencodex is macOS-only in places" is the starting hypothesis, not a finding. A | ||
| read-only inventory of every `darwin` gate in `src/` (recorded in `001`) shows the | ||
| claim is mostly FALSE and the exceptions are concentrated: | ||
|
|
||
| - 21 darwin-referencing sites were classified. | ||
| - 13 are ALREADY-HANDLED: they carry real win32 and linux branches today | ||
| (`open-url.ts`, `cursor-detect.ts`, `desktop-3p-paths.ts`, `kiro-credentials.ts`, | ||
| `app-server-processes.ts`, `service.ts` backend dispatch, `key-store.ts`, and the | ||
| Claude credential file fallback in `local-token-detect.ts`). | ||
| - 6 are a single subsystem: `src/server/system-env.ts`, which refuses with | ||
| `reason: "not macOS"` at five entry points and holds the launchctl calls behind them. | ||
| - 1 is a hard throw: `src/oauth/meta-muse.ts` refuses every non-darwin host. | ||
| - 1 is a missing developer script: `scripts/ocx-restart.sh` has no Windows counterpart. | ||
|
|
||
| So this unit is not a porting sweep. After four audit rounds it is three phases, | ||
| each its own PR in a stacked chain: | ||
|
|
||
| - **wp1** - `meta-muse` refuses on Windows and Linux with accurate reasons | ||
| instead of a false macOS-Keychain one. | ||
| - **wp2** - a platform-support reference page, so the capabilities that stay | ||
| macOS-only have a written answer rather than a silent dead end. | ||
| - **wp3** - the Windows identity decode fix, the one defect proven to exist in | ||
| the tree. | ||
|
|
||
| Everything else the audits removed is in `050` with its blocking reason. | ||
|
|
||
| ## The second problem, found while looking | ||
|
|
||
| While inventorying the Windows paths, a real defect surfaced in the tree: the | ||
| identity ACQUISITION path decodes PowerShell stdout as UTF-8 when Windows | ||
| PowerShell 5.1 emits the console code page, so a non-ASCII account name is | ||
| mojibaked before any comparison happens. The `<UserId>` comparison itself is | ||
| correct. Details and evidence: `003`. | ||
|
|
||
| Its relationship to issue #3320 is CANDIDATE, not proven. The reporter's evidence | ||
| was collected after a local patch and repair, so the original registration shape | ||
| is unknown and nothing here establishes that this defect produced that user's | ||
| failure. What can be said conditionally: #3134 moved new registrations to SID | ||
| form, and a SID is ASCII, so freshly registered tasks stay healthy; a task | ||
| registered by v2.39.0 or earlier carries a name-form `<UserId>`, and for a | ||
| non-ASCII account both sides of that comparison are separately corrupted, which | ||
| would make `ocx service repair` refuse it permanently. That is a plausible route | ||
| to the reported symptom, not a demonstrated one. | ||
|
|
||
| ## What "done" means for this unit | ||
|
|
||
| Every phase below ships as its own reviewable PR against `dev`, stacked so each | ||
| child bases on its parent's head branch (DEV-STACK-01), with CI as the verification | ||
| authority. The user has forbidden running the full local suite, so no phase may | ||
| claim a green suite as evidence; each phase names the focused reasoning or the CI | ||
| run that backs it. | ||
|
|
||
| ## Non-goals | ||
|
|
||
| - No provider catalog or model metadata churn. | ||
| - No GUI redesign. | ||
| - No release promotion to `preview` or `main`. | ||
| - No security-triage writeup in this directory (AGENTS.md: scratch only). | ||
| - The `go/` directory is untouched. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # 001 - Darwin surface inventory | ||
|
|
||
| Read-only sweep of every `darwin`, `macOS`, `Keychain`, `launchctl`, `osascript`, | ||
| `/Applications` and `plist` reference under `src/` and `scripts/`, classified for | ||
| portability. Verdicts: PORTABLE, DOCUMENT-ONLY, ALREADY-HANDLED. | ||
|
|
||
| ## Already handled - no work needed | ||
|
|
||
| These carry real win32 and linux branches today. Listed so a future reader does | ||
| not re-open them. | ||
|
|
||
| | Site | Why it is fine | | ||
| |---|---| | ||
| | `src/lib/open-url.ts:14` | Three-way branch; `rundll32 url.dll,FileProtocolHandler` on win32, `xdg-open` on linux, with an ENOENT listener so a headless host cannot kill the proxy | | ||
| | `src/integrations/cursor-detect.ts:69` | `/Applications` is one of three branches; win32 scans `LOCALAPPDATA\\Programs` and `ProgramFiles`, linux scans `/opt` and `~/.local/share` | | ||
| | `src/integrations/cursor-effort-table.ts:45` | `Contents/Resources/app` vs `resources/app`, the correct Electron layout for each | | ||
| | `src/claude/desktop-3p-paths.ts:47` | Pure resolver with `APPDATA`/`LOCALAPPDATA` and `XDG_CONFIG_HOME` branches | | ||
| | `src/oauth/kiro-credentials.ts:166,224` | Full win32 and linux branches for the session DB and executable | | ||
| | `src/oauth/local-token-detect.ts:78` | Keychain returns null off darwin and falls through to `.credentials.json`, which is what Claude Code writes on Windows and Linux | | ||
| | `src/claude/auth-detect.ts:207` | Metadata-only presence probe; "absent" off darwin is correct because the file source covers those platforms | | ||
| | `src/oauth/anthropic.ts:172` | Error text only; prints the file-only variant off darwin | | ||
| | `src/service.ts:3454` and around | Three-backend dispatch: launchd, Task Scheduler/WinSW, systemd user unit | | ||
| | `src/codex/app-server-processes.ts:523,626,676` | Named win32 and linux branches with their own timeout bounds | | ||
| | `src/codex/log-guard/path-safety.ts:13` | `/var` to `/private/var` alias normalization is genuinely macOS-shaped; a Windows canonical comparator sits alongside it | | ||
| | `src/providers/key-store.ts:33` | Not darwin-gated at all; `@napi-rs/keyring` maps to Credential Manager and libsecret | | ||
|
|
||
| ## The real gaps | ||
|
|
||
| ### 1. `src/server/system-env.ts` - five refusals, one subsystem | ||
|
|
||
| | Line | Function | Behavior off darwin | | ||
| |---|---|---| | ||
| | 142 | `installShellHook` | `{ installed: false, reason: "not macOS" }` | | ||
| | 159 | `uninstallShellHook` | `{ removed: false, reason: "not macOS" }` | | ||
| | 223 | `reconcileShellHook` | `{ changed: false, state: "absent", reason: "not macOS" }` | | ||
| | 372 | `injectSystemEnv` | `{ injected: false, reason: "not macOS" }` | | ||
| | 489 | `revertSystemEnv` | `{ reverted: false, reason: "not macOS" }` | | ||
|
|
||
| What it does on macOS: writes `~/.opencodex/claude-env.sh` (platform-neutral), | ||
| appends a marked hook line to `~/.zshrc`, and injects `ANTHROPIC_BASE_URL`, | ||
| `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY`, conditionally `ANTHROPIC_AUTH_TOKEN`, | ||
| plus seven lever keys into the launchd user domain via `launchctl setenv`. | ||
|
|
||
| Ownership and rollback are stronger than the surface suggests, and any port must | ||
| preserve them: a tracking file `~/.opencodex/system-env-port` (0600) holds pid, | ||
| port and `injectedKeys`; revert unsets ONLY tracked keys so a pre-existing user | ||
| value survives; `injectedKeys` is re-persisted after every single `setenv` so a | ||
| crash mid-injection still leaves a complete undo list; lever keys are user-wins | ||
| (`injectLever` skips a key already present); revert refuses on ownership mismatch; | ||
| and `rollbackInjectedKeys` rewrites tracking with only the keys whose unset failed | ||
| so a partial rollback stays resumable. | ||
|
|
||
| Callers: `ocx start` (`src/cli/index.ts:443` and the already-running path at 537), | ||
| `syncCleanup` at 378, `ocx stop` at 957, `ocx uninstall` at 1239, and | ||
| `applySystemEnvToggle` from `agent-settings-routes.ts:1384`. | ||
|
|
||
| Verdicts: the SHELL HOOK half (142/159/223) is PORTABLE and cheap - the writer is | ||
| already platform-neutral and the marker install/remove/verify logic is already | ||
| written; the only blocker is the `!== "darwin"` guard plus a hardcoded `~/.zshrc`. | ||
| The ENV INJECTION half (372/489) is PORTABLE but expensive and carries a real | ||
| security question: moving `ANTHROPIC_AUTH_TOKEN` from a per-boot launchd domain | ||
| into a persistent `HKCU\\Environment` hive changes secret exposure, which AGENTS.md | ||
| routes to explicit security review. It is deliberately NOT in this unit. | ||
|
|
||
| ### 2. `src/oauth/meta-muse.ts:136` - the only hard throw | ||
|
|
||
| `loginMetaMuse` throws on every non-darwin platform with a message blaming the | ||
| macOS Keychain. Measurement in `002` shows the message states the wrong reason. | ||
| Verdict: DOCUMENT-ONLY for Windows, PORTABLE for Linux pending a measured pointer. | ||
|
|
||
| ### 3. `scripts/ocx-restart.sh` - no Windows counterpart | ||
|
|
||
| Bash-only detached restart helper for agent sessions. `restart-codex-desktop-app.ps1` | ||
| is a different tool. The `darwin` mention inside it is only a `setsid` fallback. | ||
| Verdict: PORTABLE, small. | ||
|
|
||
| ### 4. `src/server/management/agent-settings-routes.ts:1091` | ||
|
|
||
| `autoConnectSupported` hardcodes `platform === "darwin"`. Honest today, since the | ||
| capability really is macOS-only, and the GUI fails closed on it. Three distinct | ||
| things must not be conflated when any of this moves: launchctl ENV INJECTION | ||
| (macOS only), writing the `claude-env.sh` SHELL FILE (deferred to its own unit, | ||
| `050`), and the SHELL HOOK that sources it. `autoConnectSupported` names the | ||
| first. A port of the second or third needs its own field rather than overloading | ||
| this one, because `tests/claude-management-api.test.ts:653-664` correctly pins | ||
| this flag false off darwin. | ||
|
|
||
| ## One latent hazard worth recording | ||
|
|
||
| `src/cli/index.ts:1240` and `:1245` allowlist the literal reason strings | ||
| `"not macOS"` and `"not installed"` so `ocx uninstall` treats them as benign. A | ||
| future backend returning a different reason string turns a benign no-op into a | ||
| failed uninstall step. | ||
|
|
||
| Audit round 3 showed this is not a free refactor: four exact `toEqual` | ||
| assertions pin the current return objects | ||
| (`tests/claude-shell-hook.test.ts:63,79,107,173`), and a discriminant applied to | ||
| "every refusal path" would classify genuine failures (`no HOME`, | ||
| `read/write failed`) as benign skips. It belongs WITH the port that needs it, as | ||
| a discriminated union designed against those call sites - deferred to `050`, | ||
| not part of any phase in this unit. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| # 002 - Measured: what the Muse Code CLI stores off macOS | ||
|
|
||
| The `meta-muse` provider is the only hard platform throw in the runtime | ||
| (`src/oauth/meta-muse.ts:136`). This document records what is known, what is | ||
| sourced, and what is still unmeasured, because the module's own contract is to | ||
| refuse any storage backend it has not verified (`meta-muse.ts:160`). | ||
|
|
||
| ## What was measured on macOS (prior unit, not re-derived here) | ||
|
|
||
| `devlog/_plan/260903_muse_spark_plan_oauth/003` records the shipped shape: | ||
|
|
||
| - `~/.config/muse/auth.json` (0600) is a POINTER carrying no secret. Its | ||
| `providers.meta` object declares `mechanism: "oauth"` and `storage: "keychain"`. | ||
| - The secret is a macOS Keychain generic-password item, service | ||
| `ai.meta.dev.credentials`, account `meta`. | ||
| - Only `api_key` authenticates the Model API; `access_token` returns 401. The key | ||
| matches `/LLM\|\d+\|[A-Za-z0-9_-]{10,}/`. | ||
|
|
||
| The important detail for this unit is that `storage` is a DECLARED field in the | ||
| pointer. The CLI tells us where it put the secret. That is the extension point: | ||
| a non-keychain host will declare a different value, and the module already | ||
| refuses unknown values rather than guessing. | ||
|
|
||
| ## What the vendor documents for other platforms | ||
|
|
||
| Meta's quickstart documents installation for macOS and Linux only, through | ||
| `curl -fsSL https://dev.meta.ai/install.sh | bash`. There is no native Windows | ||
| installer; the documented Windows route is WSL2. | ||
|
|
||
| Consequence, and it reframes the whole task: **there is no native Windows Muse | ||
| CLI to import a credential from.** The current error message is wrong about the | ||
| reason. It blames the macOS Keychain when the real reason on Windows is that the | ||
| vendor ships no Windows CLI at all. | ||
|
|
||
| ## Linux: sourced, NOT yet measured | ||
|
|
||
| Third-party setup writeups describe the Linux credential living in | ||
| `~/.config/muse/auth.json` and honoring `$XDG_CONFIG_HOME`, with the secret in | ||
| that JSON file rather than an OS keyring. That is plausible - it matches how the | ||
| pointer already declares its own backend - but it is **unverified**. No Muse CLI | ||
| install exists on this host (`~/.config/muse/auth.json` is absent, checked | ||
| 2026-09-04), so the exact `storage` value a Linux install writes has not been | ||
| observed. | ||
|
|
||
| This is the single fact that gates wp1's Linux half. The module must not invent a | ||
| `storage` value. Two honest routes: | ||
|
|
||
| 1. Implement the Linux branch keyed on the DECLARED `storage` value, accepting a | ||
| file-backed secret only when the pointer says so, and keep refusing unknown | ||
| values. If a Linux install declares `storage: "keychain"`, the refusal still | ||
| fires and nothing is silently wrong. | ||
| 2. Do not guess a specific value: accept the shapes we can validate structurally | ||
| (a secret embedded in the pointer, or a sibling file the pointer names) and | ||
| refuse everything else with a message that says what was found. | ||
|
|
||
| **Audit round 1 rejected both routes as premature (blocker 6).** The pointer | ||
| interface (`src/oauth/meta-muse.ts:58-60`) declares only `mechanism`, `storage` | ||
| and `user_email`. There is no path field and no inline-key field. Writing a reader | ||
| against fields nobody has observed is the unverified-credential path this module | ||
| refuses everywhere else, and no amount of structural validation makes an invented | ||
| schema measured. | ||
|
|
||
| So this unit ships neither route. Linux keeps refusing, with a message that states | ||
| the real reason instead of blaming the macOS Keychain. `010` implements that, and | ||
| `050` records the exact measurement that would unblock a Linux reader: a real | ||
| `~/.config/muse/auth.json` from a Linux install, with its `storage` value and, if | ||
| the secret is file-backed, the field naming the file. | ||
|
|
||
| ## What wp1 must therefore deliver | ||
|
|
||
| - Windows: replace the misleading macOS-Keychain refusal with an accurate one | ||
| that names WSL2 and the supported `META_MODEL_API_KEY` alternative. No WSL2 | ||
| pointer read: reachability was never measured, and a refusal that tells the | ||
| truth is a fix while a guess is not. | ||
|
Comment on lines
+71
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 5 'META_MODEL_API_KEY|meta-model' src testsRepository: lidge-jun/opencodex Length of output: 24725 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- provider-id environment lookup ---'
rg -n -C 8 'process\.env|apiKey|authMode|META_[A-Z0-9_]+|provider.*id|id.*provider' src/providers src/auth src 2>/dev/null | head -n 260
printf '%s\n' '--- direct declarations and callers ---'
rg -n -C 10 'get.*Env|env.*Key|API_KEY|auth.*env|credential.*env|provider.*api' src testsRepository: lidge-jun/opencodex Length of output: 19522 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- environment reads ---'
rg -n -C 6 'process\.env' src --glob '*.ts' || true
printf '%s\n' '--- provider configuration construction ---'
rg -n -C 10 'apiKey.*=|apiKey:|authMode|env.*key|key.*env|provider.*apiKey|config.*apiKey' src --glob '*.ts' | head -n 320 || trueRepository: lidge-jun/opencodex Length of output: 50376 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- exact key-variable logic ---'
rg -n -C 12 'API_KEY|api[_-]?key|toUpperCase\(\)|replaceAll|replace\(.*[-_]' src --glob '*.ts' \
| rg '(^src/|API_KEY|apiKey|api[_-]?key|toUpperCase|replace)' \
| head -n 260 || true
printf '%s\n' '--- relevant tests and configuration references ---'
rg -n -C 8 'META_MODEL_API_KEY|derived.*provider|provider.*derived|environment variable|environment-variable|env var' src tests --glob '*.ts' \
| head -n 220 || trueRepository: lidge-jun/opencodex Length of output: 49721 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- computed environment-variable access ---'
rg -n 'process\.env\[|env\[[^]]+\]|envVar|envName|environment.*variable|variable.*provider|provider.*variable|toUpperCase' src --glob '*.ts' \
| rg -v 'node_modules|generated|OPENCODEX_DEBUG|HOME|PATH|SystemRoot|CLAUDE|CODEX|OPENCLAW|PI_|KIMI|HERMES|DSH_|PROXY|TOKEN' \
| head -n 220 || true
printf '%s\n' '--- provider key resolution symbols ---'
rg -n 'function .*Key|function .*Auth|resolve.*Key|resolve.*Auth|apiKey' src/providers src/config.ts src/server --glob '*.ts' \
| head -n 260 || trueRepository: lidge-jun/opencodex Length of output: 34897 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- all provider environment-key references ---'
rg -n -C 12 'env_key|envKey|env-key|META_MODEL_API_KEY|MODEL_API_KEY|resolveEnvValue|environment variable' . \
--glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' --glob '!*.lock' \
| head -n 320 || true
printf '%s\n' '--- key-store environment resolution ---'
sed -n '1,115p' src/providers/key-store.ts
printf '%s\n' '--- provider config creation around key providers ---'
rg -n -C 18 'deriveKeyLoginMap|entryToPreset|apiKey.*provider|providers\[.*apiKey|apiKey.*providers' src/cli src/providers src/config.ts \
| head -n 320 || trueRepository: lidge-jun/opencodex Length of output: 50377 🏁 Script executed: #!/bin/bash
set -u
printf '%s\n' '--- key-login map consumers ---'
rg -n -C 14 'deriveKeyLoginMap|deriveProviderPresets|providerConfigSeed|apiKey\s*:' src tests --glob '*.ts' \
| head -n 360 || true
printf '%s\n' '--- environment substitution contract ---'
rg -n -C 18 'function resolveEnvValue|export function resolveEnvValue|resolveEnvValue\(' src/config.ts src/providers src/server --glob '*.ts' \
| head -n 220 || trueRepository: lidge-jun/opencodex Length of output: 43399 Do not describe OpenCodex resolves environment variables only when 🤖 Prompt for AI Agents |
||
| - Linux: a refusal naming the unmeasured storage rather than the Keychain. No | ||
| reader until a real pointer is measured. | ||
| - Neither platform may weaken the ToS consent warning, which is the CLI's only | ||
| warning surface. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 10052
🤖 get_repo_knowledge executed:
get_repo_knowledge lidge-jun/opencodex /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/conventions /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/learningsLength of output: 11867
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 45433
Define the counting unit for the Darwin inventory
000_problem_model.md:18-22claims 21 sites and 13 already-handled sites, but001_darwin_surface_inventory.md:14-24contains 12 already-handled rows. Several rows group multiple references, while thesystem-env.tsgap groups five refusals. The documents do not define the counting unit. Define whether a site is a row, file, or reference, then add a numbered mapping for all 21 sites before using13/21as the scope baseline.🤖 Prompt for AI Agents