diff --git a/docs/plans/2026-10-24_proxy-page-brief.md b/docs/plans/2026-10-24_proxy-page-brief.md index a27c49c44..4c005dc49 100644 --- a/docs/plans/2026-10-24_proxy-page-brief.md +++ b/docs/plans/2026-10-24_proxy-page-brief.md @@ -1,6 +1,7 @@ # Brief: row 330 (P4), the Console proxy page Written 2026-10-24 by Sage for Mos's mos-1606, which carries Jason's Q46 and Q47 answers (DECISIONS Q250). +Mos approved it in mos-1608, adding upstream API keys (item 4) and the account id rule (item 2). Read the design (`docs/plans/2026-10-24_stack-model-proxy.md`, amended for Q250) and the rows doc (`docs/plans/2026-10-24_model-proxy-rows.md`) first. The rows doc's "Rules for every row" apply here unchanged. Line numbers are at origin/refactor 0ce22dde6. Check them before you edit. @@ -15,16 +16,17 @@ landing. The P4 brief lives here instead. ## Brief: P4 the Console proxy page -**Why.** Q250: Jason enters accounts in the Console's own proxy page, not in the upstream panel. The page -calls bus routes, and the bus calls the proxy's management API (D12). 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 +**Why.** Q250: Jason enters accounts in the Console's own proxy page, not in the upstream panel. That +covers both his OAuth accounts and his API keys (Q248 item 1, Q249, mos-1608). The page calls bus routes, +and the bus calls the proxy's management API (D12). 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 (Q46). **What ships.** 1. **Bus verbs.** New broker verbs for the page, for example `proxy.accounts`, `proxy.login.start`, - `proxy.login.submit`, `proxy.login.status`, `proxy.login.cancel`, `proxy.priority.set` and - `proxy.keys`. Settle the names in BUILD.md. + `proxy.login.submit`, `proxy.login.status`, `proxy.login.cancel`, `proxy.apikey.add`, + `proxy.priority.set` and `proxy.keys`. Settle the names in BUILD.md. - Every verb is human-only. A tester, agent or granted-user caller refuses `human-required`, the way the grants admin verbs do (`packages/bus/src/broker.mjs`:3721-3731). - With no proxy in the validated config (P1 item 3), every verb refuses `not-configured`. @@ -36,8 +38,9 @@ management API across pinned proxy upgrades, and that call is Jason's (Q46). endpoint (`/credentials` in the 8.0.27 doc, `agents/researcher/work/i1-proxy-test/docs/management-api-v8.0.27.md`:101). - The bus copies only allowlisted fields: provider, a display name, status and priority, plus an - account id (the first 12 hex of a sha256, as for key ids). Every other field is dropped, so no token - can pass through even if the proxy returns one. + account id. Every other field is dropped, so no token can pass through even if the proxy returns one. + - The account id is the first 12 hex of the sha256 of the proxy's own credential identifier, never of a + token or key value (mos-1608). - Record in BUILD.md which endpoint and fields the pinned version has. 3. **OAuth sign-in** through `oauth/auth-url`, `oauth/callback` and `oauth/status` (management-api-v8.0.27.md:108-138). @@ -56,18 +59,31 @@ management API across pinned proxy upgrades, and that call is Jason's (Q46). `wait` doesn't, because the doc says acceptance isn't completion (:136-138). The bus stops polling on `ok`, `error`, or after a timeout setting (default 300 s). - **Cancel** calls `DELETE oauth/session`. -4. **Priority.** `proxy.priority.set` sets one account's priority, an integer in a range the builder +4. **Upstream API keys** (mos-1608). A human-only verb (for example `proxy.apikey.add`; name it in + BUILD.md) adds one upstream API key. + - The provider allowlist starts with z.ai only. Adding a provider is a reviewed change. BUILD.md records + which management endpoint the pinned 8.0.42 uses for it. + - The Console route has the same checks as the other writes (item 8). The page's input is + `type=password` with `autocomplete=off`. + - The value is write-only. Neither the Console nor the bus logs it, stores it, caches it, or puts it in + a receipt or an error. No part of it is echoed, not even the last four characters. The bus passes it + straight to the management API in the request body and then drops it. + - After the add, the key shows in `proxy.accounts` through the same allowlisted fields, and priority + (item 5) works on it. + - An empty value, one over a size limit, or one outside a charset refuses `apikey-invalid`. A provider + outside the allowlist refuses `provider-not-allowed`. +5. **Priority.** `proxy.priority.set` sets one account's priority, an integer in a range the builder records. Use the pinned version's credential-field endpoint (`/credentials/fields` PATCH in the 8.0.27 doc). Confirm it on the stand-in and record what it changes. A bad account id or value refuses `priority-invalid`. -5. **Client keys, list only.** `proxy.keys` reads `GET /config/access/api-keys`, maps each value to its key +6. **Client keys, list only.** `proxy.keys` reads `GET /config/access/api-keys`, maps each value to its key id in memory and drops the value (D15). - It joins P2's record to show agent, issue time and state for each key id. - The sentinel is labelled. A key without this instance's prefix shows as "not stack-managed". - The page has no key write. Issue and revoke stay with hire and fire (P2). -6. **Usage.** P5's Console view (rows doc P5 item 3) sits on this page. Whichever of P4 and P5 lands +7. **Usage.** P5's Console view (rows doc P5 item 3) sits on this page. Whichever of P4 and P5 lands second puts it there. P4 adds no usage source. -7. **The page and its Console routes.** An owner-only `/proxy` page in `packages/webui`, built like the +8. **The page and its Console routes.** An owner-only `/proxy` page in `packages/webui`, built like the existing pages, with routes under `/api/proxy/`. - Each route makes one call over the owner's human transport, the way the Agents section does (`AGENT_ROUTES` at `packages/webui/src/serve.mjs`:451, `agentCaller` at @@ -79,14 +95,14 @@ management API across pinned proxy upgrades, and that call is Jason's (Q46). - The Console never holds the management key, never reads `proxy` from the config, and never opens a connection to the proxy port. - Logs record method, route, status and user only. They never record the authorization URL, a pasted - callback URL, a state or a body. -8. **Upgrade check.** An acceptance script, beside P1's and outside the gate's `scripts/test-*.sh` set, + callback URL, a state, an API key or a body. +9. **Upgrade check.** An acceptance script, beside P1's and outside the gate's `scripts/test-*.sh` set, runs the bus verbs against the pinned binary under `unshare -rn`. It uses a made-up config, key and data root in a fresh temp root, the same way P1's run does. - With no network, it checks what can be checked offline. The account list starts empty. `auth-url` returns a URL and a state for `claude` and for `codex`. A callback with a made-up code never reports - `ok`. A priority set on a made-up credential reads back, if the stand-in can hold one. The key list - returns ids only. + `ok`. A made-up z.ai API key added through the verb shows in the account list, and a priority set on + it reads back. The key list returns ids only. - It prints the endpoints and fields the page relies on, so a version change shows up as a diff. - `packages/model-proxy/README.md` records the rule: a PIN change lands only with this script's output for the new version in that row's BUILD.md, and Sage's landing gate checks for it. @@ -96,8 +112,14 @@ management API across pinned proxy upgrades, and that call is Jason's (Q46). **Tests.** Cover each of these: - every verb against a fake management server; - each refusal code by its own test (mos-1590): `not-configured`, `human-required` for the tester, an - agent and a granted user, a provider outside the allowlist, `login-state-unknown`, - `callback-url-invalid`, `priority-invalid` and `proxy-down`; + agent and a granted user, `provider-not-allowed` for sign-in and for an API key, `login-state-unknown`, + `callback-url-invalid`, `apikey-invalid` (empty, over the size limit and outside the charset, each its + own case), `priority-invalid` and `proxy-down`; +- an API-key canary: a unique made-up value goes through the Console route against the fake management + server. Afterwards every Console and bus log, receipt, response, store file and error output from that + run has zero matches for it, and the fake received it exactly once; +- an added API key listed by `proxy.accounts` with allowlisted fields only, and priority set on it; +- account ids derived from the credential identifier, with a fake whose token values never appear in an id; - a fake that puts token-like values in every credential field, where no value reaches a response, log or receipt; - a fake key list with full values, where only ids come back, the sentinel is labelled and a foreign key @@ -110,9 +132,8 @@ management API across pinned proxy upgrades, and that call is Jason's (Q46). - no `packages/webui` module imports the model-proxy client or reads the `proxy` config. **Out of scope.** -- Upstream API keys such as z.ai. Mos's list for the page (mos-1606) doesn't name them, and entering one - means a secret crosses the Console. Sage has asked Mos. Until Mos rules, the page has no API-key entry. -- Removing or disabling an account, and every routing setting other than account priority. +- Removing or disabling an account. Mos is filing a Vikunja task for a later row (mos-1608). +- Every routing setting other than account priority. - Issuing or revoking client keys (P2), and any usage source (P5). - The upstream panel, any forward to the proxy port, and the subdomain fallback. - Traefik, DNS, Authentik settings, and the .54 Console's provider (Q218). @@ -126,4 +147,4 @@ bus and webui suites run whole, webui with `MOSAIC_REQUIRE_EDITOR_BUILD=1`. Ever whole. The named mutants below are killed. Every package suite but queue plus every `scripts/test-*.sh` is green on Sage's landing gate. BUILD.md has the upgrade-check output on the pinned version. Mutants: the accounts verb returns an unfiltered field; the callback verb skips the state check; a `/api/proxy/` route -accepts a granted user. +accepts a granted user; the API-key verb echoes the value in an error or a log. diff --git a/docs/plans/2026-10-24_stack-model-proxy.md b/docs/plans/2026-10-24_stack-model-proxy.md index f45f5144f..5f2679e98 100644 --- a/docs/plans/2026-10-24_stack-model-proxy.md +++ b/docs/plans/2026-10-24_stack-model-proxy.md @@ -106,8 +106,9 @@ 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`, and account priority. Whether the page also takes upstream API keys (for example z.ai) is -open with Mos. They live in the +`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. @@ -116,7 +117,7 @@ proxy's own panel. Jason's Q46 and Q47 answers replace it with the Console's own 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 8). +script against the new version (P4 brief item 9). ## 3. Priority routing, and how the business routing table maps onto it @@ -323,7 +324,8 @@ accounts (J7). The 8317 host proxy and its clients are not part of any step. - **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. API keys such as z.ai wait on Mos's ruling. + 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.