Source-candidate: poe d9a564e6d20f5f0fd75ce45b12cbbf8bb4c9d568\nSource-candidate: magica38ba5762b06716db1dee00d0afec162656488f5c\nValidated321whole-file phase plus whole bus1349/model-proxy100/webui490; final gate tooling checks pending, not landed.
367 lines
28 KiB
Markdown
367 lines
28 KiB
Markdown
# Design: CLIProxyAPI as the stack's model credential and routing layer
|
|
|
|
Written 2026-10-24 by Sage for Mos's mos-1597 (Vikunja 237). It carries out Jason's Q249 ruling that CLIProxyAPI
|
|
becomes the stack's credential and routing layer, with Q116 and Q127 still in force. This is a design document.
|
|
Mos rules it before any code. Its build rows go to new Codex gpt-6.1-sol threads (Q246), and each gets its own
|
|
brief once Mos has ruled. Line numbers are at origin/refactor 2ce476d54.
|
|
|
|
**Amendments.** Mos's mos-1600, mos-1601 and mos-1602 changes live in the row briefs
|
|
(`docs/plans/2026-10-24_model-proxy-rows.md`), which win over this design where they differ. Jason's Q46
|
|
and Q47 answers (DECISIONS Q250, mos-1606, 2026-10-24) are written into this file: accounts are entered in
|
|
the Console's own proxy page, and the `/proxy/` forward to the upstream panel (D8) is dropped. The P4 brief
|
|
for that page is `docs/plans/2026-10-24_proxy-page-brief.md`. Text marked "(Q250)" below is that change.
|
|
|
|
Not to be confused with queue row 237 ("one decision lands once", `2026-10-20_agent-credentials.md`). This is
|
|
Vikunja task 237.
|
|
|
|
## Inputs
|
|
|
|
- **Q116** (jason-master-restart DECISIONS.md:709, :724-725; lead-decisions.md:2578-2585). The WebUI goes under
|
|
mosaic.woltje.com behind Authentik, on a path of the existing host. A new DNS record or subdomain goes back
|
|
to Jason first. Authentik admits only jason.woltje. No agent reads the management key.
|
|
- **Q127** (:779-782). Seats talk only to a local proxy, using keys the stack issues and rotates. Upstream
|
|
tokens and keys stay in the proxy. Switching live seats to the proxy, and the subscription-terms check, go
|
|
back to Jason.
|
|
- **Q249** (:2512). The proxy serves one user's agents on that user's machine. It is not shared with other
|
|
users and not used in a commercial app. The host proxy on 127.0.0.1:8317 is not touched. Decision 108's
|
|
token file and claude-session.env are a stopgap. One proxy per user.
|
|
- **Q153/Q154** (:1088, :1096): usage sources and quota enforcement sit behind a plug-in interface. **Q222**
|
|
(:2194): limits are settings with defaults, and a setting that guards the human from agents is
|
|
human-only.
|
|
- **Row 96** (#1590), `agents/researcher/work/i1-proxy-test/REPORT.md`, tested against CLIProxyAPI 8.0.27. Issue
|
|
is 1 PUT, revoke is 1 DELETE, with no restart and effect on the next request. Keys survive a restart.
|
|
Claude Code works with `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN`. Pinned pi 0.85.1 needs a `models.json`
|
|
in the session HOME. Eight open points (:190-216) and six design items (:218-236). Row 96's queue note adds
|
|
two more. `GET api-keys` returns every client key in full. The claim that the 401 body shape causes the
|
|
Claude Code stall is unverified, so test it before any fix aimed at the shape.
|
|
- **Row 199** (landed cb5fdb064): the agent credential store, files under
|
|
`<configDir>/secrets/agents/<business>/<agent>/<kind>`, 0700 directories and 0600 files
|
|
(`agents/megavolt/work/queue-199/DESIGN.md`:63-65). Class `Credentials` is in
|
|
`packages/bus/src/credentials.mjs` (`issue` :291, `revoke` :383, `remove` :400, `useFor` :417, `status` :474).
|
|
D7 (:118-125) deferred per-agent model tokens to "a proxy ... its own row". This is that row.
|
|
- **Row 194** (landed fd6dd327c): fire stops the agent's runs, then delists it
|
|
(`packages/bus/src/broker.mjs` `#chiefLifecycle` :5038, `instance.fire` :5117). `revoke()` in
|
|
`packages/bus/src/runtime.mjs`:166-197 revokes before the write and refuses `credential-revoke-failed`.
|
|
- **How sessions get model credentials today.** The bus unit loads `claude-session.env`
|
|
(`packages/cli/systemd/mosaic-bus.service.in`:21-25). `sessionEnv` passes every `ENV_ALLOW` variable to every
|
|
session (`packages/seat/src/session.mjs`:242-262). Inside the sandbox, HOME is the run's own `home/`
|
|
(:240-241). The sandbox doesn't unshare the network (`packages/seat/src/sandbox.mjs`:70), so a session reaches
|
|
host loopback. Pi has no model credential today (P19 #383). There is no Codex harness plug-in
|
|
(`packages/plugins/harness/` holds claude-code and pi only).
|
|
|
|
## 1. Where the proxy runs
|
|
|
|
D1. **One proxy per stack instance, run by the stack.** It runs as the instance's OS user, and since every
|
|
instance has one user (invariant 2) that gives one proxy per user per machine. The default instance on
|
|
dragon-lin gets one. Melanie's .54 hub gets its own as `mosaic` under `/srv/mosaic`. Nothing is shared
|
|
between instances. The host proxy on 127.0.0.1:8317 is not started, stopped, read or configured by any of
|
|
this, and its config and data directory are not read.
|
|
|
|
D2. **Process and files.**
|
|
- A systemd user unit `mosaic-proxy.service`, written by the same scripts that write `mosaic-bus.service`
|
|
(`scripts/instance.sh`, `scripts/case2-unit.sh`). The bus unit gets `Wants=` and `After=` on it. The bus
|
|
refuses to launch a session when the proxy is configured but not answering (D17).
|
|
- The proxy's own directory, `<dataRoot>/proxy/`, mode 0700, holds `config.yaml` (0600), `auth/` (the
|
|
`oauth.auth-dir` for its OAuth logins, 0700) and `logs/`. No other component reads files in it. Run records
|
|
never copy from it.
|
|
- It listens on `127.0.0.1:<proxy.port>`. `allow-remote: false`, so management calls are accepted from
|
|
loopback only.
|
|
|
|
D3. **System config.** The instance config gets an optional `proxy` section: `{ "port": N, "binary": "<path>" }`.
|
|
With no section, the instance runs as today on claude-session.env. That is how migration works one instance
|
|
at a time (section 9). With the section, both fields are required, and a missing or bad value refuses. The
|
|
port can't be 8317, and the bus refuses at boot if another process already listens on it. Bootstrap writes
|
|
the section only for a new config, as it does for every other field. An existing config gets it by a human
|
|
edit at cutover (Jason's step J2). This changes the config schema, so Mos must rule on it.
|
|
|
|
D4. **Pinned binary.** `packages/model-proxy/PIN` records the CLIProxyAPI version, the release asset name and
|
|
its sha256, the same way pi is pinned exactly. The tested version is 8.0.27. The unit starts only a binary
|
|
whose sha256 matches PIN. The control panel asset is pinned the same way, with
|
|
`disable-auto-update-panel: true`. The proxy otherwise downloads the panel from GitHub at runtime
|
|
(`config.example.v8.0.27.yaml`:94-103), and that would be unreviewed code behind Authentik. The stack never
|
|
copies the 8317 binary. The first fetch of the release asset counts as external access, so it is Jason's
|
|
step J1 or a row Mos approves. For containerized installs the binary and panel go into the proxy image
|
|
(section 8). Q250: nothing serves the panel to the Console now. P1 keeps the pin as built, and
|
|
removing it is a later cleanup.
|
|
|
|
D5. **Initial config, then hands off.** The stack writes `config.yaml` once, when `<dataRoot>/proxy/` is
|
|
first created, from a template in `packages/model-proxy/`, and never writes the file again. After that the
|
|
stack changes the proxy only through the management API, and Jason changes it only through the Console's
|
|
proxy page, which calls the bus, which calls the management API (Q250).
|
|
Row 96 point 2 (every management write rewrites the whole file and reorders it) then stops mattering,
|
|
because nobody edits the file by hand. The template sets the defaults in section 3 and section 7,
|
|
`request-retry: 0` so no hidden retries happen (as in row 96), the client-key list holding only the
|
|
sentinel key (D11), and the management key (D6).
|
|
|
|
## 2. Credential entry
|
|
|
|
D6. **Management key.** Jason sets the management key once, at a human-only prompt
|
|
(`mosaic proxy init`, run in his own shell, path-bound human proof as for other human verbs, #225). The
|
|
prompt doesn't echo the key. It writes the key to `<configDir>/secrets/proxy-management.key` (0600) and into
|
|
the initial `config.yaml`. The proxy hashes it in place on its first start (row 96 point 6). The stack reads
|
|
only the secrets file, never the hashed value in config. The holder is the bus process alone (D12). The
|
|
sandbox doesn't mount `<configDir>/secrets`, so no session can read the key, and no verb returns it. Jason
|
|
types the key only at that prompt. The Console's proxy page needs only his Console sign-in, and the bus adds
|
|
the key to its own management calls (Q250). An agent never runs the prompt (Q173).
|
|
|
|
D7. **Upstream credentials.** Accounts are entered in the Console's proxy page (Q250). It covers OAuth
|
|
sign-ins for Claude and Codex through the management API's `oauth/auth-url`, `oauth/callback` and
|
|
`oauth/status`, upstream API keys (z.ai first, write-only and never echoed, mos-1608), and account
|
|
priority. The page shows each account by an id, the first 12 hex of the sha256 of the proxy's own
|
|
credential identifier, never of a token or key value (mos-1608). The credentials live in the
|
|
proxy's `auth/` directory and config and nowhere else. There is no per-seat credential file. The stack never
|
|
reads, copies or logs an upstream credential.
|
|
|
|
D8. **Dropped (Q250).** The design had the Console forward `https://mosaic.woltje.com/proxy/` to the
|
|
proxy's own panel. Jason's Q46 and Q47 answers replace it with the Console's own proxy page (D7). The page
|
|
calls bus routes, the bus calls the management API (D12), and the management key stays in the bus (D6). No
|
|
new hostname is added. A panel subdomain is a fallback only if our page can't keep up with the management
|
|
API across pinned proxy upgrades, and that call is Jason's. Each pinned upgrade runs the page's acceptance
|
|
script against the new version (P4 brief item 9).
|
|
|
|
## 3. Priority routing, and how the business routing table maps onto it
|
|
|
|
D9. **Who picks what.** The business routing table picks the harness, model and thinking for a role
|
|
(`packages/business/src/vars.mjs`:139-147, resolved by `sessionModel` in `packages/harness/src/bundle.mjs`:77-86).
|
|
The proxy picks which upstream account serves that model. The two don't overlap:
|
|
- The model id in the business table is the id the session sends. The proxy routes it to an account that
|
|
serves that model.
|
|
- The business `fallback.*` vars stay the stack's. They still apply only when the primary harness's preflight
|
|
fails (`packages/cli/src/launcher.mjs`:390-397).
|
|
- Account failover inside one model is the proxy's job. The stack doesn't model accounts per session.
|
|
|
|
D10. **Priority settings.** Defaults set in the template are listed below (Q222: settings with defaults).
|
|
Jason sets account priority in the Console's proxy page. The other routing settings keep the template
|
|
defaults, and a page control for them is a later row (Q250).
|
|
- `routing.strategy: fill-first`. A subscription account is used until it is rate-limited or out of quota, then
|
|
the next one (`config.example`:150).
|
|
- Each account gets a `priority` (larger first, `config.example`:480). Jason sets the order in the Console's
|
|
proxy page (J4).
|
|
- `routing.session-affinity: true`, so a session stays on one account while it works and keeps its prompt
|
|
cache (:154-171). Default `session-affinity-rebind-on: quota`, so a transient 5xx doesn't move a session.
|
|
- No model aliases or alias pools by default. A pool that maps one name to several upstream models
|
|
(:869) would let the proxy change the model under a role. Model choice stays in the business table, where
|
|
a reviewed edit can see it. If Jason adds an alias himself, that's his call.
|
|
|
|
`launch.models` (`packages/business/src/launch.mjs`:12-24) gets a provider value `proxy`, meaning "this model id
|
|
through the instance proxy". `launch.account` (:81-89) with a proxy names the proxy rather than an
|
|
environment variable set. The row that implements it settles the exact key name. A business without
|
|
`proxy` providers keeps working unchanged.
|
|
|
|
Not settled by the design, and checked in the build row: whether 8.0.27 can restrict a client key to some
|
|
models. If it can, an agent's key is narrowed to its role's models as a second guard. If not, the bundle's
|
|
model choice is the only guard, as it is today.
|
|
|
|
## 4. Per-agent keys
|
|
|
|
D11. **Key format and the sentinel.** A stack-issued client key is `msk-<instanceTag>-<32 random bytes, base64url>`.
|
|
The prefix tells stack keys apart from any key Jason adds himself. The stack manages only keys with its own
|
|
prefix and leaves every other key alone. One sentinel key, `msk-<tag>-sentinel-...`, is written by the
|
|
template and never handed out. It keeps the list non-empty, so the proxy never falls into a possible open
|
|
mode on an empty list (row 96 point 5). The build row tests the empty-list behavior on a stand-in, and the
|
|
result goes in BUILD.md either way. The bus refuses to launch (`proxy-open`) if a `GET` of the list shows
|
|
no sentinel.
|
|
|
|
D12. **Who calls the management API.** The bus process only, through a new `packages/model-proxy/` client. It
|
|
reads the management key from D6's file, holds it in memory, and sends it only in the `Authorization` header to
|
|
loopback. No CLI verb, Console route or session tool exposes it. Every management call is logged as a
|
|
receipt (call, status, key id) with no key value.
|
|
|
|
D13. **The stack's record.** The key is a row 199 store kind, `model`, one per agent:
|
|
`<configDir>/secrets/agents/<business>/<agent>/model` (0600). This replaces D7's "per stack instance only"
|
|
for instances with a proxy. The file holds the key value, so the bus can hand it to a session. Its status
|
|
record (`Credentials.status`) holds a key id (the first 12 hex of its sha256), the issue time and the state.
|
|
It never holds the value. The record is authoritative. The proxy's list is derived state, and the bus
|
|
reconciles it.
|
|
- **Boot.** The bus GETs the list once and compares it with the record. A key that is in the record but not
|
|
in the proxy is re-PUT. A stack-prefixed key that is in the proxy but not in the record, or is revoked
|
|
there, is removed. The reconcile writes a receipt.
|
|
- **No role-file change.** The model key needs no `credentials.model` reference in the business file. Every
|
|
agent of an instance with a proxy gets one implicitly. The tradeoff: a business can't opt a role out. The
|
|
other way would add `model` to every role definition, which means changing `roles/*.json`, and that is a
|
|
reviewed policy change for no benefit today.
|
|
|
|
D14. **Issue, rotate, revoke.**
|
|
- **Issue.** `instance.hire` (`packages/business/src/instance-settings.mjs`:354) and the first launch of an agent
|
|
without a key both call `Credentials.issue(model)`. The bus GETs the list, adds the key and PUTs the list.
|
|
It then GETs again and checks the key is present before it writes the store file. If any step fails, no
|
|
file is written and the bus refuses `proxy-key-issue-failed`.
|
|
- **Rotate.** `rotateBy` (row 199 D2) defaults to 30 days for model keys (Q222 setting). The bus PUTs the new
|
|
key alongside the old one, writes the new file, and launches new sessions with the new key. It removes the
|
|
old key once the agent's runs that started on it have ended, or after `proxy.rotateGraceMinutes` (default
|
|
120), whichever comes first. A session still running at that point is stopped. That's 3 calls with no
|
|
restart, and the two keys overlap for the window (row 96 rotation item).
|
|
- **Revoke.** Fire already stops the agent's runs before it delists the agent (row 194). The revoke step in
|
|
`runtime.mjs revoke()` gains the model kind: a GET, then a PUT of the list without the key, then a GET to
|
|
confirm. The store file goes only after that. If the revoke fails, the fire is refused
|
|
`credential-revoke-failed`, as for the other kinds. Revoke never uses the v0 `DELETE ?value=<key>`, because it
|
|
writes the full key into the access log (row 96 point 1). The v8 PUT doesn't.
|
|
|
|
D15. **Plaintext at rest, accepted.** The proxy keeps client keys in plaintext in `config.yaml`, and `GET api-keys`
|
|
returns them in full (row 96 point 1, queue note a). This is accepted, with these limits:
|
|
- the file and the store sit under the instance user's 0700 directories, which no session mounts;
|
|
- the management client never logs a list body, and the GET response stays in memory only;
|
|
- the usage reader (section 7) maps a key to its id at read time and stores only the id.
|
|
|
|
A key leak lets someone spend through the proxy until the key is revoked. It never exposes an upstream
|
|
credential.
|
|
|
|
## 5. Harness config written from the proxy
|
|
|
|
D16. **One source.** At launch the bus resolves the session's route and the agent's key, and passes the harness
|
|
module `{ baseUrl: "http://127.0.0.1:<port>", key }` alongside today's `env` answer. Each harness module writes
|
|
its own config from that. It's rewritten on every launch, so a port or key change reaches the next session
|
|
with nothing cached. No harness reads `~/.claude`, `~/.pi` or `~/.codex` of the host (managed-session-config.md:36-39).
|
|
When the proxy is configured, `sessionEnv` stops passing the instance-wide `CLAUDE_CODE_OAUTH_TOKEN`,
|
|
`ANTHROPIC_*`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY` and `ZAI_API_KEY`. A session gets only its own key, so a
|
|
proxy-mode session can't reach an upstream with the instance's token.
|
|
- **Claude Code** (`packages/plugins/harness/claude-code/index.mjs`, `env` :119-126): `ANTHROPIC_BASE_URL` and
|
|
`ANTHROPIC_AUTH_TOKEN=<key>`. This already satisfies `SESSION_CREDENTIALS` (`session.mjs`:249-253) and
|
|
passes `ENV_ALLOW`. Row 96 ran exactly this.
|
|
- **pi** (`packages/plugins/harness/pi/index.mjs`, `env` :63-70): writes `<run>/home/.pi/agent/models.json` with
|
|
`{"providers":{"anthropic":{"baseUrl":"<baseUrl>"}}}` (no `/v1`), and sets `ANTHROPIC_API_KEY=<key>` (row 96
|
|
pi finding, :123-136). For a non-Anthropic route (z.ai through the proxy), the same file names that
|
|
provider's base URL. pi gains a preflight that refuses `no-session-credential` when neither a proxy key nor
|
|
a pi credential is present. This settles P19 #383 for proxy-mode instances: pi gets a key of its own, and
|
|
nobody maps `CLAUDE_CODE_OAUTH_TOKEN` into a pi variable. Whether that closes #383 is Jason's call.
|
|
- **Codex.** The stack has no Codex harness plug-in yet. This design fixes its proxy config for when one
|
|
exists: `CODEX_HOME=<run>/home/.codex`, with `config.toml` naming a `model_provider` with
|
|
`base_url = "<baseUrl>/v1"`, `env_key = "OPENAI_API_KEY"` and `wire_api = "responses"`, and
|
|
`OPENAI_API_KEY=<key>`. The Codex harness row writes it. The T3 Codex threads the development seats use are
|
|
not stack sessions and are out of scope.
|
|
|
|
## 6. Row 96's open points
|
|
|
|
| # | Point | Disposition |
|
|
|---|---|---|
|
|
| 1 | Plaintext keys in config, the usage queue and the v0 DELETE log | Accepted at rest (D15). Avoided in logs: no v0 DELETE (D14), and the usage reader stores ids (section 7) |
|
|
| 2 | Every management write rewrites the whole config | Solved: nobody edits the file after creation (D5) |
|
|
| 3 | No per-key accounting, 60 s queue window | Solved by the stack counting its own tokens (REQ-LIMIT-2). The queue reader is an optional plug-in that polls faster than the window (section 7) |
|
|
| 4 | Revoked or wrong key looks like a hang in Claude Code | Solved without relying on the client (D17). The 401-shape cause stays unverified and no shape fix is in scope. The build row tests it on the stand-in and records the result (queue note b) |
|
|
| 5 | Empty key list unknown | Solved by the sentinel (D11). Tested on a stand-in, result recorded |
|
|
| 6 | Management key hashed in place at startup | Solved: the stack reads only its own secrets file (D6) |
|
|
| 7 | pi ignores `ANTHROPIC_BASE_URL` | Solved: pi gets a session `models.json` (D16) |
|
|
| 8 | Stand-in limits: no real API, rate limits, token counting or routing | Accepted for the build rows, which run on stand-ins. The first real upstream traffic is Jason's cutover step (Q127), J5 |
|
|
| a | `GET api-keys` returns every key in full | Accepted, in memory only (D15) |
|
|
| b | 401 shape as the stall cause, unverified | Tested before any shape fix. None is planned (point 4) |
|
|
|
|
D17. **Key and proxy preflight.** Before a launch in proxy mode, the bus makes one request with the agent's key,
|
|
`GET /v1/models`. The build row confirms on the stand-in that this is answered locally with no upstream call. A
|
|
401 refuses the launch with `proxy-key-refused`. A connection failure refuses with `proxy-down`. Neither starts a
|
|
session, so a revoked key never reaches Claude Code's silent retry. A key that is revoked mid-session is
|
|
revoked by the stack, which stops that session first (D14). Idle before the first actual adapter turn
|
|
is unarmed. The successful adapter-child spawn arms one initial `proxy.firstOutputSeconds` budget
|
|
(default 120) per proxy run; later turns never reset or extend it and first output cancels it. A turn
|
|
still silent when that budget expires is stopped through the existing trusted path and logged as
|
|
`proxy-stall`. Invalid first-turn metadata also uses that same stop and value-free receipt once,
|
|
unless first output already cancelled the watcher. The internal timestamp protocol uses the current
|
|
Linux host/runner shared monotonic clock; a confined timestamp-bound check is required verification.
|
|
|
|
## 7. Usage and limits
|
|
|
|
D18. **What counts.** The stack counts the tokens its own sessions use, per agent key and per account, as
|
|
REQ-LIMIT-2 says (docs/prd/mosaic-stack.md:964, `packages/tasks/requirements.json`:57). It's the default and
|
|
only built-in source. The existing `launch.limits.tokens` keys (`keys`, `daily`;
|
|
`packages/business/src/launch.mjs`:60-75) get the agent key id as a valid `keys` name, so a daily quota per agent
|
|
key is a business setting. It defaults to unset, which means no per-key quota.
|
|
|
|
D19. **The plug-in.** Under Q153/Q154, a `usage` plug-in kind takes one optional module, `cliproxy-queue`. It
|
|
drains `GET /v8/management/observability/usage/queue?count=100` through the bus's management client every
|
|
`usage.cliproxyQueue.pollSeconds` (default 15, under the 60 s retention). It maps each event's `api_key` to a key
|
|
id, then drops the value. It records upstream failures (`fail.status_code`) per key id. It is off by default,
|
|
and turning it on is a `plugins.set` change. When on, it is a second source that the Console shows next to the
|
|
stack's own count. It never enforces a quota alone. A queue event for a key with a `msk-` prefix that isn't
|
|
in the record goes in the reconcile log.
|
|
|
|
D20. **Settings and who changes them.** Every number in this design is a setting with the default shown:
|
|
`rotateBy` 30 d, `proxy.rotateGraceMinutes` 120, `proxy.firstOutputSeconds` 120, `usage.cliproxyQueue.pollSeconds`
|
|
15, routing strategy fill-first, affinity on. The per-key daily quota and the rotation period guard the human
|
|
from agents, so only a human can change them (Q222). The routing settings live in the proxy and only Jason
|
|
changes them. Account priority is in the Console's proxy page, and the rest keep their defaults (Q250).
|
|
|
|
## 8. Row 157 re-scope (#1652, Vikunja 617)
|
|
|
|
Row 157 is on hold (Q248, mos-1568/1569), and this design doesn't start it. When it resumes, its brief is
|
|
amended this way, and Mos rules the amendment:
|
|
- **Egress.** The `agents` network's allowed list is the proxy container only. api.anthropic.com, api.z.ai and
|
|
api.openai.com come off the agents list (bentina/row157's draft `contracts/egress.json`) and go to a new `proxy`
|
|
list. The proxy container sits on `mosaic-agents` and `mosaic-egress` (containerized-install-design.md:786-830),
|
|
and its CONNECT calls are logged to `<dataRoot>/state/egress.jsonl` like any other.
|
|
- **The real-token run is cancelled.** `provider-hosts.sh` and `provider-runner.mjs` passed
|
|
`CLAUDE_CODE_OAUTH_TOKEN` and `ZAI_API_KEY` into a CLI child. That run is not done, and is replaced by a
|
|
stand-in run. A session container reaches the stand-in only through the proxy container, and a direct
|
|
connection to an upstream name is refused.
|
|
- Decision 108's mounted token file (`/run/mosaic/secrets/claude-code-oauth.token`) is not mounted into session
|
|
containers of a proxy-mode install. The proxy container holds the upstream credentials in its own volume.
|
|
|
|
## 9. Migration
|
|
|
|
claude-session.env (host installs) and decision 108's file (containerized) stay until each instance switches.
|
|
An instance switches only when its config gains the `proxy` section (D3). Until then nothing about it
|
|
changes. Cutover, per instance, with Jason present (Q127):
|
|
|
|
1. The build rows land and pass the landing gate. They don't change any instance yet.
|
|
2. Jason fetches and verifies the pinned binary and panel (J1). ops-01 or Sage checks the sha256 against PIN.
|
|
3. Jason adds the `proxy` section to the instance config (J2), runs `mosaic proxy init` (D6), and starts
|
|
`mosaic-proxy.service`. The bus isn't restarted yet.
|
|
4. Jason adds the accounts in the Console's proxy page (J3, J4). No session uses the proxy yet.
|
|
5. A Console or bus restart (Jason or Mos, per Q173) picks up the section. The bus reconciles, issues a key
|
|
for each listed agent, and runs D17's preflight for each one. Any refusal stops the cutover, and the fix
|
|
is to remove the section and restart.
|
|
6. One test session per harness, launched by the tester actor, makes one real turn through the proxy (J5).
|
|
This is the first real upstream traffic on the new path.
|
|
7. Once both harnesses pass, the bus unit drops `EnvironmentFile=` for claude-session.env. The file stays on disk
|
|
for a 7-day rollback window, then Jason deletes it (J6). Rollback is removing the section and restoring the
|
|
`EnvironmentFile=` line.
|
|
|
|
Order: the dragon-lin default instance first, then .53, then Melanie's .54 hub once Jason has picked its
|
|
accounts (J7). The 8317 host proxy and its clients are not part of any step.
|
|
|
|
## 10. Jason's steps
|
|
|
|
- **J1.** In a browser, open the CLIProxyAPI release page for the version in PIN and download the named asset
|
|
and the panel asset into the folder the cutover note names. Sage checks the hashes. (If Mos assigns the
|
|
fetch to a row instead, J1 drops.)
|
|
- **J2.** Add `"proxy": { "port": <port from the cutover note>, "binary": "<path>" }` to the instance's
|
|
`config.json`, in an editor.
|
|
- **J3.** In a terminal of your own, run `mosaic proxy init` and type a new management key twice. Keep it in your
|
|
password manager. You don't type it anywhere else (Q250).
|
|
- **J4.** Open the Console's proxy page and sign in to Authentik as jason.woltje. Sign in to each subscription
|
|
account you want this instance to use (Claude, Codex). If the sign-in tab doesn't return on its own, copy
|
|
the full address from that tab and paste it into the page. Set each account's priority. The highest one is
|
|
used first, and the next one only when the first is limited. Paste each API key you want it to have (for
|
|
example z.ai) into the page's API-key field. The page never shows it again.
|
|
- **J5.** Watch the Console while the two test sessions run (cutover step 6), and confirm the proxy page's usage
|
|
section shows their requests on the account you expect.
|
|
- **J6.** After 7 days on the proxy, delete `claude-session.env` for that instance.
|
|
- **J7.** For Melanie's .54 hub, decide which accounts it uses: hers, yours, or new ones. Then do J2 to J4 on .54
|
|
through its own Console's Authentik provider (Q218). Only accounts you entered for that hub go into its
|
|
proxy.
|
|
|
|
## Build rows (proposed, each briefed after Mos rules)
|
|
|
|
All are Codex builds on new gpt-6.1-sol threads (Q246), first review by a separate Codex thread. They touch
|
|
credentials, so a Claude reviewer (Dewey) gives the second review. Sage runs the landing gate.
|
|
|
|
| Row | Piece | Depends on |
|
|
|---|---|---|
|
|
| P1 | `packages/model-proxy/`: PIN, template, management client, `mosaic proxy init`, the `mosaic-proxy` unit, config `proxy` section, stand-in tests (empty list, `/v1/models` local, 401 shape, OAuth callback URL; the panel checks are dropped, Q250) | Mos's ruling on D3 |
|
|
| P2 | Per-agent keys: `model` store kind, issue, rotate, revoke, reconcile, D17 preflight | P1, rows 194 and 199 |
|
|
| P3 | Harness config: Claude Code env, pi `models.json` and preflight, `sessionEnv` proxy mode | P2 |
|
|
| P4 | The Console proxy page and its bus verbs (Q250, `2026-10-24_proxy-page-brief.md`) | P1; lands after P2 |
|
|
| P5 | `usage` plug-in kind and the `cliproxy-queue` module; key ids in `launch.limits.tokens` | P2 |
|
|
| P6 | Row 157 brief amendment (section 8) | Row 157 resuming |
|
|
| P7 | Cutover note and per-instance window (section 9) | P1 to P4 landed; Jason present |
|
|
|
|
The Codex harness config (D16) goes with the Codex harness row, which isn't on this list.
|
|
|
|
## What Mos rules
|
|
|
|
1. D1/D3: the `proxy` section in the instance config (a schema change), and the proxy as its own user unit.
|
|
2. D6: the management key set by Jason at a human-only prompt and held only by the bus process.
|
|
3. D8: ruled. Jason's Q46 and Q47 (Q250) drop the forward for the Console's own proxy page.
|
|
4. D13: an implicit per-agent model key with no role-file change.
|
|
5. D15: plaintext keys at rest accepted under those limits.
|
|
6. D16: proxy mode drops the instance-wide provider variables from every session.
|
|
7. J1: Jason fetches the binary, or a row does.
|