-
Notifications
You must be signed in to change notification settings - Fork 700
fix(server,config): extension CORS origin identity + symlink-safe atomic writes #886
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
39abf4c
a47a167
91b3921
921070b
3195c71
ef71180
c327fdb
470618d
14eae93
072b8df
60b417c
e0b15fe
478354e
24d0691
d8261a2
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,134 @@ | ||
| # 000 — Plan: client-integration toggle research (API-side on/off switch) | ||
|
|
||
| Research unit. Docs-only: this unit produces survey and design-option documents, | ||
| not production code. Implementation, if approved later, gets its own decade docs | ||
| in a follow-up cycle. | ||
|
|
||
| Parent unit: `devlog/_plan/260731_client_config_export/` (the read-only export | ||
| surface this unit extends). Sibling survey `001_client_config_survey.md` there | ||
| already covers Pi/OpenCode injection layers and the no-standard landscape; this | ||
| unit does not re-survey them. | ||
|
|
||
| ## Loop spec | ||
|
|
||
| - Loop archetype: spec-satisfaction research (verifier defines done). | ||
| - Trigger: user request — can client integrations be applied as an API-side | ||
| on/off switch (enable writes the provider block into the client config, | ||
| disable removes it), for Hermes Agent, OpenClaw, Kimi Code CLI, and | ||
| Gajae Code, with cc-switch as the reference implementation. | ||
| - Goal: durable devlog evidence answering (a) how cc-switch implements | ||
| apply/remove, (b) what each of the four clients requires for a safe external | ||
| toggle, (c) what shape an opencodex management-API toggle would take and its | ||
| risks. | ||
| - Non-goals: no `src/`, `gui/`, or test changes; no new clients serialized in | ||
| `config-export.ts`; no implementation plan at diff level (that is the next | ||
| cycle's decade docs, only if the design is approved). | ||
| - Verifier: every load-bearing claim carries a source URL or repo file path | ||
| opened this cycle (cxc-search Tier 2); unverifiable claims are marked | ||
| `candidate — unverified` and listed as open questions. `bun run | ||
| privacy:scan` stays green. | ||
| - Stop condition: all four clients + cc-switch lane questions answered or | ||
| explicitly marked unreachable. | ||
| - Memory artifact: this unit's 001–003 docs. | ||
| - Expected terminal outcomes: DONE (docs written, claims sourced) or BLOCKED | ||
| (sources unreachable, named). | ||
| - Escalation: a client whose config cannot be toggled safely from outside is a | ||
| finding, not a failure — record the blocker and mark that client UNSAFE for | ||
| the toggle design. | ||
|
|
||
| ## Research questions | ||
|
|
||
| ### Q1 — cc-switch reference mechanics (doc 001) | ||
|
|
||
| - How does cc-switch apply a provider into each app's config (writer modules, | ||
| atomic write, backup/restore), and how does it remove or restore one? | ||
| - Does it offer any local HTTP API / relay ("universal endpoint") as an | ||
| alternative to file writes? | ||
| - What state does it own (`~/.cc-switch/config.json`) versus mutate in the | ||
| client's files? | ||
|
|
||
| ### Q2 — Per-client toggle requirements (doc 002) | ||
|
|
||
| For each of Hermes Agent, OpenClaw, Kimi Code CLI, Gajae Code: | ||
|
|
||
| - Config file, format, and the minimal provider block that points at | ||
| `http://127.0.0.1:<port>/v1`. | ||
| - Hot-reload semantics: does the client re-read the file, or only at | ||
| session/process start? What does "applies on toggle" mean in practice? | ||
| - Non-interactive management surface, if any (CLI subcommands, local API), | ||
| which would beat raw file writes. | ||
| - Credential handling: env-reference support versus literal-only (decides | ||
| whether the no-secret-serialization invariant survives a write path). | ||
| - Removal semantics: what must be cleaned up beyond the provider block | ||
| (default model pointers, sessions, auth storage). | ||
|
|
||
| ### Q3 — opencodex API toggle design options (doc 003) | ||
|
|
||
| - Current surface: `GET /api/client-config` is read-only by design; mutating | ||
| precedents exist (`PUT /api/disabled-models`, `PUT /api/model-visibility`). | ||
| - Trust boundary (A-gate amendment, blocker 2): the toggle endpoint lives on | ||
| the **management plane** — every `/api/*` request passes | ||
| `requireManagementAuth`, loopback GUI sessions are origin-bound, and | ||
| mutations require CSRF (`src/server/management-auth.ts`, | ||
| `src/server/index.ts:448`). This is separate from **data-plane** admission | ||
| (`resolveApiAuth` in `src/server/auth-cors.ts`), whose loopback shortcut | ||
| only decides what a *client* must send to `/v1`. Doc 003 must not conflate | ||
| the two. | ||
| - Launcher precedence (A-gate amendment, blocker 3): `ocx opencode` injects | ||
| `provider.opencodex` through `OPENCODE_CONFIG_CONTENT`, which outranks the | ||
| disk config for that process (`src/cli/opencode.ts`). Read-back must | ||
| distinguish "disk state" from "runtime state when launched via `ocx`", and | ||
| the design must say whether the disk toggle even applies to OpenCode. | ||
| - Design space: `PUT /api/client-config/:client {enabled: bool}` or a | ||
| `/api/client-integrations` resource; read-back/health (is our block present | ||
| and current?) versus fire-and-forget writes. | ||
| - State model (A-gate amendment, blocker 4): no cc-switch-style DB exists, so | ||
| "is it on?" must be read back from the client file. A boolean `enabled` | ||
| hides drift; the design needs richer states — `absent`, `current`, `stale` | ||
| (our block but not what we would generate now), `conflict` (a block with our | ||
| provider id that we did not write), `unsafe` (unparseable file / damaged | ||
| ownership markers). | ||
| - Invariants to carry over: no secret serialized, additive merge only, | ||
| preserve unknown fields, atomic write + backup, never touch blocks we did | ||
| not write. | ||
|
|
||
| ### Q4 — Local writer/read-back precedents (doc 003, A-gate amendment) | ||
|
|
||
| - `src/grok/inject.ts` — the repo's one existing third-party config writer: | ||
| BEGIN/END managed fence in `~/.grok/config.toml`, orphan-marker refusal, | ||
| non-loopback refusal with credential-fallthrough reasoning, placeholder | ||
| `api_key = "opencodex-loopback"` (never a real secret), `stripGrokConfig` | ||
| removal path. | ||
| - `src/grok/status.ts` — the read-only status reader paired with the writer; | ||
| parses only our own fenced region. | ||
| - `src/config.ts` `atomicWriteFile`/`renameAtomicFile` — temp+rename, Windows | ||
| EBUSY/EPERM/EACCES retry with backoff, 0o600 mode, ACL hardening, residual | ||
| temp scrubbing. | ||
| - Claude Desktop writer/reader — second precedent to survey in 003. | ||
| - Risk register: format fidelity (YAML/JSON5/TOML round-trip), concurrent | ||
| writes while the client runs, clients that rewrite their own config | ||
| (Kimi Code), drift between proxy catalog and written model list. | ||
|
|
||
| ## Doc map (000-range, research only) | ||
|
|
||
| - `000_plan.md` — this document. | ||
| - `001_ccswitch_toggle_analysis.md` — Q1 findings. | ||
| - `002_client_toggle_matrix.md` — Q2 findings, one section per client. | ||
| - `003_api_design_options.md` — Q3 design space + risk register + open | ||
| questions. Options, not a commitment. | ||
| - `004_ux_design.md` — (cycle 2) GUI design for the unified integrations | ||
| surface: tab rename, hero with install detection + switches, per-client | ||
| sub-pages, and the rollback UX that makes the toggle trustworthy. Design | ||
| spec only; component diffs belong to a later implementation cycle. | ||
|
|
||
| ## Dispatch plan | ||
|
|
||
| - 5 research lanes (subagents, read-only): cc-switch, Hermes, OpenClaw, | ||
| Kimi Code, Gajae Code. Lane output is candidate evidence; load-bearing | ||
| claims are re-opened by the main agent before promotion (cxc-search proof | ||
| handoff). | ||
| - Local (main agent): current management-API surface, GUI consumer, loopback | ||
| admission semantics — already read this cycle: | ||
| `src/clients/config-export.ts`, `src/server/management/model-routes.ts`, | ||
| `src/server/auth-cors.ts` (`resolveApiAuth`, loopback admission), | ||
| `gui/src/components/apikeys-workspace/client-config-clients.ts`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # 001 — cc-switch toggle mechanics (reference implementation) | ||
|
|
||
| Research only. No diffs here. Sources were opened against upstream `main` on | ||
| 2026-08-02 via the GitHub API (research lane Zeno, main-agent spot-check of the | ||
| two load-bearing claims). | ||
|
|
||
| cc-switch v3.17.0 (Tauri 2, Rust + React) manages Claude Code, Claude Desktop, | ||
| Codex, Gemini CLI, Grok Build, OpenCode, OpenClaw, and Hermes Agent. It is the | ||
| closest shipping implementation of the "on/off switch per client app" this unit | ||
| evaluates. Its own provider store is `~/.cc-switch/cc-switch.db` (SQLite), with | ||
| `settings.json` for device preferences and `backups/` for database backups | ||
| ([README.md#L302-L305](https://github.com/farion1231/cc-switch/blob/main/README.md#L302-L305), | ||
| [database/mod.rs#L96-L109](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/database/mod.rs#L96-L109)). | ||
|
|
||
| ## 1. Two app classes: exclusive vs additive | ||
|
|
||
| The toggle semantics split on whether the client keeps one active provider or | ||
| many coexisting ones: | ||
|
|
||
| | Class | Clients | "Switch on" means | "Switch off" means | | ||
| |-------|---------|-------------------|--------------------| | ||
| | Exclusive | Claude Code, Codex, Gemini | backfill current live config into the stored provider record, then overwrite live config with the target provider | switching away backfills and overwrites with the next provider | | ||
| | Additive | OpenCode, OpenClaw, Hermes | insert the provider entry into the client's live config, alongside existing ones | `remove_provider_from_live_config` deletes only that live entry; the provider stays in cc-switch's DB, marked `live_config_managed=false` | | ||
|
|
||
| Source: [provider/mod.rs#L2893-L2951](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/provider/mod.rs#L2893-L2951), | ||
| [provider/mod.rs#L2954-L2966](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/provider/mod.rs#L2954-L2966), | ||
| [provider/mod.rs#L3087-L3146](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/provider/mod.rs#L3087-L3146). | ||
| Main-agent spot-check: `live_config_managed` markers and | ||
| `provider_live_config_managed` confirmed in `provider/mod.rs` (lines 1870, | ||
| 2437-2450). | ||
|
|
||
| All four of this unit's target clients (Hermes, OpenClaw, and by their config | ||
| shapes Kimi Code and Gajae Code) are **additive-class**: a toggle writes and | ||
| removes one provider entry, never a whole-file takeover. | ||
|
Comment on lines
+32
to
+34
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. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files 'devlog/_plan/260802_client_toggle_api/*'
printf '%s\n' '--- source map ---'
ast-grep outline devlog/_plan/260802_client_toggle_api/001_ccswitch_toggle_analysis.md
ast-grep outline devlog/_plan/260802_client_toggle_api/002_client_toggle_matrix.md
printf '%s\n' '--- analysis excerpt ---'
sed -n '1,95p' devlog/_plan/260802_client_toggle_api/001_ccswitch_toggle_analysis.md
printf '%s\n' '--- client matrix excerpt ---'
sed -n '1,130p' devlog/_plan/260802_client_toggle_api/002_client_toggle_matrix.mdRepository: lidge-jun/opencodex Length of output: 15357 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- exact disputed lines ---'
nl -ba devlog/_plan/260802_client_toggle_api/001_ccswitch_toggle_analysis.md | sed -n '26,78p'
printf '%s\n' '--- remaining client matrix ---'
nl -ba devlog/_plan/260802_client_toggle_api/002_client_toggle_matrix.md | sed -n '90,230p'
printf '%s\n' '--- cross-document references ---'
rg -n -C 3 'Kimi|Gajae|additive|model alias|default_model|default_provider|strict|schema|cleanup|provider entry' devlog/_plan/260802_client_toggle_apiRepository: lidge-jun/opencodex Length of output: 41762 Separate Kimi Code and Gajae Code from the additive-provider pattern. The cc-switch evidence covers OpenClaw and Hermes Agent, not Kimi Code or Gajae Code. Kimi provider removal also removes referencing model aliases, 🤖 Prompt for AI Agents |
||
|
|
||
| ## 2. Write discipline | ||
|
|
||
| - Provider-specific writers project into native files: Claude -> | ||
| `~/.claude/settings.json`, Codex -> `~/.codex/auth.json` + `config.toml`, | ||
| OpenClaw -> `~/.openclaw/openclaw.json` (JSON5), Hermes -> | ||
| `~/.hermes/config.yaml` (YAML). | ||
| - JSON/TOML/text writes go through a temporary sibling file + rename (atomic on | ||
| Unix; Windows removes the destination first). | ||
| [provider/live.rs#L1015-L1060](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/provider/live.rs#L1015-L1060), | ||
| [config.rs#L273-L351](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/config.rs#L273-L351) | ||
| - Writer modules preserve unknown fields (`serde(flatten)` extra maps in | ||
| `hermes_config.rs` / `openclaw_config.rs`) and honor the client's own | ||
| config-dir overrides (`HERMES_HOME`, Windows `%LOCALAPPDATA%\hermes`). | ||
| - Health warnings are returned on write (`OpenClawWriteOutcome.warnings`). | ||
|
|
||
| ## 3. Proxy takeover: the *other* kind of toggle | ||
|
|
||
| cc-switch has a second, coarser switch — "proxy takeover" — which is | ||
| backup-and-restore, not provider-entry deletion: | ||
|
|
||
| 1. Enable: start the local proxy if needed, snapshot the app's current live | ||
| config into the DB's live-backup storage, then write proxy | ||
| endpoint/placeholder fields into the client config. | ||
| 2. Disable/stop-with-restore: write the saved snapshot back and delete the | ||
| backup rows. Missing backup -> attempt SSOT/provider reconstruction and | ||
| placeholder cleanup. | ||
| 3. Switching providers *while* takeover is active is a hot switch: the client | ||
| keeps pointing at the local proxy and only the proxy's upstream target | ||
| changes. | ||
|
|
||
| [services/proxy.rs#L730-L820](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/proxy.rs#L730-L820), | ||
| [services/proxy.rs#L1292-L1323](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/proxy.rs#L1292-L1323), | ||
| [services/proxy.rs#L1822-L1865](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/proxy.rs#L1822-L1865), | ||
| [provider/mod.rs#L3004-L3054](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/services/provider/mod.rs#L3004-L3054) | ||
|
|
||
| For opencodex this pattern is mostly redundant — opencodex *is* the proxy, so | ||
| the client config only ever needs the additive provider entry; there is no | ||
| upstream target to hot-switch inside the client. | ||
|
|
||
| ## 4. Universal endpoint (relay), not a management API | ||
|
|
||
| cc-switch embeds an Axum server (default `http://127.0.0.1:15721`) exposing | ||
| Claude / OpenAI Responses / Gemini routes plus health and status; tools point | ||
| their base URL at it. It is a data-plane relay, **not** a control API — the | ||
| toggle operations live in Tauri commands, and the only URL scheme is | ||
| `ccswitch://v1/import?...` for config import. | ||
| [proxy/server.rs#L100-L145](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/proxy/server.rs#L100-L145), | ||
| [proxy/server.rs#L291-L366](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/proxy/server.rs#L291-L366), | ||
| [deeplink/parser.rs#L11-L67](https://github.com/farion1231/cc-switch/blob/main/src-tauri/src/deeplink/parser.rs#L11-L67) | ||
|
|
||
| Main-agent spot-check: `proxy/server.rs` binds | ||
| `config.listen_address:listen_port` via tokio TcpListener (lines 101-145). | ||
|
|
||
| ## 5. What this means for an opencodex toggle | ||
|
|
||
| - The additive per-client writer is the pattern to copy: insert/remove exactly | ||
| one provider entry, preserve everything else, atomic write, health warnings | ||
| on read-back. | ||
| - cc-switch's "is it on?" state is *its own DB record* plus a | ||
| `live_config_managed` marker — it does not re-derive state from the client | ||
| file. An opencodex toggle has no such DB; state must be read back from the | ||
| client config itself (does our entry exist, and does it match what we would | ||
| generate now?). This is a real design divergence, explored in 003. | ||
| - Its exclusive-app machinery (backfill/overwrite) and proxy-takeover | ||
| snapshot/restore solve problems opencodex does not have; neither needs | ||
| porting. | ||
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 | 🟠 Major | 🏗️ Heavy lift
Scope the write invariants per client format.
The unconditional
preserve unknown fieldsrule conflicts withdevlog/_plan/260802_client_toggle_api/002_client_toggle_matrix.mdLines 120-124 and 138-144. Kimi loses comments and formatting during serialization, while Gajae requires schema-known fields and rejects unknown fields.Split these into global invariants and format-specific rules. Otherwise the implementation cycle may generate invalid Gajae blocks or promise preservation that the serializer cannot provide.
🤖 Prompt for AI Agents