diff --git a/docs/plans/2026-10-24_stack-model-proxy.md b/docs/plans/2026-10-24_stack-model-proxy.md new file mode 100644 index 000000000..b62b1dd96 --- /dev/null +++ b/docs/plans/2026-10-24_stack-model-proxy.md @@ -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 + `/secrets/agents///`, 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, `/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:`. `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": "" }`. +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 `/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 `/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 `/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:`, 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--<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--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: +`/secrets/agents///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=`, 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:", 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=`. 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 `/home/.pi/agent/models.json` with + `{"providers":{"anthropic":{"baseUrl":""}}}` (no `/v1`), and sets `ANTHROPIC_API_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=/home/.codex`, with `config.toml` naming a `model_provider` with + `base_url = "/v1"`, `env_key = "OPENAI_API_KEY"` and `wire_api = "responses"`, and + `OPENAI_API_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 `/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": , "binary": "" }` 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.