docs: row 330 P4 brief and design D7/J4 for mos-1608

Mos approved the P4 brief with upstream API keys added: a human-only write-only verb, z.ai-only allowlist, apikey-invalid and provider-not-allowed refusals, a canary test and an echo mutant. Account ids hash the proxy's own credential identifier, never a token or key value. Account removal stays out of P4.
This commit is contained in:
jason.woltje committed 2026-10-24 15:54:18 -05:00
1 parent 696ea3a3af
commit 966ee31ea8
2 files changed
+48 -25

No files matched your search

+42 -21
View File
@@ -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.
+6 -4
View File
@@ -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.