Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
134 changes: 134 additions & 0 deletions devlog/_plan/260802_client_toggle_api/000_plan.md
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.
Comment on lines +91 to +93

Copy link
Copy Markdown
Contributor

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 fields rule conflicts with devlog/_plan/260802_client_toggle_api/002_client_toggle_matrix.md Lines 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devlog/_plan/260802_client_toggle_api/000_plan.md` around lines 91 - 93,
Revise the invariant list in the plan to separate global guarantees from
client-format-specific behavior: retain secret exclusion, additive merging,
atomic write with backup, and avoiding untouched blocks globally; move
unknown-field preservation into the applicable format rules, documenting Kimi’s
loss of comments/formatting and Gajae’s schema-only, unknown-field-rejecting
serialization in alignment with the client toggle matrix.


### 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

Copy link
Copy Markdown
Contributor

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

🧩 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.md

Repository: 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_api

Repository: 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, default_model, and default_provider. Gajae Code requires schema-known fields and cleanup of related models.yml references. Mark the classification at lines 32–34 as a hypothesis, and revise lines 71–73 to require each client’s complete add and remove contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devlog/_plan/260802_client_toggle_api/001_ccswitch_toggle_analysis.md` around
lines 32 - 34, The classification of Kimi Code and Gajae Code as
additive-provider clients is unsupported and should be marked as a hypothesis.
Update the related plan guidance to require documenting each client’s complete
add and remove contract, including Kimi’s model aliases, default_model, and
default_provider cleanup and Gajae Code’s schema fields and models.yml
references.


## 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.
Loading
Loading