docs: design for CLIProxyAPI as the stack's model credential and routing layer, one proxy per instance, per-agent keys tied to rows 199 and 194, harness config from the proxy, row 96 points placed, row 157 re-scope, cutover and Jason's steps (mos-1597, Q249, Vikunja 237); doc only
This commit is contained in:
1 parent
2ce476d54c
commit
9a50f082f5
1 file changed
+360
@@ -0,0 +1,360 @@
|
||||
# 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.
|
||||
|
||||
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).
|
||||
|
||||
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 panel.
|
||||
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 same key into the panel to sign in. An agent never runs the prompt (Q173).
|
||||
|
||||
D7. **Upstream credentials.** Jason adds every upstream account in the proxy's own panel: OAuth sign-ins
|
||||
(Claude, Codex and the others CLIProxyAPI supports) and API keys (for example z.ai). They 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. **Panel path.** The Console forwards `https://mosaic.woltje.com/proxy/` to `127.0.0.1:<proxy.port>`, for
|
||||
the panel route and the management routes only. It forwards only after its existing auth-header and
|
||||
allowed-user check (decision 103, `packages/webui/src/serve.mjs`:453-469) passes. The Console never adds or
|
||||
stores the management key. Jason types it in the panel, and it passes through the forward. This adds no DNS
|
||||
record and no subdomain, as Q116 requires. Traefik and the tunnel already deliver mosaic.woltje.com to the
|
||||
Console (7332), so no infra change is needed. The forward gives no new authority. The management key
|
||||
is the barrier, and loopback processes can already reach the proxy port. Authentik is the outer layer. The
|
||||
#385 hole (a loopback process forging the Host and header) applies here the same way and is no worse,
|
||||
since the management key is still required.
|
||||
|
||||
A second Console (.54) forwards to its own instance's proxy, and its Authentik provider settings are Jason's
|
||||
(Q218).
|
||||
|
||||
Checks for the build row, on a stand-in under `unshare -rn` as in row 96:
|
||||
- the pinned panel works under the `/proxy/` sub-path (rewrite or base-path setting);
|
||||
- an OAuth sign-in done from the panel through the forward completes. If the callback can't return through
|
||||
the forward, the fallback is Jason running the proxy's CLI login in his own shell (`-claude-login
|
||||
-no-browser` and similar), and J3 says so.
|
||||
|
||||
## 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 can change all of them in the panel.
|
||||
- `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 panel (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). A session with no output for
|
||||
`proxy.firstOutputSeconds` (default 120) is stopped and logged as `proxy-stall`.
|
||||
|
||||
## 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, in the panel.
|
||||
|
||||
## 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 signs in to the panel and adds the accounts (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. Then open `https://mosaic.woltje.com/proxy/` and sign in to Authentik as jason.woltje.
|
||||
Paste the management key into the panel's login box.
|
||||
- **J4.** In the panel's auth or OAuth page, sign in to each subscription account you want this instance to use
|
||||
(Claude, Codex, and so on). On the API keys page, paste each API key you want it to have (for example z.ai).
|
||||
Set each account's priority. The highest one is used first, and the next one only when the first is
|
||||
limited. If an OAuth sign-in doesn't return to the panel, run the CLI login the cutover note gives, in your
|
||||
own terminal.
|
||||
- **J5.** Watch the Console while the two test sessions run (cutover step 6), and confirm the panel's usage page
|
||||
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, panel sub-path, OAuth through the forward) | 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 | Console `/proxy/` forward behind the existing auth check | P1 |
|
||||
| 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: the Console forwards `/proxy/` rather than a new Traefik route.
|
||||
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.
|
||||
Reference in new issue
Block a user