diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 7a035fde7..bd5757351 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -5591,3 +5591,5 @@ claim. Receipts and limitations in agents/lena/work/queue-296/BUILD.md and run-r 2026-10-25 Poe — row330 / #1828 focused block: first account RED6pass/4expected; source-only fixes; eight new files88/88 green, no skips. Offline PIN8.0.42 acceptance then exited1 before initial-account checkpoint and reported nice16 despite19 wrapper. Stopped at first unexpected red, returned slot to Sage, no rerun or mutants. Root cause unresolved; no full-gate or landing claim. Evidence: agents/poe/work/queue-330/BUILD.md and green-*.txt / upgrade-8.0.42.txt. 2026-10-25 Poe — row330 focused retry per sage-4446: all8 files88/88 green again; uptime per step retained. After Store fixture correction, offline PIN8.0.42 acceptance reached broker-bound but refused proxy-down at initial-accounts (load5.98/15.98/16.44). Stopped, returned slot, no rerun or mutants. See agents/poe/work/queue-330/block2-* and BUILD.md. No full-gate or completion claim. + +2026-10-25 Poe — row330 sage-4469 block: approved fixed GET provider404 normalization after exact RED2fail/9pass; GREEN10whole files99/99, pinned8.0.42 offline acceptance passed (both priority read-backs), four named mutants killed/restored with all controls green. No retries, old assertions or timeout changes. Slot returned. Evidence agents/poe/work/queue-330/404-* and BUILD.md. P2 integration/full global gate/Q246 reviews still pending; no landing claim. diff --git a/agents/poe/work/queue-330/404-acceptance.end b/agents/poe/work/queue-330/404-acceptance.end new file mode 100644 index 000000000..1dea56a42 --- /dev/null +++ b/agents/poe/work/queue-330/404-acceptance.end @@ -0,0 +1,2 @@ +2026-10-25T07:47:01Z + 02:47:01 up 11 days, 18:06, 2 users, load average: 13.77, 14.23, 20.51 diff --git a/agents/poe/work/queue-330/404-acceptance.start b/agents/poe/work/queue-330/404-acceptance.start new file mode 100644 index 000000000..c9781fe10 --- /dev/null +++ b/agents/poe/work/queue-330/404-acceptance.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:54Z + 02:46:54 up 11 days, 18:06, 2 users, load average: 13.49, 14.18, 20.53 diff --git a/agents/poe/work/queue-330/404-acceptance.txt b/agents/poe/work/queue-330/404-acceptance.txt new file mode 100644 index 000000000..f5cc6fa70 --- /dev/null +++ b/agents/poe/work/queue-330/404-acceptance.txt @@ -0,0 +1,20 @@ +P4 offline upgrade version=8.0.42 nice=16 +P4 diagnostic {"stage":"binary-start","nodePriority":16,"childPriority":16,"lastClientErrorCode":null,"nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"management-ready","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"store-open","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"broker-bound","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"initial-accounts","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 empty-provider diagnostic {"httpStatus":404,"type":"object","isArray":true,"count":0} +proxy.accounts initial empty; GET /credentials + GET /config/api-keys/claude +proxy.login.start claude GET /oauth/auth-url; fields=url,state only +proxy.login.submit POST /oauth/callback; fake code never ok; GET /oauth/status=wait +proxy.login.cancel DELETE /oauth/session confirmed +proxy.login.start codex GET /oauth/auth-url; fields=url,state only +proxy.login.submit POST /oauth/callback; fake code never ok; GET /oauth/status=wait +proxy.login.cancel DELETE /oauth/session confirmed +proxy.apikey.add PUT /config/api-keys/claude; account fields=accountId,provider,displayName,status,priority; id from auth_index +proxy.priority.set API-key PUT /config/api-keys/claude; priority read-back=7 +proxy.keys GET /config/access/api-keys; fields=keyId,management,agent,issuedAt,state; sentinel labelled; values absent +proxy.priority.set OAuth PATCH /credentials/fields body={name,priority}; priority read-back=8 +PASS offline native-page bus contract and secret-free receipts +P4 diagnostic {"stage":"initial-accounts","nodePriority":16,"childPriority":null,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":null,"childExitCode":0,"childSignal":null} diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt new file mode 100644 index 000000000..62db1d5e1 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt @@ -0,0 +1,19 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/ICE0YV +✔ accounts have exactly the allowlisted public fields, id from credential identifier (28.859027ms) +✔ token-like data in every upstream field never reaches public accounts (5.070752ms) +✔ API key account is derived from proxy auth_index, never api-key, and can set priority (12.225188ms) +✔ OAuth priority uses top-level name/priority and confirms it through list (4.580885ms) +✔ priority-unconfirmed: accepted API-key write with unchanged read-back refuses (5.588673ms) +✔ priority-write-refused: OAuth PATCH status 201 (3.491938ms) +✔ priority-write-refused: OAuth PATCH status 400 (4.410853ms) +✔ priority-invalid: unknown account refuses before any write (2.983779ms) +✔ proxy-down: a closed management listener refuses once (1.996951ms) +✔ client keys return ids, sentinel/foreign labels and only scoped record metadata (3.672515ms) +ℹ tests 10 +ℹ suites 0 +ℹ pass 10 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 183.915712 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.end new file mode 100644 index 000000000..92e725787 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:24Z + 02:46:24 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.start new file mode 100644 index 000000000..92e725787 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:24Z + 02:46:24 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt new file mode 100644 index 000000000..ddc29dc77 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt @@ -0,0 +1,12 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/WlQ8bY +✔ fresh404 yields no accounts, then adds the first key once with no value leakage (32.08173ms) +✔ malformed200 null remains proxy-down, no retry or write (7.8998ms) +✔ malformed200 object remains proxy-down, no retry or write (3.878116ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 3 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 147.324337 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end new file mode 100644 index 000000000..900ed6954 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:23Z + 02:46:23 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start new file mode 100644 index 000000000..4be30884d --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:22Z + 02:46:22 up 11 days, 18:06, 2 users, load average: 13.76, 14.37, 20.83 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt new file mode 100644 index 000000000..3df048fbc --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt @@ -0,0 +1,26 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/zFmWGh +✔ proxy.login.start: provider-not-allowed (32.675653ms) +✔ proxy.apikey.add: provider-not-allowed (0.468026ms) +✔ apikey-invalid empty (0.345583ms) +✔ apikey-invalid too long (0.330348ms) +✔ apikey-invalid charset (0.281139ms) +✔ callback-url-invalid not URL (1.581908ms) +✔ callback-url-invalid oversize (0.382376ms) +✔ callback-url-invalid repeated state (0.344928ms) +✔ login-state-unknown callback never reaches proxy (0.456109ms) +✔ login state is scoped to the human and business (0.514738ms) +✔ accepted callback wait is pending; only a status ok completes and retires state (0.915716ms) +✔ expiry reports login-timeout once with no management call and retires state (0.306001ms) +✔ cancel calls DELETE and retires state (0.39506ms) +✔ upstream OAuth error body never crosses bus response (0.266204ms) +✔ priority-invalid {"accountId":"bad","priority":1} (0.216492ms) +✔ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1.5} (0.206751ms) +✔ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1001} (0.245944ms) +ℹ tests 17 +ℹ suites 0 +ℹ pass 17 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 107.671923 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.end new file mode 100644 index 000000000..92e725787 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:24Z + 02:46:24 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.start new file mode 100644 index 000000000..92e725787 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:24Z + 02:46:24 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt new file mode 100644 index 000000000..4edeb6baa --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt @@ -0,0 +1,50 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/WXpqyV +✔ proxy.accounts: owner calls page once with business/human scope (26.116796ms) +✔ proxy.accounts: tester human-required (20.297319ms) +✔ proxy.accounts: agent human-required (16.822485ms) +✔ proxy.accounts: user human-required (17.662186ms) +✔ proxy.accounts: not-configured (23.121015ms) +✔ proxy.login.start: owner calls page once with business/human scope (17.138825ms) +✔ proxy.login.start: tester human-required (17.581147ms) +✔ proxy.login.start: agent human-required (20.879742ms) +✔ proxy.login.start: user human-required (24.234711ms) +✔ proxy.login.start: not-configured (19.75879ms) +✔ proxy.login.submit: owner calls page once with business/human scope (22.157242ms) +✔ proxy.login.submit: tester human-required (25.380596ms) +✔ proxy.login.submit: agent human-required (21.217295ms) +✔ proxy.login.submit: user human-required (23.521967ms) +✔ proxy.login.submit: not-configured (24.585017ms) +✔ proxy.login.status: owner calls page once with business/human scope (22.913517ms) +✔ proxy.login.status: tester human-required (21.7594ms) +✔ proxy.login.status: agent human-required (28.198167ms) +✔ proxy.login.status: user human-required (18.344285ms) +✔ proxy.login.status: not-configured (20.030294ms) +✔ proxy.login.cancel: owner calls page once with business/human scope (16.420549ms) +✔ proxy.login.cancel: tester human-required (20.486195ms) +✔ proxy.login.cancel: agent human-required (17.871874ms) +✔ proxy.login.cancel: user human-required (20.801042ms) +✔ proxy.login.cancel: not-configured (24.049445ms) +✔ proxy.apikey.add: owner calls page once with business/human scope (20.743633ms) +✔ proxy.apikey.add: tester human-required (20.590224ms) +✔ proxy.apikey.add: agent human-required (20.621701ms) +✔ proxy.apikey.add: user human-required (23.304327ms) +✔ proxy.apikey.add: not-configured (26.176447ms) +✔ proxy.priority.set: owner calls page once with business/human scope (18.750335ms) +✔ proxy.priority.set: tester human-required (19.039252ms) +✔ proxy.priority.set: agent human-required (18.745218ms) +✔ proxy.priority.set: user human-required (17.214992ms) +✔ proxy.priority.set: not-configured (17.152828ms) +✔ proxy.keys: owner calls page once with business/human scope (18.060003ms) +✔ proxy.keys: tester human-required (17.424731ms) +✔ proxy.keys: agent human-required (14.943668ms) +✔ proxy.keys: user human-required (16.025103ms) +✔ proxy.keys: not-configured (16.373436ms) +✔ API-key canary never enters broker events or unknown error output (15.813479ms) +ℹ tests 41 +ℹ suites 0 +ℹ pass 41 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 994.095736 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.end new file mode 100644 index 000000000..92e725787 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:24Z + 02:46:24 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.start new file mode 100644 index 000000000..900ed6954 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:23Z + 02:46:23 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt new file mode 100644 index 000000000..bbb1abb69 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt @@ -0,0 +1,17 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/qBvaqm +✔ empty provider GET404 yields empty array, discards body and receipts actual404 once (23.337829ms) +✔ provider GET401 remains proxy-client-refused, no retry (5.03975ms) +✔ provider GET403 remains proxy-client-refused, no retry (2.874184ms) +✔ provider GET500 remains proxy-client-refused, no retry (2.839998ms) +✔ getCredentials404 remains proxy-client-refused, no retry (3.031846ms) +✔ getKeys404 remains proxy-client-refused, no retry (2.739596ms) +✔ provider PUT404 remains proxy-client-refused, never treated as empty GET (1.681292ms) +✔ provider GET200 array passes through, with receipt and no retry (1.893456ms) +ℹ tests 8 +ℹ suites 0 +ℹ pass 8 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 148.993332 diff --git a/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.end new file mode 100644 index 000000000..4be30884d --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:22Z + 02:46:22 up 11 days, 18:06, 2 users, load average: 13.76, 14.37, 20.83 diff --git a/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.start new file mode 100644 index 000000000..4be30884d --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:22Z + 02:46:22 up 11 days, 18:06, 2 users, load average: 13.76, 14.37, 20.83 diff --git a/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt new file mode 100644 index 000000000..7bcdbc28b --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt @@ -0,0 +1,12 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/NaZhtr +✔ OAuth management calls keep URL and state out of receipts (25.417454ms) +✔ upstream key goes only in request body and upstream refusal body is discarded (5.332331ms) +✔ redirected management calls are not followed or retried (2.562305ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 3 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 139.356091 diff --git a/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.end new file mode 100644 index 000000000..900ed6954 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:23Z + 02:46:23 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.start new file mode 100644 index 000000000..900ed6954 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:23Z + 02:46:23 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt new file mode 100644 index 000000000..081f010ec --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt @@ -0,0 +1,10 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/UVkG6Q +✔ Console production module graph cannot import the model-proxy client (93.862563ms) +ℹ tests 1 +ℹ suites 0 +ℹ pass 1 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 177.142216 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.end new file mode 100644 index 000000000..62d2f22bf --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:30Z + 02:46:30 up 11 days, 18:06, 2 users, load average: 12.40, 14.06, 20.66 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.start new file mode 100644 index 000000000..7e1c625bd --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:29Z + 02:46:29 up 11 days, 18:06, 2 users, load average: 12.40, 14.06, 20.66 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt new file mode 100644 index 000000000..4503349f1 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt @@ -0,0 +1,12 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/HkzUFL +✔ proxy browser clears write-only key immediately, sends once and renders data as text (923.252097ms) +✔ proxy browser OAuth opens safely, callback wait stays pending, status ok completes (2763.533016ms) +✔ proxy browser read-only disables account changes (662.846864ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 3 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 4447.991189 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.end new file mode 100644 index 000000000..7e1c625bd --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:29Z + 02:46:29 up 11 days, 18:06, 2 users, load average: 12.40, 14.06, 20.66 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.start new file mode 100644 index 000000000..f177835b0 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:25Z + 02:46:25 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt new file mode 100644 index 000000000..41321f87b --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt @@ -0,0 +1,11 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/MVzxL3 +✔ API-key canary reaches fake once and zero responses/logs/receipts/store files (49.859682ms) +✔ OAuth URLs, state and code stay out of broker records and management receipts (21.222227ms) +ℹ tests 2 +ℹ suites 0 +ℹ pass 2 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 266.31653 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.end new file mode 100644 index 000000000..f177835b0 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:25Z + 02:46:25 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.start new file mode 100644 index 000000000..f177835b0 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:25Z + 02:46:25 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt new file mode 100644 index 000000000..2abd863af --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt @@ -0,0 +1,20 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/pooz0I +✔ owner proxy routes each make exactly one allowlisted call (30.857128ms) +✔ proxy write refuses signed-out (3.336504ms) +✔ proxy write refuses loopback (2.704228ms) +✔ proxy write refuses cross-origin (1.698264ms) +✔ proxy write refuses missing Fetch Metadata (1.705732ms) +✔ all proxy writes are read-only without --send (4.69854ms) +✔ granted user never reaches owner page or any proxy route (7.577808ms) +✔ native page offers write-only key input and no panel script (2.379451ms) +✔ Console caller rejects every non-proxy verb (1.337229ms) +✔ tester socket refuses proxy page, reads and writes before any owner call (8.577124ms) +✔ malformed API-key JSON never appears in errors or logs (1.827938ms) +ℹ tests 11 +ℹ suites 0 +ℹ pass 11 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 255.783124 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.end b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.end new file mode 100644 index 000000000..f177835b0 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:25Z + 02:46:25 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.start b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.start new file mode 100644 index 000000000..92e725787 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:24Z + 02:46:24 up 11 days, 18:06, 2 users, load average: 13.22, 14.25, 20.76 diff --git a/agents/poe/work/queue-330/404-green-run-list.txt b/agents/poe/work/queue-330/404-green-run-list.txt new file mode 100644 index 000000000..d6199cb98 --- /dev/null +++ b/agents/poe/work/queue-330/404-green-run-list.txt @@ -0,0 +1,10 @@ +packages/model-proxy/tests/empty-provider.test.mjs +packages/bus/tests/proxy-page-empty-provider.test.mjs +packages/model-proxy/tests/page-client.test.mjs +packages/bus/tests/proxy-page.test.mjs +packages/bus/tests/proxy-page-state.test.mjs +packages/bus/tests/proxy-page-accounts.test.mjs +packages/webui/tests/proxy-page.test.mjs +packages/webui/tests/proxy-canary.test.mjs +packages/webui/tests/proxy-browser.test.mjs +packages/webui/tests/proxy-architecture.test.mjs diff --git a/agents/poe/work/queue-330/404-mutants.end b/agents/poe/work/queue-330/404-mutants.end new file mode 100644 index 000000000..e58fc4464 --- /dev/null +++ b/agents/poe/work/queue-330/404-mutants.end @@ -0,0 +1,2 @@ +2026-10-25T07:47:52Z + 02:47:52 up 11 days, 18:07, 2 users, load average: 9.43, 13.06, 19.79 diff --git a/agents/poe/work/queue-330/404-mutants.start b/agents/poe/work/queue-330/404-mutants.start new file mode 100644 index 000000000..e8396326b --- /dev/null +++ b/agents/poe/work/queue-330/404-mutants.start @@ -0,0 +1,2 @@ +2026-10-25T07:47:50Z + 02:47:50 up 11 days, 18:07, 2 users, load average: 9.43, 13.06, 19.79 diff --git a/agents/poe/work/queue-330/404-mutants.txt b/agents/poe/work/queue-330/404-mutants.txt new file mode 100644 index 000000000..a522664c1 --- /dev/null +++ b/agents/poe/work/queue-330/404-mutants.txt @@ -0,0 +1,8 @@ +{"mutant":"accounts-unfiltered-field","test":"packages/bus/tests/proxy-page-accounts.test.mjs","exit":1,"nice":19,"failures":2,"passes":8,"failingTests":["accounts have exactly the allowlisted public fields, id from credential identifier","token-like data in every upstream field never reaches public accounts"],"killed":true,"restored":true,"originalSha256":"14acdb4d3fdaabad508eec7378740b13c175d4bdd0c6e707a673138eff82365b"} +{"mutant":"accounts-unfiltered-field","positiveControl":true,"test":"packages/bus/tests/proxy-page-accounts.test.mjs","exit":0,"nice":19,"failures":0,"passes":10,"failingTests":[],"positive":true} +{"mutant":"callback-skips-state","test":"packages/bus/tests/proxy-page-state.test.mjs","exit":1,"nice":19,"failures":2,"passes":15,"failingTests":["login-state-unknown callback never reaches proxy","accepted callback wait is pending; only a status ok completes and retires state"],"killed":true,"restored":true,"originalSha256":"14acdb4d3fdaabad508eec7378740b13c175d4bdd0c6e707a673138eff82365b"} +{"mutant":"callback-skips-state","positiveControl":true,"test":"packages/bus/tests/proxy-page-state.test.mjs","exit":0,"nice":19,"failures":0,"passes":17,"failingTests":[],"positive":true} +{"mutant":"proxy-route-accepts-granted-user","test":"packages/webui/tests/proxy-page.test.mjs","exit":1,"nice":19,"failures":1,"passes":10,"failingTests":["granted user never reaches owner page or any proxy route"],"killed":true,"restored":true,"originalSha256":"7b7bee91604b7bd973027b177de83205db9aed366e694072fc6343b46465b5ba"} +{"mutant":"proxy-route-accepts-granted-user","positiveControl":true,"test":"packages/webui/tests/proxy-page.test.mjs","exit":0,"nice":19,"failures":0,"passes":11,"failingTests":[],"positive":true} +{"mutant":"apikey-logged","test":"packages/webui/tests/proxy-canary.test.mjs","exit":1,"nice":19,"failures":1,"passes":1,"failingTests":["API-key canary reaches fake once and zero responses/logs/receipts/store files"],"killed":true,"restored":true,"originalSha256":"14acdb4d3fdaabad508eec7378740b13c175d4bdd0c6e707a673138eff82365b"} +{"mutant":"apikey-logged","positiveControl":true,"test":"packages/webui/tests/proxy-canary.test.mjs","exit":0,"nice":19,"failures":0,"passes":2,"failingTests":[],"positive":true} diff --git a/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt b/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt new file mode 100644 index 000000000..01e676e16 --- /dev/null +++ b/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt @@ -0,0 +1,26 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/zwP2zD +✖ fresh404 yields no accounts, then adds the first key once with no value leakage (31.629717ms) +✔ malformed200 null remains proxy-down, no retry or write (10.121658ms) +✔ malformed200 object remains proxy-down, no retry or write (5.73007ms) +ℹ tests 3 +ℹ suites 0 +ℹ pass 2 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 218.324391 + +✖ failing tests: + +test at packages/bus/tests/proxy-page-empty-provider.test.mjs:7:1 +✖ fresh404 yields no accounts, then adds the first key once with no value leakage (31.629717ms) + Error [ProxyError]: proxy-down + at refuse (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/model-proxy/src/common.mjs:8:39) + at ProxyPage.request (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs:80:7) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-empty-provider.test.mjs:10:17) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + code: 'proxy-down' + } diff --git a/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end b/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end new file mode 100644 index 000000000..b5f5a0e66 --- /dev/null +++ b/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:06Z + 02:46:06 up 11 days, 18:05, 2 users, load average: 15.17, 14.64, 21.02 diff --git a/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start b/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start new file mode 100644 index 000000000..b5f5a0e66 --- /dev/null +++ b/agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:06Z + 02:46:06 up 11 days, 18:05, 2 users, load average: 15.17, 14.64, 21.02 diff --git a/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt b/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt new file mode 100644 index 000000000..3177f56d1 --- /dev/null +++ b/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt @@ -0,0 +1,35 @@ +nice=19 TMPDIR=/mnt/storage/scratch/tmp/gJcxnP +✖ empty provider GET404 yields empty array, discards body and receipts actual404 once (30.718928ms) +✔ provider GET401 remains proxy-client-refused, no retry (8.324236ms) +✔ provider GET403 remains proxy-client-refused, no retry (2.996504ms) +✔ provider GET500 remains proxy-client-refused, no retry (2.838583ms) +✔ getCredentials404 remains proxy-client-refused, no retry (3.923009ms) +✔ getKeys404 remains proxy-client-refused, no retry (4.502681ms) +✔ provider PUT404 remains proxy-client-refused, never treated as empty GET (2.271108ms) +✔ provider GET200 array passes through, with receipt and no retry (2.585158ms) +ℹ tests 8 +ℹ suites 0 +ℹ pass 7 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 192.563954 + +✖ failing tests: + +test at packages/model-proxy/tests/empty-provider.test.mjs:35:1 +✖ empty provider GET404 yields empty array, discards body and receipts actual404 once (30.718928ms) + Error [ProxyError]: proxy-client-refused + at ClientRequest. (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/model-proxy/src/client.mjs:30:49) + at ClientRequest.wrapper (node:events:639:12) + at ClientRequest.emit (node:events:514:20) + at HTTPParser.parserOnIncomingClient (node:_http_client:973:27) + at HTTPParser.parserOnHeadersComplete (node:_http_common:125:17) + at Socket.socketOnData (node:_http_client:808:22) + at Socket.emit (node:events:514:20) + at addChunk (node:internal/streams/readable:575:12) + at readableAddChunkPushByteMode (node:internal/streams/readable:526:3) + at Readable.push (node:internal/streams/readable:406:5) { + code: 'proxy-client-refused' + } diff --git a/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.end b/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.end new file mode 100644 index 000000000..b5f5a0e66 --- /dev/null +++ b/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.end @@ -0,0 +1,2 @@ +2026-10-25T07:46:06Z + 02:46:06 up 11 days, 18:05, 2 users, load average: 15.17, 14.64, 21.02 diff --git a/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.start b/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.start new file mode 100644 index 000000000..8d86afa5e --- /dev/null +++ b/agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.start @@ -0,0 +1,2 @@ +2026-10-25T07:46:05Z + 02:46:05 up 11 days, 18:05, 2 users, load average: 15.17, 14.64, 21.02 diff --git a/agents/poe/work/queue-330/BUILD.md b/agents/poe/work/queue-330/BUILD.md index 2b992bba1..617aa6521 100644 --- a/agents/poe/work/queue-330/BUILD.md +++ b/agents/poe/work/queue-330/BUILD.md @@ -523,3 +523,66 @@ Required later GREEN should include both new whole files and the8 focused whole files because the helper is shared; full gate and P2 key-join still wait their separate authorized stage. No Node syntax check, test, acceptance, mutant or proxy process ran during preparation. Slot remains with Magica331r4. + +## Test-first404 block — sage-4469 CLEAR + +2026-10-25T07:46Z–07:47Z. On prepared e7438743f, unchanged product RED was +exactly the two intended failures: model-proxy empty-provider8cases7pass/1fail +(proxy-client-refused rather than[]), bus empty-provider3cases2pass/1fail +(proxy-down on fresh accounts). Negative controls all passed; no skipped, +cancelled or timed-out case. Receipts404-red-*.txt plus per-file uptime.start/.end. + +Applied ONLY the approved proposed-empty-provider.patch: three lines in the +client's #page catch normalize404 only for the fixed GET provider-config path. +Actual404 receipt remains; error body was discarded by #request. No assertion, +timeout, scheduling, public schema, other product file or refusal code changed. + +GREEN: both new files and the existing8focused files whole, serialized,99/99 +passes,0skip/cancel. Fresh short TMPDIR per file; shell wrappers requested and +recorded nice19. No automatic retry. Receipts404-green-*.txt and each file's +UTC/uptime start/end. Idle scheduling is accepted under mos-1678/sage-4446. + +Pinned8.0.42 acceptance rc0,07:46:54Z–07:47:01Z. Load13.49/14.18/20.53 -> +13.77/14.23/20.51. Node and its fixture child observed nice16/policy5. Exact +safe output is copied below from404-acceptance.txt; private namespace/temp-root +cleanup completed with childexit0. No real provider credential or external +upstream request; the made-up OAuth codes never complete sign-in. + +P4 offline upgrade version=8.0.42 nice=16 +P4 diagnostic {"stage":"binary-start","nodePriority":16,"childPriority":16,"lastClientErrorCode":null,"nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"management-ready","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"store-open","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"broker-bound","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 diagnostic {"stage":"initial-accounts","nodePriority":16,"childPriority":16,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":5,"childExitCode":null,"childSignal":null} +P4 empty-provider diagnostic {"httpStatus":404,"type":"object","isArray":true,"count":0} +proxy.accounts initial empty; GET /credentials + GET /config/api-keys/claude +proxy.login.start claude GET /oauth/auth-url; fields=url,state only +proxy.login.submit POST /oauth/callback; fake code never ok; GET /oauth/status=wait +proxy.login.cancel DELETE /oauth/session confirmed +proxy.login.start codex GET /oauth/auth-url; fields=url,state only +proxy.login.submit POST /oauth/callback; fake code never ok; GET /oauth/status=wait +proxy.login.cancel DELETE /oauth/session confirmed +proxy.apikey.add PUT /config/api-keys/claude; account fields=accountId,provider,displayName,status,priority; id from auth_index +proxy.priority.set API-key PUT /config/api-keys/claude; priority read-back=7 +proxy.keys GET /config/access/api-keys; fields=keyId,management,agent,issuedAt,state; sentinel labelled; values absent +proxy.priority.set OAuth PATCH /credentials/fields body={name,priority}; priority read-back=8 +PASS offline native-page bus contract and secret-free receipts +P4 diagnostic {"stage":"initial-accounts","nodePriority":16,"childPriority":null,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":null,"childExitCode":0,"childSignal":null} + +Named mutants rc0 overall,07:47:50Z–07:47:52Z, load9.43/13.06/19.79 at both +endpoints. Each mutated whole file ran once, was restored byte-for-byte, then +its restored positive control ran once before the next mutant: +- accounts-unfiltered-field:2fail, restored control10pass; +- callback-skips-state:2fail, restored control17pass; +- proxy-route-accepts-granted-user:1fail, restored control11pass; +- apikey-logged:1fail, restored control2pass. +Runner records nice19 launch observations and original source SHA256s. All4 +killed, all4 restored, all4 controls green. No raw mutant output/key values +were written; canary captures process output. Receipt404-mutants.txt/.start/.end. + +SLOT BACK reported to Sage as poe-330-0030. No unexpected result in this block, +no retry, no old assertion or timeout edits, no protected action. This is scoped +99-test/upgrade/mutant evidence, NOT the full package/global landing gate. P2 +landing/rebase/key-record join and Q246 independent review remain outstanding. +Final product/test diff and manifest prepared for independent review; no landing +or refactor push authorized. No further test/process run after returning slot. diff --git a/agents/poe/work/queue-330/REVIEW.md b/agents/poe/work/queue-330/REVIEW.md new file mode 100644 index 000000000..360a68899 --- /dev/null +++ b/agents/poe/work/queue-330/REVIEW.md @@ -0,0 +1,49 @@ +# Row330 / P4 review packet — #1828 + +Owner Poe; first independent Codex review Gladstone; landing review Filbert. +Brief docs/plans/2026-10-24_proxy-page-brief.md (heading17, blob4c005dc4936c). +Original base78b69e1e6. Candidate branch poe/row330. No landing authority. + +## Review scope and evidence + +review-product-test.diff is the complete changed packages/ source/test/docs diff +from78b69e1e6. review-404-normalization.diff is the exact latest three-line product +fix against prepared e7438743f; its tests/helper were committed first there. +review-MANIFEST.sha256 pins every changed package file and the current offline +acceptance/mutant scripts, diff artifacts, run list and successful scoped receipts. +BUILD.md preserves all earlier failures and corrections append-only. + +Current scoped verification: exact intended RED2fail/9pass, then99/99 focused +GREEN, pinned8.0.42 offline bus acceptance, four named mutants killed with four +restored positive controls (10/17/11/2passes). Whole-file receipts and uptime +are404-*.txt/.start/.end. Actual idle scheduling is reported, not overwritten. +No test reruns after an unexpected outcome. Earlier fixture-root and missing +provider404 findings are separate commits; product normalization is exactly +GET /v8/management/config/api-keys/claude HTTP404 ->[], with original404 receipt. +Every other endpoint/method/status still refuses, malformed200 null/object still +refuses proxy-down; no new refusal code, assertion weakening or timeout change. + +## Security focus + +- Owner human-only broker gate before generic user/tester handling; all8verbs. +- Native /proxy and /api/proxy routes through fixed Console caller allowlist; + writes require signed-in owner, named host, --send, own Origin and Fetch + Metadata. Granted/tester/loopback callers refuse. No webui management client. +- API-key input is write-only, promptly cleared, no browser storage; management + header only numeric loopback. OAuth pending-state ownership/expiry/finish + guards, no wait-to-success shortcut. Unknown errors are sanitized. +- Accounts project exactly5public fields. Stable generated display names avoid + arbitrary upstream credential strings; ids derive from proxy identifiers, + never token/key values. Client keys return ids and management labels only. +- Canary checks HTTP responses, structured logs/receipts, captured process output + and bus files; fake receives first key once. No real host proxy/credential access. + +## Explicit outstanding work / review limits + +P2 has not yet been rebased/joined into this branch. Runtime key-status projection +is provisional until the actual P2 metadata contract lands. Broad package suites +(model-proxy/bus/webui, webui requiring editor build), collateral import-derived +files and Sage's global landing gate still need authorized execution after that +integration. The prior broader inventory is not a current post-P2 manifest. +Independent Q246 first/landing verdicts are not claimed. Do not treat this packet +or the scoped checks as approval to land, push refactor, deploy or cut over. diff --git a/agents/poe/work/queue-330/review-404-normalization.diff b/agents/poe/work/queue-330/review-404-normalization.diff new file mode 100644 index 000000000..313f65793 --- /dev/null +++ b/agents/poe/work/queue-330/review-404-normalization.diff @@ -0,0 +1,14 @@ +diff --git a/packages/model-proxy/src/client.mjs b/packages/model-proxy/src/client.mjs +index 7440bc3c7..c7d72f234 100644 +--- a/packages/model-proxy/src/client.mjs ++++ b/packages/model-proxy/src/client.mjs +@@ -70,6 +70,9 @@ export class ManagementClient { + const answer = await this.#request(method, endpoint, payload, value => { status = value; }); + if (expectedStatus !== null && status !== expectedStatus) refuse('proxy-client-refused'); + return answer; ++ } catch (error) { ++ if (method === 'GET' && endpoint === '/v8/management/config/api-keys/claude' && status === 404) return []; ++ throw error; + } + finally { this.#receipt({ call: `${method} ${endpoint.split('?')[0]}`, status, keyIds: [] }); } + } diff --git a/agents/poe/work/queue-330/review-MANIFEST.sha256 b/agents/poe/work/queue-330/review-MANIFEST.sha256 new file mode 100644 index 000000000..778493e17 --- /dev/null +++ b/agents/poe/work/queue-330/review-MANIFEST.sha256 @@ -0,0 +1,74 @@ +72d5b9c800ab62f0daad197730bdb483f86d8d30b7b229a4c5060e1c726e0736 agents/poe/work/queue-330/404-acceptance.end +93869f4c4ba709250785294c766ba35c5e566611979b5f1c1168eb1ac38b2187 agents/poe/work/queue-330/404-acceptance.start +37ee6bf7ff69f1eb2043237ee4d6901d0fa511081fbb43ce4aebea146f1d6b56 agents/poe/work/queue-330/404-acceptance.txt +cccd9c46429681a225a717b330626fdcb3fa4b4c2c316c848dc3ddd6dacdbbc6 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt +9424865d186040e600adea91450379376c9affac71b6536f5738cf40152cca69 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.end +9424865d186040e600adea91450379376c9affac71b6536f5738cf40152cca69 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-accounts.test.mjs.txt.start +21cf4b7b5a66151595ecadb079a4a8456a10fa6c7bd1d3bc5c4f7ddf1f9f57a9 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt +1b7488b852dbd23785233f9feeb33e40bd60cf5613bbc501ca6e87dd93ef3884 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end +7c47a8b1c9ab3a67a1ee8f0efacfb1123ec4a93aa37b7838d5c68781c8a31138 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start +dfe6cf20ace7c20c62080617ce9599c8918ecb63867ffc3f02cff111c873bd56 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt +9424865d186040e600adea91450379376c9affac71b6536f5738cf40152cca69 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.end +9424865d186040e600adea91450379376c9affac71b6536f5738cf40152cca69 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page-state.test.mjs.txt.start +53e4f011864d90b97a53d76f5c2408661bc6a8ecd6cf02eafbefe5a2c853e348 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt +9424865d186040e600adea91450379376c9affac71b6536f5738cf40152cca69 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.end +1b7488b852dbd23785233f9feeb33e40bd60cf5613bbc501ca6e87dd93ef3884 agents/poe/work/queue-330/404-green-packages-bus-tests-proxy-page.test.mjs.txt.start +0ccd15e17c34462dafbd66ef356d1db30bc7ac4b835822be8395fde34721126f agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt +7c47a8b1c9ab3a67a1ee8f0efacfb1123ec4a93aa37b7838d5c68781c8a31138 agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.end +7c47a8b1c9ab3a67a1ee8f0efacfb1123ec4a93aa37b7838d5c68781c8a31138 agents/poe/work/queue-330/404-green-packages-model-proxy-tests-empty-provider.test.mjs.txt.start +14e6b2f1bc3970b52286ab170d37797f69dc9a3c02a1f231d63e5da8fda669c0 agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt +1b7488b852dbd23785233f9feeb33e40bd60cf5613bbc501ca6e87dd93ef3884 agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.end +1b7488b852dbd23785233f9feeb33e40bd60cf5613bbc501ca6e87dd93ef3884 agents/poe/work/queue-330/404-green-packages-model-proxy-tests-page-client.test.mjs.txt.start +9a85972389ed133138a47e956ceb982f8ec15a9b7138474513e358b241150866 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt +2f565840aadf7b1303e5b35902d4674d913adae865bbd91c038aa4a0fca7656c agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.end +f47cf83581a4d837d58aaa34ace54d8d2e4fd53e218250167994f37be39ffaa2 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-architecture.test.mjs.txt.start +e3e45bae3306be1fdb166950163bcf1ffad0f65bc641a39eb4b94a0b146dcf3f agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt +f47cf83581a4d837d58aaa34ace54d8d2e4fd53e218250167994f37be39ffaa2 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.end +61c9f069944fd925380cb181020a80fa249fa14f1875271dc99a8cddd52568b6 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-browser.test.mjs.txt.start +cb90be4c02955ca66c193a24b9c26e80b7aacb50adb8750a8be11b603154f1f4 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt +61c9f069944fd925380cb181020a80fa249fa14f1875271dc99a8cddd52568b6 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.end +61c9f069944fd925380cb181020a80fa249fa14f1875271dc99a8cddd52568b6 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-canary.test.mjs.txt.start +afcf804d388ff9cbc648812908b2ac44ca3eedb18796afa77e05a15b3e355f32 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt +61c9f069944fd925380cb181020a80fa249fa14f1875271dc99a8cddd52568b6 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.end +9424865d186040e600adea91450379376c9affac71b6536f5738cf40152cca69 agents/poe/work/queue-330/404-green-packages-webui-tests-proxy-page.test.mjs.txt.start +afcfb1f9624449cf6cd609ceb1f23ebd5a797fb45a71f7be9b2b41ac05b9ed98 agents/poe/work/queue-330/404-green-run-list.txt +576a8f1e6f9f58bca449ed432eeb9ee8c5d6b457d0178225f8330bdc1e077cc8 agents/poe/work/queue-330/404-mutants.end +ccfb1b4d9585dfc89ec7fd98111d7e97f08135d7c09fd52c6c0c6837184fe282 agents/poe/work/queue-330/404-mutants.start +1e3943a96d33275d83ff25e6ea8589c008b34d9141696f700fb55a05c053bd54 agents/poe/work/queue-330/404-mutants.txt +b2580eb45cb4fea2703d090c6c2e13fa7e368b025450506db06504ca9e705b57 agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt +89792291111ce2df8233a33007188fa8a7b1f83e6088e0f16f9b9d7521b7189d agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.end +89792291111ce2df8233a33007188fa8a7b1f83e6088e0f16f9b9d7521b7189d agents/poe/work/queue-330/404-red-packages-bus-tests-proxy-page-empty-provider.test.mjs.txt.start +6d773572f0ccb74c434eba04733552b6b534cff557398305e41e4da9f9d4802b agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt +89792291111ce2df8233a33007188fa8a7b1f83e6088e0f16f9b9d7521b7189d agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.end +51fa70c90e470656e97e2c0536ed9c5b412d929e48f6671cf9587f46464e5d4c agents/poe/work/queue-330/404-red-packages-model-proxy-tests-empty-provider.test.mjs.txt.start +722e211acc48aa319a378c01ccf3e0174aef958c5570ce69962896811888d8fb agents/poe/work/queue-330/REVIEW.md +e55807f718375b0914bcd6448e2a420611c25d37061fd9237986b1484acf1f30 agents/poe/work/queue-330/acceptance.mjs +8aa1007a19e70b6be1696896160e7ffdf4ba9afd9fb2d35a996f4a41515b8564 agents/poe/work/queue-330/acceptance.sh +ea7584f8935251baa9c32bf675343e8c8cbe19a45c4e7dc48675e14b4409bb1c agents/poe/work/queue-330/empty-provider-prepared-imports.json +335e8b1ec268cab5fe7fe644d635cf78e727b2f257eef5b3dc1bf89fb4f7aa5b agents/poe/work/queue-330/mutants.mjs +ebd56413e48604ad7e0d0ce135fcae851f529a020bd1b2dba50afffb2e275bb6 agents/poe/work/queue-330/review-404-normalization.diff +75bedb29f3e48c148e7a5047a6c2282f198db00cea02ad92e59a60f1e37cc656 agents/poe/work/queue-330/review-product-test.diff +ef197f4b52cba6e45fb6aa283a8432b408864bd3ebda14e3d21e7a9245ced62e packages/bus/src/broker.mjs +14acdb4d3fdaabad508eec7378740b13c175d4bdd0c6e707a673138eff82365b packages/bus/src/proxy-page.mjs +e759c5de92ff4ccb10bf9404f2f0532ea218ef0617cec7a039b9a15101a74642 packages/bus/src/runtime.mjs +6c3dce2fed1402ff30331deeaa7524fdaae2a3a16516538a4f74c917b20cdd77 packages/bus/tests/proxy-page-accounts.test.mjs +c55cc0bb47cd7716a2c31c665cd2dd426e2c54ab7b629637bdc12089dfeb40ce packages/bus/tests/proxy-page-empty-provider.test.mjs +01c18d8d09dac72bed45ce302a88cea449ed22ffde374529cf784bad38adbd72 packages/bus/tests/proxy-page-fixture.mjs +1f68f6a0847adaf4ac0ab35f17353c520fae1f29af36b8588b5f1d01097304f8 packages/bus/tests/proxy-page-state.test.mjs +4d57069c95339b8208d8f4595497352c386da9aea433b00873ab4f4e7b2986e6 packages/bus/tests/proxy-page.test.mjs +3315613da3d2153b7916090bf7ef6168c5fc7fac562d4a20e86c87a1f5b58df4 packages/model-proxy/README.md +8e4143c3adb5e00338dadc5db71f440ea846828c2a446e1c944a7ea15f2c24f2 packages/model-proxy/src/client.mjs +ed5dfcda6db77921f90f1eb587fb0044d6472fb613ecaa98445ed42bc04d5d46 packages/model-proxy/tests/empty-provider.test.mjs +f24e1d121a340cbcaa3f6cd6390eeb7d4b05f778dd498d76ba099cba5a719dea packages/model-proxy/tests/page-client.test.mjs +c87158fa73697df244d615c7145d46d06aa4f83c8fb38489008f7e6ac66ecac5 packages/webui/README.md +76c4e0a2f964de91483fee3671140ffaa52c35e91d00fd775db047db61718a27 packages/webui/src/cli.mjs +6a67c23114fe1d6fed7d496aec46a096f9c0279ce7d02b976e99cce0f03de8ec packages/webui/src/console-bus.mjs +a09028cd5ca57367f597d0681e84926ca871fd4626769e9947927d949c2b6ab7 packages/webui/src/proxy-http.mjs +34ea24817c8288f7bbb1e7eb2a73290cc0f022594ac0872d5ce4710d55b2d61c packages/webui/src/public/index.html +028fdb21b5f6bc7f3a81f53e950d3c867280cec08be3d17a8efc9b0f17d70a0e packages/webui/src/public/proxy.html +c329b1ea690e656d89fa0e2b9ad853e18a3209bc56cc47ba778c7bcd56897796 packages/webui/src/public/proxy.js +7b7bee91604b7bd973027b177de83205db9aed366e694072fc6343b46465b5ba packages/webui/src/serve.mjs +037e4df1ed928570f49d4862d13bd6fbf74169b683a382d789744099104ee71c packages/webui/tests/proxy-architecture.test.mjs +866e28ac5aaf73b54dd1a0bd3ad2f404449f94e03f2198b1e1af694c5e6de4b7 packages/webui/tests/proxy-browser.test.mjs +add80427295f0756f456e8d578c123b2fc0209104a297649eb44eb791ebd6e2f packages/webui/tests/proxy-canary.test.mjs +efa01078110be6021e14a2eda09e549a4e17d4b62f15c4e1d609b5f4d1c7c1a2 packages/webui/tests/proxy-page.test.mjs diff --git a/agents/poe/work/queue-330/review-product-test.diff b/agents/poe/work/queue-330/review-product-test.diff new file mode 100644 index 000000000..f3695848b --- /dev/null +++ b/agents/poe/work/queue-330/review-product-test.diff @@ -0,0 +1,1507 @@ +diff --git a/packages/bus/src/broker.mjs b/packages/bus/src/broker.mjs +index 338a52dbc..d54829e20 100644 +--- a/packages/bus/src/broker.mjs ++++ b/packages/bus/src/broker.mjs +@@ -1,3 +1,4 @@ ++import { PROXY_PAGE_VERBS, PROXY_PAGE_CODES } from './proxy-page.mjs'; + import { randomUUID, randomBytes, createHash } from 'node:crypto'; + import { parseAddress } from './address.mjs'; + import { isAbsolute, dirname } from 'node:path'; +@@ -419,6 +420,7 @@ export class Broker { + #organization; + #credentialVerbs; + #proxyInit; ++ #proxyPage; + #project; + #library; + // C2-06 (design 5.2, B7): cap -> the session's kept web.fetch pages, in a library business only. +@@ -477,7 +479,7 @@ export class Broker { + #clock = 0; + #processing; + // credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use(). +- constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, library = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, proxyInit = null, hostGrant = null, hostKeys = null, processing = null, testerOn = null, grantScope = null, roleFile = null, giteaGrant = null, giteaKeys = null, access = null, accessStamp = null, compactions = null }) { ++ constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, library = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, proxyInit = null, proxyPage = null, hostGrant = null, hostKeys = null, processing = null, testerOn = null, grantScope = null, roleFile = null, giteaGrant = null, giteaKeys = null, access = null, accessStamp = null, compactions = null }) { + if (!object(businesses) || !Object.keys(businesses).length) fail('invalid-business'); + // Row 194 b15 (sage-2501): the function-argument checks come before anything reads the store. + if (harnessOn !== null && typeof harnessOn !== 'function') throw new TypeError('harnessOn must be a function'); +@@ -502,6 +504,7 @@ export class Broker { + // Row 199: the runtime's credential lifecycle (credential-verbs.mjs); it holds the store, not the broker. + this.#credentialVerbs = credentialVerbs; + this.#proxyInit = proxyInit; ++ this.#proxyPage = proxyPage; + this.#hostGrant = hostGrant; + this.#hostKeys = hostKeys; + this.#giteaGrant = giteaGrant; +@@ -4803,6 +4806,17 @@ export class Broker { + try { + keys(request, ['verb', 'args'], ['verb']); + string(request.verb, 64); ++ // P4: this boundary precedes the generic user/tester gates so every ++ // nonhuman caller gets human-required. Never secret-check, clone or trail ++ // a write-only API key or a callback URL as request content. ++ if (PROXY_PAGE_VERBS.includes(request.verb)) { ++ this.#human(s); ++ if (!this.#proxyPage) fail('not-configured'); ++ const args = request.args ?? {}; ++ if (!object(args)) fail('invalid-request'); ++ return Promise.resolve().then(() => this.#proxyPage(request.verb, args, { business: s.business, human: s.human })) ++ .catch(error => { throw this.#refused(s, new BusError(PROXY_PAGE_CODES.includes(error.code) ? error.code : 'proxy-down'), { verb: request.verb }); }); ++ } + // FED R3 (design 3.5, 3.7 trigger 1): a user reaches the collaborator list only, and each + // request reads the access file again, so a removed or expired grant refuses the next one. + let grants = null; +diff --git a/packages/bus/src/proxy-page.mjs b/packages/bus/src/proxy-page.mjs +new file mode 100644 +index 000000000..04ff26a3c +--- /dev/null ++++ b/packages/bus/src/proxy-page.mjs +@@ -0,0 +1,179 @@ ++// P4: bus-only native Console operations. No upstream credential is persisted here. ++import { randomBytes } from 'node:crypto'; ++import { sha256, refuse } from '../../model-proxy/src/common.mjs'; ++export const PROXY_PAGE_VERBS = Object.freeze(['proxy.accounts', 'proxy.login.start', 'proxy.login.submit', ++ 'proxy.login.status', 'proxy.login.cancel', 'proxy.apikey.add', 'proxy.priority.set', 'proxy.keys']); ++export const PROXY_PAGE_CODES = Object.freeze(['not-configured', 'human-required', 'provider-not-allowed', ++ 'login-state-unknown', 'callback-url-invalid', 'apikey-invalid', 'priority-invalid', 'priority-unconfirmed', ++ 'priority-write-refused', 'proxy-down', 'invalid-request']); ++const object = x => x !== null && typeof x === 'object' && !Array.isArray(x); ++function fields(args, names) { ++ if (!object(args) || Object.keys(args).length !== names.length || names.some(k => !Object.hasOwn(args, k))) refuse('invalid-request'); ++} ++const priorityOk = n => Number.isInteger(n) && n >= -1000 && n <= 1000; ++const identifier = x => typeof x === 'string' && x.length > 0 && x.length <= 512; ++const stateOk = x => typeof x === 'string' && /^[a-zA-Z0-9_-]{1,512}$/.test(x); ++const keyId = value => sha256(value).slice(0, 12); ++const ZAI_URL = 'https://api.z.ai/api/anthropic'; ++const providerName = provider => ({ claude: 'Claude', codex: 'Codex', 'z.ai': 'z.ai', other: 'Other' })[provider]; ++// Credential labels/emails and group names are arbitrary upstream strings. A stable ++// generated display name provides identification without reflecting a misplaced token. ++function view(id, provider, status, priority) { ++ return { accountId: keyId(id), provider, displayName: `${providerName(provider)} account ${keyId(id)}`, ++ status, priority: priorityOk(priority) ? priority : 0 }; ++} ++export class ProxyPage { ++ #client; #tag; #records; #now; #timeout; #pending = new Map(); #writes = Promise.resolve(); ++ constructor({ client, instanceTag, keyRecords = () => [], now = Date.now, loginTimeoutSeconds = 300 }) { ++ if (!client || !/^[a-zA-Z0-9_-]{1,64}$/.test(instanceTag) || typeof keyRecords !== 'function' || typeof now !== 'function' ++ || !Number.isSafeInteger(loginTimeoutSeconds) || loginTimeoutSeconds <= 0) throw new TypeError('invalid proxy page settings'); ++ this.#client = client; this.#tag = instanceTag; this.#records = keyRecords; this.#now = now; this.#timeout = loginTimeoutSeconds * 1000; ++ } ++ // Serializes read-modify-write provider config within the bus. P2 owns the ++ // independent client-key list. Store's lock admits only one bus per data root. ++ #write(operation) { ++ const next = this.#writes.then(operation); this.#writes = next.catch(() => {}); return next; ++ } ++ async #accounts() { ++ const response = await this.#client.getCredentials(); ++ const groups = await this.#client.getUpstreamGroups(); ++ if (!Array.isArray(response?.files) || !Array.isArray(groups)) refuse('proxy-down'); ++ const rows = []; ++ for (const row of response.files) { ++ if (!identifier(row?.id) || !identifier(row?.name)) continue; ++ const provider = ['claude', 'codex'].includes(row.provider) ? row.provider : 'other'; ++ const status = row.disabled === true ? 'disabled' : row.unavailable === true ? 'unavailable' ++ : ['ready', 'active', 'error', 'disabled', 'unavailable'].includes(row.status) ? row.status : 'unknown'; ++ rows.push({ ...view(row.id, provider, status, row.priority), source: 'oauth', name: row.name }); ++ } ++ for (const [groupIndex, group] of groups.entries()) { ++ if (!Array.isArray(group?.keys)) continue; ++ for (const [keyIndex, key] of group.keys.entries()) { ++ // auth_index is generated by the proxy. Never fall back to hashing api-key. ++ if (!identifier(key?.auth_index)) continue; ++ const provider = group['base-url'] === ZAI_URL ? 'z.ai' : 'other'; ++ const status = key.disabled === true || group.disabled === true ? 'disabled' : 'active'; ++ rows.push({ ...view(key.auth_index, provider, status, key.priority ?? group.priority), source: 'apikey', groupIndex, keyIndex }); ++ } ++ } ++ return { rows, groups }; ++ } ++ #public(row) { ++ const { accountId, provider, displayName, status, priority } = row; ++ return { accountId, provider, displayName, status, priority }; ++ } ++ #session(state, scope, { reportTimeout = false } = {}) { ++ const entry = stateOk(state) ? this.#pending.get(state) : null; ++ if (!entry || entry.business !== scope.business || entry.human !== scope.human) refuse('login-state-unknown'); ++ if (entry.expires <= this.#now()) { ++ this.#pending.delete(state); ++ if (reportTimeout) return null; ++ refuse('login-state-unknown'); ++ } ++ return entry; ++ } ++ async request(verb, args, scope) { ++ try { return await this.#request(verb, args, scope); } ++ catch (error) { ++ // A client or fixture may put request values in its errors. Only locally ++ // enumerated refusal codes cross the broker, never a raw exception/body. ++ refuse(PROXY_PAGE_CODES.includes(error.code) ? error.code : 'proxy-down'); ++ } ++ } ++ async #request(verb, args, scope) { ++ if (verb === 'proxy.accounts') { ++ fields(args, []); ++ return { accounts: (await this.#accounts()).rows.map(row => this.#public(row)) }; ++ } ++ if (verb === 'proxy.login.start') { ++ fields(args, ['provider']); ++ if (!['claude', 'codex'].includes(args.provider)) refuse('provider-not-allowed'); ++ for (const [state, value] of this.#pending) if (value.expires <= this.#now()) this.#pending.delete(state); ++ if (this.#pending.size >= 128) refuse('proxy-down'); ++ const answer = await this.#client.oauthStart(args.provider); ++ let url; try { url = new URL(answer.url); } catch { refuse('proxy-down'); } ++ const host = args.provider === 'claude' ? 'claude.ai' : 'auth.openai.com'; ++ if (url.protocol !== 'https:' || url.hostname !== host || url.username || url.password || url.port ++ || url.href.length > 8192 || !stateOk(answer.state) || url.searchParams.getAll('state').length !== 1 ++ || url.searchParams.get('state') !== answer.state || this.#pending.has(answer.state)) refuse('proxy-down'); ++ this.#pending.set(answer.state, { business: scope.business, human: scope.human, expires: this.#now() + this.#timeout }); ++ return { url: url.href, state: answer.state }; ++ } ++ if (verb === 'proxy.login.submit') { ++ fields(args, ['redirect_url']); ++ let url; ++ try { ++ if (typeof args.redirect_url !== 'string' || args.redirect_url.length > 8192) throw Error(); ++ url = new URL(args.redirect_url); ++ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash || url.searchParams.getAll('state').length !== 1) throw Error(); ++ } catch { refuse('callback-url-invalid'); } ++ const state = url.searchParams.get('state'); this.#session(state, scope); ++ await this.#client.oauthCallback(args.redirect_url); ++ // Callback acceptance is explicitly not proof of credential persistence. ++ return { status: 'wait' }; ++ } ++ if (verb === 'proxy.login.status') { ++ fields(args, ['state']); ++ if (!this.#session(args.state, scope, { reportTimeout: true })) return { status: 'error', reason: 'login-timeout' }; ++ const answer = await this.#client.oauthStatus(args.state); ++ if (answer?.status === 'wait') return { status: 'wait' }; ++ if (!['ok', 'error'].includes(answer?.status)) refuse('proxy-down'); ++ this.#pending.delete(args.state); ++ return answer.status === 'ok' ? { status: 'ok' } : { status: 'error', reason: 'login-failed' }; ++ } ++ if (verb === 'proxy.login.cancel') { ++ fields(args, ['state']); this.#session(args.state, scope); ++ await this.#client.oauthCancel(args.state); this.#pending.delete(args.state); ++ return { status: 'cancelled' }; ++ } ++ if (verb === 'proxy.apikey.add') { ++ fields(args, ['provider', 'key']); ++ if (args.provider !== 'z.ai') refuse('provider-not-allowed'); ++ if (typeof args.key !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(args.key)) refuse('apikey-invalid'); ++ return this.#write(async () => { ++ const groups = await this.#client.getUpstreamGroups(); ++ if (!Array.isArray(groups)) refuse('proxy-down'); ++ const group = { name: `mosaic-zai-${randomBytes(6).toString('hex')}`, 'base-url': ZAI_URL, ++ priority: 0, keys: [{ 'api-key': args.key }] }; ++ await this.#client.putUpstreamGroups([...groups, group]); ++ return { added: true }; ++ }); ++ } ++ if (verb === 'proxy.priority.set') { ++ fields(args, ['accountId', 'priority']); ++ if (typeof args.accountId !== 'string' || !/^[a-f0-9]{12}$/.test(args.accountId) || !priorityOk(args.priority)) refuse('priority-invalid'); ++ return this.#write(async () => { ++ const { rows, groups } = await this.#accounts(); ++ const matches = rows.filter(row => row.accountId === args.accountId); ++ if (matches.length !== 1) refuse('priority-invalid'); ++ const row = matches[0]; ++ try { ++ if (row.source === 'oauth') await this.#client.patchCredential(row.name, args.priority); ++ else { ++ groups[row.groupIndex].keys[row.keyIndex].priority = args.priority; ++ await this.#client.putUpstreamGroups(groups); ++ } ++ } catch (error) { ++ refuse(error.code === 'proxy-client-refused' || error.code === 'proxy-client-redirect' ? 'priority-write-refused' : 'proxy-down'); ++ } ++ const after = (await this.#accounts()).rows.find(r => r.accountId === args.accountId); ++ if (!after || after.priority !== args.priority) refuse('priority-unconfirmed'); ++ return { accountId: row.accountId, priority: args.priority }; ++ }); ++ } ++ if (verb === 'proxy.keys') { ++ fields(args, []); ++ const values = await this.#client.getKeys(); ++ if (!Array.isArray(values) || values.some(k => typeof k !== 'string')) refuse('proxy-down'); ++ const records = await this.#records(scope.business); ++ const prefix = `msk-${this.#tag}-`; ++ return { keys: values.map(value => { ++ const id = keyId(value), sentinel = value.startsWith(prefix + 'sentinel-'); ++ const record = records.find(r => r.keyId === id); ++ return { keyId: id, management: sentinel ? 'sentinel' : value.startsWith(prefix) ? 'stack-managed' : 'not stack-managed', ++ agent: record?.agent ?? null, issuedAt: record?.issuedAt ?? null, state: record?.state ?? null }; ++ }) }; ++ } ++ refuse('invalid-request'); ++ } ++} +diff --git a/packages/bus/src/runtime.mjs b/packages/bus/src/runtime.mjs +index 342314b94..2cbd65a35 100644 +--- a/packages/bus/src/runtime.mjs ++++ b/packages/bus/src/runtime.mjs +@@ -14,6 +14,8 @@ import { publishRemote, workingRepo, WorkingError } from '../../seat/src/working + import { createLoader, hasModule } from '../../plugins/src/loader.mjs'; + import { pluginSettings } from '../../business/src/plugin-settings.mjs'; + import { loadManagementClient } from '../../model-proxy/src/client.mjs'; ++import { ProxyPage } from './proxy-page.mjs'; ++import { sha256 } from '../../model-proxy/src/common.mjs'; + import { initializeProxy } from '../../model-proxy/src/settings.mjs'; + import { instanceSettings } from '../../business/src/instance-settings.mjs'; + import { organizationWriter, OrganizationError, setReplyLimits } from '../../business/src/organization.mjs'; +@@ -158,7 +160,7 @@ export async function startBroker({ + || (Object.hasOwn(librarySync, 'intervalMs') && !(Number.isSafeInteger(librarySync.intervalMs) && librarySync.intervalMs > 0)) + || (Object.hasOwn(librarySync, 'configDir') && typeof librarySync.configDir !== 'string'))) + throw new BusError('invalid-host-request'); +- let modelProxyClient = null; ++ let modelProxyClient = null, proxyPage = null; + let store, credentials, server, adapter, processing, syncTimer = null, syncPass = null, accessTimer = null; + try { + const proxyReceipt = entry => process.stderr.write(`model-proxy: ${JSON.stringify(entry)}\n`); +@@ -199,6 +201,16 @@ export async function startBroker({ + // gitea, and `held` names the files memory must not load until reconcile or a revoke. + const { held } = storeRoot ? giteaStart(store, storeRoot, businesses, references) : { held: [] }; + credentials = new Credentials({ references, repoRoots: protectedRoots, dataRoot, storeRoot, held }); ++ const configureProxyPage = () => { ++ proxyPage = new ProxyPage({ client: modelProxyClient, instanceTag: sha256(organization.dir).slice(0, 12), ++ // P2 supplies the model metadata; only the matching business's record ++ // participates in this owner view. No credential value is read here. ++ keyRecords: business => credentials.status().filter(r => r.service === 'model' && r.instance?.startsWith(business + '/')) ++ .map(r => ({ keyId: r.keyId, agent: r.instance.slice(business.length + 1), issuedAt: r.issuedAt, state: r.state })), ++ }); ++ }; ++ if (modelProxyClient) configureProxyPage(); ++ + // vikunja:19/305: the plug-in switch through the same human-only path as the limits: the verbs + // plugins and plugins.set, logged in changes.jsonl with the actor, a missing module refused. + const plugins=organization?pluginSettings({dir:organization.dir,rolesDir:organization.rolesDir,exists:hasModule}):null; +@@ -420,9 +432,14 @@ export async function startBroker({ + credentialStatus: () => credentials.status(), + // Row 199 (#1697, design section 6): issue, rotate, revoke and delete, live with no restart; each + // rewrites or records through the organization writer, so changes.jsonl holds the receipt. ++ proxyPage: async (verb, args, scope) => { ++ if (!proxyPage) throw new BusError('not-configured'); ++ return proxyPage.request(verb, args, scope); ++ }, + proxyInit: organization ? async args => { + const settings = await initializeProxy({ configDir: organization.dir, dataRoot, ...args }); + modelProxyClient = await loadManagementClient({ configDir: organization.dir, proxy: settings, receipt: proxyReceipt }); ++ configureProxyPage(); + return settings; + } : null, + credentialVerbs: organization +diff --git a/packages/bus/tests/proxy-page-accounts.test.mjs b/packages/bus/tests/proxy-page-accounts.test.mjs +new file mode 100644 +index 000000000..c30052d0a +--- /dev/null ++++ b/packages/bus/tests/proxy-page-accounts.test.mjs +@@ -0,0 +1,81 @@ ++// P4 named mutant: returning any unfiltered credential field. Every projection ++// and write hits the real management client against a local stand-in server. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import { createHash } from 'node:crypto'; ++import { fakeManagement } from './proxy-page-fixture.mjs'; ++const id = value => createHash('sha256').update(value).digest('hex').slice(0, 12); ++const oauth = extra => ({ id: 'oauth-one.json', name: 'oauth-one.json', provider: 'claude', label: 'p4-private-label', ++ email: 'p4-private-email', token: 'p4-private-token', status: 'ready', priority: 3, ...extra }); ++test('accounts have exactly the allowlisted public fields, id from credential identifier', async t => { ++ const s = await fakeManagement(t, { files: [oauth()] }); ++ const result = await s.call('proxy.accounts'); ++ assert.deepEqual(result, { accounts: [{ accountId: id('oauth-one.json'), provider: 'claude', ++ displayName: `Claude account ${id('oauth-one.json')}`, status: 'ready', priority: 3 }] }); ++ for (const value of ['p4-private-label', 'p4-private-email', 'p4-private-token']) { ++ assert.equal(JSON.stringify(result).includes(value), false); ++ assert.notEqual(result.accounts[0].accountId, id(value)); ++ assert.equal(JSON.stringify(s.state.receipts).includes(value), false); ++ } ++}); ++test('token-like data in every upstream field never reaches public accounts', async t => { ++ const toxic = 'p4-private-token'; ++ const s = await fakeManagement(t, { files: [oauth({ label: toxic, email: toxic, token: toxic, status: toxic, ++ provider: toxic, account: toxic, account_type: toxic, auth_index: toxic, path: toxic, size: toxic, priority: toxic })] }); ++ const result = await s.call('proxy.accounts'); ++ assert.equal(JSON.stringify(result).includes(toxic), false); ++ assert.equal(JSON.stringify(s.state.receipts).includes(toxic), false); ++ assert.deepEqual(Object.keys(result.accounts[0]).sort(), ['accountId', 'displayName', 'priority', 'provider', 'status']); ++}); ++test('API key account is derived from proxy auth_index, never api-key, and can set priority', async t => { ++ const s = await fakeManagement(t); ++ await s.call('proxy.apikey.add', { provider: 'z.ai', key: 'p4-canary-key' }); ++ const first = (await s.call('proxy.accounts')).accounts[0]; ++ assert.equal(first.accountId, id('upstream-1')); assert.notEqual(first.accountId, id('p4-canary-key')); ++ assert.equal(first.provider, 'z.ai'); assert.equal(first.priority, 0); ++ // Fake preserves derived identifiers across writes, just as the pinned proxy does. ++ // The next write is expected to strip derived auth_index before sending config. ++ await s.call('proxy.priority.set', { accountId: first.accountId, priority: 7 }); ++ assert.equal((await s.call('proxy.accounts')).accounts[0].priority, 7); ++ assert.equal(JSON.stringify(s.state.receipts).includes('p4-canary-key'), false); ++}); ++test('OAuth priority uses top-level name/priority and confirms it through list', async t => { ++ const s = await fakeManagement(t, { files: [oauth()] }); ++ assert.deepEqual(await s.call('proxy.priority.set', { accountId: id('oauth-one.json'), priority: 8 }), ++ { accountId: id('oauth-one.json'), priority: 8 }); ++ assert.equal(s.state.files[0].priority, 8); ++ assert.equal(s.state.calls.filter(c => c === 'PATCH /v8/management/credentials/fields').length, 1); ++}); ++test('priority-unconfirmed: accepted API-key write with unchanged read-back refuses', async t => { ++ const s = await fakeManagement(t, { priorityNoop: true, ++ groups: [{ name: 'z.ai', 'base-url': 'https://api.z.ai/api/anthropic', priority: 0, ++ keys: [{ 'api-key': 'p4-upstream', auth_index: 'upstream-1' }] }] }); ++ await assert.rejects(s.call('proxy.priority.set', { accountId: id('upstream-1'), priority: 8 }), { code: 'priority-unconfirmed' }); ++}); ++for (const status of [201, 400]) test(`priority-write-refused: OAuth PATCH status ${status}`, async t => { ++ const s = await fakeManagement(t, { files: [oauth()], patchStatus: status }); ++ await assert.rejects(s.call('proxy.priority.set', { accountId: id('oauth-one.json'), priority: 8 }), { code: 'priority-write-refused' }); ++ assert.equal(s.state.calls.filter(c => c.startsWith('PATCH ')).length, 1); ++}); ++test('priority-invalid: unknown account refuses before any write', async t => { ++ const s = await fakeManagement(t); ++ await assert.rejects(s.call('proxy.priority.set', { accountId: 'a'.repeat(12), priority: 8 }), { code: 'priority-invalid' }); ++ assert.equal(s.state.calls.some(c => c.startsWith('PUT ') || c.startsWith('PATCH ')), false); ++}); ++test('proxy-down: a closed management listener refuses once', async t => { ++ const s = await fakeManagement(t); ++ await new Promise(r => s.server.close(r)); ++ await assert.rejects(s.call('proxy.accounts'), { code: 'proxy-down' }); ++ assert.equal(s.state.receipts.length, 1); ++}); ++test('client keys return ids, sentinel/foreign labels and only scoped record metadata', async t => { ++ const s = await fakeManagement(t, { keys: ['msk-fixture-sentinel-x', 'msk-fixture-agent', 'foreign-key'], ++ records: [{ keyId: id('msk-fixture-agent'), agent: 'coder', issuedAt: '2026-10-24T00:00:00Z', state: 'valid' }] }); ++ const result = await s.call('proxy.keys'); ++ assert.deepEqual(result, { keys: [ ++ { keyId: id('msk-fixture-sentinel-x'), management: 'sentinel', agent: null, issuedAt: null, state: null }, ++ { keyId: id('msk-fixture-agent'), management: 'stack-managed', agent: 'coder', issuedAt: '2026-10-24T00:00:00Z', state: 'valid' }, ++ { keyId: id('foreign-key'), management: 'not stack-managed', agent: null, issuedAt: null, state: null }, ++ ] }); ++ for (const value of s.state.keys) assert.equal(JSON.stringify(result).includes(value), false); ++}); +diff --git a/packages/bus/tests/proxy-page-empty-provider.test.mjs b/packages/bus/tests/proxy-page-empty-provider.test.mjs +new file mode 100644 +index 000000000..b864d3d95 +--- /dev/null ++++ b/packages/bus/tests/proxy-page-empty-provider.test.mjs +@@ -0,0 +1,33 @@ ++// P4 real client/page regression: fresh pinned proxy404 must allow the first key. ++// Malformed successful bodies still fail closed with the existing refusal code. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import { fakeManagement } from './proxy-page-fixture.mjs'; ++const route = '/v8/management/config/api-keys/claude'; ++test('fresh404 yields no accounts, then adds the first key once with no value leakage', async t => { ++ const canary = 'p4-first-key'; ++ const s = await fakeManagement(t, { emptyProviderResponse: { status: 404, body: { error: 'p4-body-canary' } } }); ++ const empty = await s.call('proxy.accounts'); ++ assert.equal(Array.isArray(empty.accounts), true); assert.equal(empty.accounts.length, 0); ++ const added = await s.call('proxy.apikey.add', { provider: 'z.ai', key: canary }); ++ assert.equal(added.added, true); ++ const populated = await s.call('proxy.accounts'); ++ assert.equal(populated.accounts.length, 1); assert.equal(populated.accounts[0].provider, 'z.ai'); ++ assert.equal(s.state.receives, 1, 'one PUT, no retry'); ++ assert.equal(s.state.groups[0].keys[0]['api-key'] === canary, true); ++ assert.deepEqual(s.state.calls, ['GET /v8/management/credentials', `GET ${route}`, `GET ${route}`, `PUT ${route}`, ++ 'GET /v8/management/credentials', `GET ${route}`]); ++ assert.deepEqual(s.state.receipts.filter(r => r.call === `GET ${route}`).map(r => r.status), [404, 404, 200]); ++ for (const value of [canary, 'p4-body-canary', 'p4-management']) { ++ assert.equal(JSON.stringify([empty, added, populated]).includes(value), false); ++ assert.equal(JSON.stringify(s.state.receipts).includes(value), false); ++ } ++}); ++for (const [name, body] of [['null', null], ['object', { groups: [] }]]) ++ test(`malformed200 ${name} remains proxy-down, no retry or write`, async t => { ++ const s = await fakeManagement(t, { emptyProviderResponse: { status: 200, body } }); ++ await assert.rejects(s.call('proxy.accounts'), { code: 'proxy-down' }); ++ assert.deepEqual(s.state.calls, ['GET /v8/management/credentials', `GET ${route}`]); ++ assert.equal(s.state.receives, 0); ++ assert.deepEqual(s.state.receipts.map(r => r.status), [200, 200]); ++ }); +diff --git a/packages/bus/tests/proxy-page-fixture.mjs b/packages/bus/tests/proxy-page-fixture.mjs +new file mode 100644 +index 000000000..d20b3c775 +--- /dev/null ++++ b/packages/bus/tests/proxy-page-fixture.mjs +@@ -0,0 +1,49 @@ ++// Local management stand-in. All credentials made up, kept only in this test's ++// memory; logs collect endpoint names, never query strings, headers or bodies. ++import http from 'node:http'; ++import { ManagementClient } from '../../model-proxy/src/client.mjs'; ++import { ProxyPage } from '../src/proxy-page.mjs'; ++export async function fakeManagement(t, options = {}) { ++ const state = { files: [], groups: [], keys: ['msk-fixture-sentinel-x', 'foreign-key'], receives: 0, ++ calls: [], receipts: [], priorityNoop: false, patchStatus: 200, ...options }; ++ let nextId = 1; ++ const server = http.createServer(async (req, res) => { ++ const u = new URL(req.url, 'http://localhost'); ++ const chunks = []; for await (const c of req) chunks.push(c); ++ const value = chunks.length ? JSON.parse(Buffer.concat(chunks)) : null; ++ state.calls.push(req.method + ' ' + u.pathname); ++ const reply = (status, body) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); }; ++ if (req.headers.authorization !== 'Bearer p4-management') return reply(401, { error: 'refused' }); ++ if (u.pathname === '/v8/management/credentials') return reply(200, { files: state.files, observed_at: '2026-10-24T00:00:00Z' }); ++ if (u.pathname === '/v8/management/config/api-keys/claude') { ++ if (req.method === 'GET') { ++ if (state.groups.length === 0 && state.emptyProviderResponse) ++ return reply(state.emptyProviderResponse.status, state.emptyProviderResponse.body); ++ return reply(200, state.groups); ++ } ++ // Stand-in treats auth_index as derived/read-only metadata. ++ if (value.some(g => (g.keys ?? []).some(k => Object.hasOwn(k, 'auth_index')))) return reply(422, { error: 'derived field' }); ++ state.receives++; ++ if (!state.priorityNoop) state.groups = value.map(g => ({ ...g, keys: g.keys.map(k => ({ ...k, auth_index: state.groups.flatMap(old => old.keys ?? []).find(old => old['api-key'] === k['api-key'])?.auth_index ?? `upstream-${nextId++}` })) })); ++ return reply(200, { status: 'ok' }); ++ } ++ if (u.pathname === '/v8/management/credentials/fields') { ++ if (state.patchStatus !== 200) return reply(state.patchStatus, { error: 'p4-private-detail' }); ++ const row = state.files.find(f => f.name === value.name); ++ if (!row) return reply(404, { error: 'not found' }); ++ if (!state.priorityNoop) row.priority = value.priority; ++ return reply(200, { status: 'ok' }); ++ } ++ if (u.pathname === '/v8/management/config/access/api-keys') return reply(200, state.keys); ++ if (u.pathname === '/v8/management/oauth/auth-url') return reply(200, { status: 'ok', state: 'p4-state', url: 'https://claude.ai/oauth/authorize?state=p4-state' }); ++ if (u.pathname === '/v8/management/oauth/status') return reply(200, { status: 'wait' }); ++ if (u.pathname === '/v8/management/oauth/callback' || u.pathname === '/v8/management/oauth/session') return reply(200, { status: 'ok' }); ++ return reply(404, {}); ++ }); ++ await new Promise(r => server.listen(0, '127.0.0.1', r)); ++ t.after(() => new Promise(r => server.close(r))); ++ const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${server.address().port}`, key: 'p4-management', receipt: entry => state.receipts.push(entry) }); ++ const page = new ProxyPage({ client, instanceTag: 'fixture', keyRecords: () => options.records ?? [] }); ++ const call = (verb, args = {}) => page.request(verb, args, { business: 'demo', human: 'owner' }); ++ return { state, page, call, client, server }; ++} +diff --git a/packages/bus/tests/proxy-page-state.test.mjs b/packages/bus/tests/proxy-page-state.test.mjs +new file mode 100644 +index 000000000..1e2b8c878 +--- /dev/null ++++ b/packages/bus/tests/proxy-page-state.test.mjs +@@ -0,0 +1,80 @@ ++// P4: callback must match an unfinished, unexpired login owned by this human/business. ++// Named mutant: skip pending-state check. Canary errors must never reflect upstream input. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++const moduleURL = new URL('../src/proxy-page.mjs', import.meta.url); ++const owner = { business: 'demo', human: 'owner' }; ++async function setup(options = {}) { ++ const { ProxyPage } = await import(moduleURL); ++ let now = 1000; ++ const calls = []; ++ const client = Object.fromEntries(['getCredentials', 'getKeys', 'oauthStart', 'oauthCallback', 'oauthStatus', 'oauthCancel', ++ 'getUpstreamGroups', 'putUpstreamGroups', 'patchCredential'].map(name => [name, async (...args) => { ++ calls.push([name, ...args]); ++ if (name === 'oauthStart') return { status: 'ok', state: 'p4-state', url: 'https://claude.ai/oauth/authorize?state=p4-state' }; ++ if (name === 'oauthStatus') return { status: 'wait' }; ++ if (name === 'getCredentials') return { files: [] }; ++ if (name === 'getUpstreamGroups') return []; ++ if (name === 'getKeys') return []; ++ return { status: 'ok' }; ++ }])); ++ const p = new ProxyPage({ client, instanceTag: 'fixture', now: () => now, ...options }); ++ return { client, calls, p, time: value => { now = value; }, call: (verb, args = {}, who = owner) => p.request(verb, args, who) }; ++} ++for (const [verb, args] of [ ++ ['proxy.login.start', { provider: 'other' }], ['proxy.apikey.add', { provider: 'other', key: 'fixture-key' }], ++]) test(`${verb}: provider-not-allowed`, async () => { ++ const s = await setup(); await assert.rejects(s.call(verb, args), { code: 'provider-not-allowed' }); assert.equal(s.calls.length, 0); ++}); ++for (const [name, key] of [['empty', ''], ['too long', 'x'.repeat(4097)], ['charset', 'bad\nkey']]) ++ test(`apikey-invalid ${name}`, async () => { ++ const s = await setup(); await assert.rejects(s.call('proxy.apikey.add', { provider: 'z.ai', key }), { code: 'apikey-invalid' }); assert.equal(s.calls.length, 0); ++ }); ++for (const [name, url] of [['not URL', 'not a url'], ['oversize', 'http://localhost/?' + 'a'.repeat(8193)], ++ ['repeated state', 'http://localhost/?state=p4-state&state=p4-state']]) ++ test(`callback-url-invalid ${name}`, async () => { ++ const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); ++ await assert.rejects(s.call('proxy.login.submit', { redirect_url: url }), { code: 'callback-url-invalid' }); ++ assert.equal(s.calls.filter(c => c[0] === 'oauthCallback').length, 0); ++ }); ++test('login-state-unknown callback never reaches proxy', async () => { ++ const s = await setup(); ++ await assert.rejects(s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=unknown&code=p4-code' }), { code: 'login-state-unknown' }); ++ assert.equal(s.calls.length, 0); ++}); ++test('login state is scoped to the human and business', async () => { ++ const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); ++ for (const who of [{ business: 'other', human: 'owner' }, { business: 'demo', human: 'other' }]) ++ await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }, who), { code: 'login-state-unknown' }); ++ assert.equal(s.calls.length, 1); ++}); ++test('accepted callback wait is pending; only a status ok completes and retires state', async () => { ++ const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); ++ assert.deepEqual(await s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=p4-state&code=p4-code' }), { status: 'wait' }); ++ assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'wait' }); ++ s.client.oauthStatus = async () => ({ status: 'ok', token: 'never-return' }); ++ assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'ok' }); ++ await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' }); ++ await assert.rejects(s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=p4-state&code=p4-code' }), { code: 'login-state-unknown' }); ++}); ++test('expiry reports login-timeout once with no management call and retires state', async () => { ++ const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); s.time(301000); ++ assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'error', reason: 'login-timeout' }); ++ assert.equal(s.calls.length, 1); ++ await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' }); ++}); ++test('cancel calls DELETE and retires state', async () => { ++ const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); ++ assert.deepEqual(await s.call('proxy.login.cancel', { state: 'p4-state' }), { status: 'cancelled' }); ++ assert.deepEqual(s.calls.at(-1), ['oauthCancel', 'p4-state']); ++ await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' }); ++}); ++test('upstream OAuth error body never crosses bus response', async () => { ++ const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); ++ s.client.oauthStatus = async () => ({ status: 'error', error: 'p4-private-code', state: 'p4-state' }); ++ assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'error', reason: 'login-failed' }); ++}); ++for (const args of [{ accountId: 'bad', priority: 1 }, { accountId: 'a'.repeat(12), priority: 1.5 }, ++ { accountId: 'a'.repeat(12), priority: 1001 }]) test('priority-invalid ' + JSON.stringify(args), async () => { ++ const s = await setup(); await assert.rejects(s.call('proxy.priority.set', args), { code: 'priority-invalid' }); ++}); +diff --git a/packages/bus/tests/proxy-page.test.mjs b/packages/bus/tests/proxy-page.test.mjs +new file mode 100644 +index 000000000..2e0d29994 +--- /dev/null ++++ b/packages/bus/tests/proxy-page.test.mjs +@@ -0,0 +1,55 @@ ++// P4: broker security boundary. Mutants: granted user accepted; secret in error/log. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import { mkdtempSync, rmSync } from 'node:fs'; ++import { tmpdir } from 'node:os'; ++import { join } from 'node:path'; ++import { Broker } from '../src/broker.mjs'; ++import { Store } from '../src/store.mjs'; ++ ++const verbs = ['proxy.accounts', 'proxy.login.start', 'proxy.login.submit', 'proxy.login.status', ++ 'proxy.login.cancel', 'proxy.apikey.add', 'proxy.priority.set', 'proxy.keys']; ++function setup(t, proxyPage = async () => ({ ok: true })) { ++ const root = mkdtempSync(join(tmpdir(), 'p4-bus-')); ++ const store = new Store(root); ++ t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); ++ const calls = []; ++ const broker = new Broker({ store, proxyPage: proxyPage && (async (...args) => { calls.push(args); return proxyPage(...args); }), ++ access: () => ({ accessVersion: 1, revision: 1, users: [], grants: [ ++ { id: 'g-1', user: 'alice', business: 'demo', projects: ['*'], instances: ['coder'], level: 'collaborator' }, ++ ] }), ++ businesses: { demo: { id: 'demo', human: 'owner', arbiters: { technical: 'coder', delivery: 'coder' }, ++ vars: { 'bus.tester': true }, roles: { coder: { definition: 'coder', authority: { withinRole: ['message.send'], crossRole: [] } } } } }, ++ }); ++ const caps = { ++ human: broker.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true }), ++ tester: broker.bindTester({ business: 'demo' }), ++ agent: broker.bindLaunch({ business: 'demo', role: 'coder', run: 'p4-run', harness: 'pi' }), ++ user: broker.bindUser({ business: 'demo', user: 'alice' }), ++ }; ++ return { calls, store, call: (who, verb, args = {}) => broker.request(caps[who], { verb, args }) }; ++} ++for (const verb of verbs) { ++ test(`${verb}: owner calls page once with business/human scope`, async t => { ++ const s = setup(t); ++ assert.deepEqual(await s.call('human', verb), { ok: true }); ++ assert.deepEqual(s.calls, [[verb, {}, { business: 'demo', human: 'owner' }]]); ++ }); ++ for (const who of ['tester', 'agent', 'user']) test(`${verb}: ${who} human-required`, async t => { ++ const s = setup(t); ++ await assert.rejects(async () => s.call(who, verb), { code: 'human-required' }); ++ assert.equal(s.calls.length, 0); ++ }); ++ test(`${verb}: not-configured`, async t => { ++ const s = setup(t, null); ++ await assert.rejects(async () => s.call('human', verb), { code: 'not-configured' }); ++ }); ++} ++test('API-key canary never enters broker events or unknown error output', async t => { ++ const canary = 'p4-canary-key'; ++ const s = setup(t, async () => { throw new Error(canary); }); ++ await assert.rejects(async () => s.call('human', 'proxy.apikey.add', { provider: 'z.ai', key: canary }), ++ e => e.code === 'proxy-down' && !e.message.includes(canary)); ++ assert.equal(s.calls.length, 1); ++ assert.equal(JSON.stringify(s.store.all('SELECT body FROM events')).includes(canary), false); ++}); +diff --git a/packages/model-proxy/README.md b/packages/model-proxy/README.md +index a173f9db6..dea5249b1 100644 +--- a/packages/model-proxy/README.md ++++ b/packages/model-proxy/README.md +@@ -47,3 +47,20 @@ Then start the bus normally and retry init from the human terminal. If the bus + or initialization state is uncertain, leave the lock in place. A refused fetch + may leave empty `opt/cliproxyapi/` parent directories; they are not an installed + version and need not be removed before retrying. ++ ++PIN upgrades must carry the native Console page's offline upgrade receipt. ++Run `bash agents/poe/work/queue-330/acceptance.sh ` ++under the test-slot rules, and include its output for the new PIN version in ++that upgrade row's BUILD.md. Sage's landing gate checks the receipt. The script ++uses a new temp root and private network namespace, with made-up credentials, ++and calls the page's bus verbs. If the page cannot pass against the new PIN, ++the upgrade stops; Sage takes Q46's fallback to Jason through Mos. ++ ++The native `/proxy` page uses human-only bus verbs; only the bus holds the ++management client. On PIN 8.0.42, OAuth credentials are listed under ++`/v8/management/credentials` and priority is patched as `{name, priority}`. ++API-key groups are absent from that list: z.ai keys use the Claude-compatible ++provider configuration at `/v8/management/config/api-keys/claude`, whose keys ++carry proxy-generated `auth_index` identifiers. Their priority updates use ++provider configuration and a read-back check. No upstream API-key value is ++returned by the bus. +diff --git a/packages/model-proxy/src/client.mjs b/packages/model-proxy/src/client.mjs +index d0829b789..c7d72f234 100644 +--- a/packages/model-proxy/src/client.mjs ++++ b/packages/model-proxy/src/client.mjs +@@ -15,14 +15,14 @@ export class ManagementClient { + || typeof receipt !== 'function' || !Number.isSafeInteger(timeout) || timeout <= 0) refuse('proxy-client-invalid'); + this.#base = u.origin; this.#key = key; this.#receipt = receipt; this.#timeout = timeout; + } +- #request(method, keys, onStatus) { ++ #request(method, endpoint, payload, onStatus) { + // node:http addresses the fixed numeric loopback directly. An ambient fetch + // dispatcher or HTTP_PROXY cannot send the Authorization header elsewhere. + let deadline; + return new Promise((resolve, reject) => { + let received = false; +- const req = http.request(this.#base + KEY_PATH, { +- method, headers: { Authorization: `Bearer ${this.#key}`, ...(method === 'PUT' ? { 'Content-Type': 'application/json' } : {}) }, ++ const req = http.request(this.#base + endpoint, { ++ method, headers: { Authorization: `Bearer ${this.#key}`, ...(payload !== undefined ? { 'Content-Type': 'application/json' } : {}) }, + }, response => { + received = true; onStatus(response.statusCode); + const error = response.statusCode >= 300 && response.statusCode < 400 ? 'proxy-client-redirect' +@@ -43,13 +43,13 @@ export class ManagementClient { + }); + req.on('error', () => reject(new ProxyError(received ? 'proxy-client-response' : 'proxy-client-down'))); + deadline = setTimeout(() => { req.destroy(); reject(new ProxyError('proxy-client-down')); }, this.#timeout); +- req.end(method === 'PUT' ? JSON.stringify(keys) : undefined); ++ req.end(payload === undefined ? undefined : JSON.stringify(payload)); + }).finally(() => clearTimeout(deadline)); + } + async #call(method, keys) { + let status = 0, ids = []; + try { +- const body = await this.#request(method, keys, value => { status = value; }); ++ const body = await this.#request(method, KEY_PATH, keys, value => { status = value; }); + if (method === 'GET') { + if (!validKeys(body)) refuse('proxy-client-response'); + ids = body.map(sha256).map(h => h.slice(0, 12)); +@@ -62,6 +62,35 @@ export class ManagementClient { + } + getKeys() { return this.#call('GET'); } + putKeys(keys) { if (!validKeys(keys)) refuse('proxy-client-invalid'); return this.#call('PUT', keys); } ++ // Fixed management operations for the bus's native page. Queries contain login ++ // states; neither queries nor request/response bodies belong in a receipt. ++ async #page(method, endpoint, payload, expectedStatus = null) { ++ let status = 0; ++ try { ++ const answer = await this.#request(method, endpoint, payload, value => { status = value; }); ++ if (expectedStatus !== null && status !== expectedStatus) refuse('proxy-client-refused'); ++ return answer; ++ } catch (error) { ++ if (method === 'GET' && endpoint === '/v8/management/config/api-keys/claude' && status === 404) return []; ++ throw error; ++ } ++ finally { this.#receipt({ call: `${method} ${endpoint.split('?')[0]}`, status, keyIds: [] }); } ++ } ++ getCredentials() { return this.#page('GET', '/v8/management/credentials'); } ++ getUpstreamGroups() { return this.#page('GET', '/v8/management/config/api-keys/claude'); } ++ putUpstreamGroups(groups) { ++ // auth_index is derived response metadata, not writable provider config. ++ const payload = groups.map(group => ({ ...group, keys: group.keys.map(({ auth_index, ...key }) => key) })); ++ return this.#page('PUT', '/v8/management/config/api-keys/claude', payload, 200); ++ } ++ patchCredential(name, priority) { return this.#page('PATCH', '/v8/management/credentials/fields', { name, priority }, 200); } ++ oauthStart(provider) { ++ if (!['claude', 'codex'].includes(provider)) refuse('proxy-client-invalid'); ++ return this.#page('GET', `/v8/management/oauth/auth-url?provider=${provider}`); ++ } ++ oauthCallback(redirect_url) { return this.#page('POST', '/v8/management/oauth/callback', { redirect_url }); } ++ oauthStatus(state) { return this.#page('GET', `/v8/management/oauth/status?state=${encodeURIComponent(state)}`); } ++ oauthCancel(state) { return this.#page('DELETE', `/v8/management/oauth/session?state=${encodeURIComponent(state)}`); } + } + + // Trusted bus boot only: no CLI/Console/session exports this value or client. +diff --git a/packages/model-proxy/tests/empty-provider.test.mjs b/packages/model-proxy/tests/empty-provider.test.mjs +new file mode 100644 +index 000000000..ef5a3412d +--- /dev/null ++++ b/packages/model-proxy/tests/empty-provider.test.mjs +@@ -0,0 +1,66 @@ ++// P4 pinned8.0.42 regression: absent Claude provider config is HTTP404. ++// Normalize only that fixed GET; all other methods/endpoints/statuses refuse. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import http from 'node:http'; ++import { ManagementClient } from '../src/client.mjs'; ++const endpoint = '/v8/management/config/api-keys/claude'; ++const bodyCanary = 'p4-body-canary'; ++async function fixture(t, status, body = { error: bodyCanary, 'api-key': bodyCanary }) { ++ const requests = [], receipts = []; ++ const server = http.createServer(async (req, res) => { ++ const chunks = []; for await (const chunk of req) chunks.push(chunk); ++ requests.push({ method: req.method, path: req.url, body: Buffer.concat(chunks).toString('utf8') }); ++ res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); ++ }); ++ await new Promise(r => server.listen(0, '127.0.0.1', r)); ++ t.after(() => new Promise(r => server.close(r))); ++ const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${server.address().port}`, key: 'p4-management', receipt: r => receipts.push(r) }); ++ return { client, requests, receipts }; ++} ++function evidence(s, method, path, status) { ++ assert.equal(s.requests.length, 1, 'one call, no retry'); ++ assert.deepEqual(s.requests.map(r => [r.method, r.path]), [[method, path]]); ++ assert.deepEqual(s.receipts, [{ call: `${method} ${path}`, status, keyIds: [] }]); ++ assert.equal(JSON.stringify(s.receipts).includes(bodyCanary), false); ++ assert.equal(JSON.stringify(s.receipts).includes('p4-management'), false); ++} ++async function refused(operation) { ++ let failure; ++ try { await operation(); } catch (error) { failure = error; } ++ assert.equal(failure?.code, 'proxy-client-refused'); ++ assert.equal(failure.message.includes(bodyCanary), false, 'no error-body value escapes'); ++ assert.equal(JSON.stringify(failure).includes(bodyCanary), false); ++} ++test('empty provider GET404 yields empty array, discards body and receipts actual404 once', async t => { ++ const s = await fixture(t, 404); ++ const result = await s.client.getUpstreamGroups(); ++ assert.equal(Array.isArray(result), true); assert.equal(result.length, 0); ++ assert.equal(JSON.stringify(result).includes(bodyCanary), false); ++ evidence(s, 'GET', endpoint, 404); ++}); ++for (const status of [401, 403, 500]) test(`provider GET${status} remains proxy-client-refused, no retry`, async t => { ++ const s = await fixture(t, status); ++ await refused(() => s.client.getUpstreamGroups()); ++ evidence(s, 'GET', endpoint, status); ++}); ++for (const [method, path] of [['getCredentials', '/v8/management/credentials'], ['getKeys', '/v8/management/config/access/api-keys']]) ++ test(`${method}404 remains proxy-client-refused, no retry`, async t => { ++ const s = await fixture(t, 404); ++ await refused(() => s.client[method]()); ++ evidence(s, 'GET', path, 404); ++ }); ++test('provider PUT404 remains proxy-client-refused, never treated as empty GET', async t => { ++ const s = await fixture(t, 404); ++ await refused(() => s.client.putUpstreamGroups([{ name: 'fixture', keys: [{ 'api-key': 'p4-first-key' }] }])); ++ evidence(s, 'PUT', endpoint, 404); ++ assert.equal(s.requests[0].body.includes('p4-first-key'), true); ++ assert.equal(JSON.stringify(s.receipts).includes('p4-first-key'), false); ++}); ++test('provider GET200 array passes through, with receipt and no retry', async t => { ++ const s = await fixture(t, 200, [{ name: 'fixture', keys: [] }]); ++ const result = await s.client.getUpstreamGroups(); ++ assert.equal(Array.isArray(result), true); assert.equal(result.length, 1); ++ assert.equal(result[0].name === 'fixture' && Array.isArray(result[0].keys), true); ++ evidence(s, 'GET', endpoint, 200); ++}); +diff --git a/packages/model-proxy/tests/page-client.test.mjs b/packages/model-proxy/tests/page-client.test.mjs +new file mode 100644 +index 000000000..b48b215b0 +--- /dev/null ++++ b/packages/model-proxy/tests/page-client.test.mjs +@@ -0,0 +1,49 @@ ++// P4 client contract; endpoint/payload cases are probe-pending on PIN8.0.42. ++// Catches management key/body/query values leaking into receipts, redirects/retries. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import http from 'node:http'; ++import { ManagementClient } from '../src/client.mjs'; ++async function setup(t, handler) { ++ const calls = [], receipts = []; ++ const server = http.createServer(async (req, res) => { ++ let body = ''; for await (const c of req) body += c; ++ calls.push({ method: req.method, path: req.url, auth: req.headers.authorization, body }); ++ const answer = handler?.(req, body) ?? { status: 200, body: { status: 'ok' } }; ++ res.writeHead(answer.status, { 'content-type': 'application/json', ...answer.headers }); ++ res.end(JSON.stringify(answer.body)); ++ }); ++ await new Promise(r => server.listen(0, '127.0.0.1', r)); ++ t.after(() => new Promise(r => server.close(r))); ++ const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${server.address().port}`, key: 'p4-management', receipt: r => receipts.push(r) }); ++ return { client, calls, receipts }; ++} ++test('OAuth management calls keep URL and state out of receipts', async t => { ++ const s = await setup(t); ++ assert.equal(typeof s.client.oauthStart, 'function'); ++ await s.client.oauthStart('claude'); ++ await s.client.oauthCallback('http://localhost/callback?state=p4-state&code=p4-code'); ++ await s.client.oauthStatus('p4-state'); ++ await s.client.oauthCancel('p4-state'); ++ assert.deepEqual(s.calls.map(({ method, path }) => [method, path]), [ ++ ['GET', '/v8/management/oauth/auth-url?provider=claude'], ['POST', '/v8/management/oauth/callback'], ++ ['GET', '/v8/management/oauth/status?state=p4-state'], ['DELETE', '/v8/management/oauth/session?state=p4-state'], ++ ]); ++ assert.ok(s.calls.every(c => c.auth === 'Bearer p4-management')); ++ for (const value of ['p4-state', 'p4-code', 'localhost', 'p4-management']) assert.equal(JSON.stringify(s.receipts).includes(value), false); ++}); ++test('upstream key goes only in request body and upstream refusal body is discarded', async t => { ++ const s = await setup(t, () => ({ status: 400, body: { error: 'p4-canary-key' } })); ++ assert.equal(typeof s.client.putUpstreamGroups, 'function'); ++ await assert.rejects(s.client.putUpstreamGroups([{ name: 'z.ai', keys: [{ 'api-key': 'p4-canary-key' }] }]), ++ e => !e.message.includes('p4-canary-key')); ++ assert.equal(s.calls.length, 1); ++ assert.ok(s.calls[0].body.includes('p4-canary-key')); ++ assert.equal(JSON.stringify(s.receipts).includes('p4-canary-key'), false); ++}); ++test('redirected management calls are not followed or retried', async t => { ++ const s = await setup(t, () => ({ status: 302, headers: { location: 'http://127.0.0.1:8317/' }, body: {} })); ++ assert.equal(typeof s.client.getCredentials, 'function'); ++ await assert.rejects(s.client.getCredentials(), { code: 'proxy-client-redirect' }); ++ assert.equal(s.calls.length, 1); ++}); +diff --git a/packages/webui/README.md b/packages/webui/README.md +index 1c81c895c..2f5b71d6b 100644 +--- a/packages/webui/README.md ++++ b/packages/webui/README.md +@@ -1104,3 +1104,23 @@ apply separately to each project and instance; a collaborator grant elsewhere + does not enable writes here. Grant hints refresh on each user chat render and + background refresh, while the bus decides every operation from live grants. + The existing user notice names the governance records visible to the owner. ++ ++## Model proxy page (P4, #1828) ++ ++The owner's `/proxy` page signs in Claude/Codex accounts, accepts write-only ++z.ai API keys, sets account priority, and lists client-key IDs. It uses only ++`/api/proxy/` routes over the owner's human bus transport. A missing instance ++proxy refuses `not-configured`; it does not ask for a management key. That key ++stays in the bus. No panel HTML or proxy-port forward is served. ++ ++The page and routes refuse granted users, tester listeners and loopback Hosts. ++Writes also require `--send`, the Console's own Origin and same-origin Fetch ++Metadata. Account display names use a provider label and stable hashed id; ++arbitrary upstream labels and emails are not reflected. Key inputs and pasted ++callback URLs are cleared when submitted and never saved in browser storage. ++Only an `ok` status completes OAuth; accepted callbacks remain pending. At the ++bus's default300-second timeout, status returns `error/login-timeout` once and ++retires the state. Refreshing the page does not restore a pending login. ++ ++Usage is reserved on this page for P5; P4 adds no usage source. PIN changes ++require the offline upgrade acceptance documented in packages/model-proxy. +diff --git a/packages/webui/src/cli.mjs b/packages/webui/src/cli.mjs +index 2b77e88d5..6a9d767d9 100644 +--- a/packages/webui/src/cli.mjs ++++ b/packages/webui/src/cli.mjs +@@ -68,14 +68,14 @@ try { + // the system config, Business can't resolve authority and Settings has + // no notifier. A refusal printed here names no path (redactor). + // --send without a bus refuses to start rather than serve a composer that can't send. +- let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, libraryRevert = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, decisionWrite = null, busLine, system = null; ++ let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, libraryRevert = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, proxyCall = null, decisionWrite = null, busLine, system = null; + let processing = null, ownerCall = null; + try { + system = loadSystem(); + const { dataRoot } = system; + const call = ownerCall = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null }); + // FED R6 (#1800): the bus-backed options, shared with each granted user's session (console-bus.mjs). +- ({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, libraryRevert, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite } = consoleBus(call, { send: sendOn })); ++ ({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, libraryRevert, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall } = consoleBus(call, { send: sendOn })); + // Row 252 (#1753): Approve and Deny on grant requests, through the console wrapper, only with + // --sso and --send. + if (ssoOn && sendOn) decisionWrite = consoleCall({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null }); +@@ -107,7 +107,7 @@ try { + } + let server; + try { +- server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, libraryRevert, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, decisionWrite, userOptions, federation: system?.federation ?? null }); ++ server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, libraryRevert, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall, processing, decisionWrite, userOptions, federation: system?.federation ?? null }); + } catch (err) { sso?.close(); throw err; } + let testerServer = null; + if (testerSocket) { +diff --git a/packages/webui/src/console-bus.mjs b/packages/webui/src/console-bus.mjs +index df88b0ae7..5443c42c3 100644 +--- a/packages/webui/src/console-bus.mjs ++++ b/packages/webui/src/console-bus.mjs +@@ -8,6 +8,9 @@ import { views } from '../../bus/src/views.mjs'; + // Row 194 (#1692, D7): the Agents section's verbs. The caller refuses any other, so the server's + // agentWrite reaches only these. + export const AGENT_VERBS = ['instance.launch', 'instance.regenerate', 'instance.end', 'veto.set', 'instance.request']; ++// P4 owner transport only; the HTTP boundary and broker both refuse users/testers. ++export const PROXY_VERBS = Object.freeze(['proxy.accounts', 'proxy.login.start', 'proxy.login.submit', ++ 'proxy.login.status', 'proxy.login.cancel', 'proxy.apikey.add', 'proxy.priority.set', 'proxy.keys']); + export const TESTER_AGENT_VERBS = ['instance.launch', 'instance.end', 'instance.request']; + export const agentCaller = (call, verbs) => async (verb, args) => { + if (!verbs.includes(verb)) throw new Error(`the Console does not call ${verb}`); +@@ -18,6 +21,7 @@ export const agentCaller = (call, verbs) => async (verb, args) => { + // a granted user's options; it goes to createChat, which sends it only the `plugins` read. + export function consoleBus(call, { send: sendOn = false, owner = null } = {}) { + const o = { ++ proxyCall: agentCaller(call, PROXY_VERBS), + processing: { read: () => call('processing.status'), halt: args => call('processing.halt', args), resume: args => call('processing.resume', args) }, + chat: createChat({ call, owner }), + // Row 155 (#1649): `names` is a webui read only, so it sits beside the shared views. Row 165 +diff --git a/packages/webui/src/proxy-http.mjs b/packages/webui/src/proxy-http.mjs +new file mode 100644 +index 000000000..6a5beaf8a +--- /dev/null ++++ b/packages/webui/src/proxy-http.mjs +@@ -0,0 +1,52 @@ ++// Native Console routes: fixed path -> one owner human-transport call. Never ++// imports a management client, reads proxy settings, or opens a proxy connection. ++const routes = Object.freeze({ ++ '/api/proxy/accounts': ['GET', 'proxy.accounts', []], ++ '/api/proxy/keys': ['GET', 'proxy.keys', []], ++ '/api/proxy/login/start': ['POST', 'proxy.login.start', ['provider']], ++ '/api/proxy/login/submit': ['POST', 'proxy.login.submit', ['redirect_url']], ++ '/api/proxy/login/status': ['POST', 'proxy.login.status', ['state']], ++ '/api/proxy/login/cancel': ['POST', 'proxy.login.cancel', ['state']], ++ '/api/proxy/apikey/add': ['POST', 'proxy.apikey.add', ['provider', 'key']], ++ '/api/proxy/priority/set': ['POST', 'proxy.priority.set', ['accountId', 'priority']], ++}); ++const codes = ['not-configured', 'human-required', 'provider-not-allowed', 'login-state-unknown', ++ 'callback-url-invalid', 'apikey-invalid', 'priority-invalid', 'priority-unconfirmed', 'priority-write-refused', 'proxy-down', 'invalid-request']; ++const json = (res, status, value) => { ++ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value)); ++}; ++export async function proxyHTTP(req, res, { path, call, named, tester, granted, send, origin, user, log }) { ++ const route = routes[path]; ++ const done = (status, value) => { ++ // No request body, query, login state, authorization URL or exception text. ++ log(`${new Date().toISOString()} ${route?.[0] ?? 'GET'} ${route ? path : '/api/proxy/unknown'} ${status} user=${JSON.stringify(user ?? '')}`); ++ return json(res, status, value); ++ }; ++ if (tester || granted || !named) return done(403, { error: 'human-required' }); ++ if (!route) return done(404, { error: 'not found' }); ++ if (req.method !== route[0]) return done(405, { error: 'method not allowed' }); ++ if (req.method === 'POST') { ++ if (!send) return done(403, { error: 'read-only' }); ++ if (req.headersDistinct.origin?.length !== 1 || req.headers.origin !== origin) return done(403, { error: 'cross-origin request refused' }); ++ if (req.headersDistinct['sec-fetch-site']?.length !== 1 || req.headers['sec-fetch-site'] !== 'same-origin') return done(403, { error: 'cross-site request refused' }); ++ } ++ if (!call) return done(503, { error: 'not-configured' }); ++ let args = {}; ++ if (req.method === 'POST') { ++ try { ++ if ((req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase() !== 'application/json') throw Error(); ++ const chunks = []; let size = 0; ++ for await (const chunk of req) { ++ size += chunk.length; if (size > 16384) throw Error(); chunks.push(chunk); ++ } ++ args = JSON.parse(Buffer.concat(chunks)); ++ if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).length !== route[2].length ++ || route[2].some(k => !Object.hasOwn(args, k))) throw Error(); ++ } catch { return done(400, { error: 'invalid-request' }); } ++ } ++ try { return done(200, await call(route[1], args)); } ++ catch (error) { ++ const code = codes.includes(error.code) ? error.code : 'outcome-unknown'; ++ return done(code === 'human-required' ? 403 : ['not-configured', 'proxy-down', 'outcome-unknown'].includes(code) ? 503 : 400, { error: code }); ++ } ++} +diff --git a/packages/webui/src/public/index.html b/packages/webui/src/public/index.html +index a2185147c..ebbf679b1 100644 +--- a/packages/webui/src/public/index.html ++++ b/packages/webui/src/public/index.html +@@ -20,7 +20,7 @@ + +

Processing controls are unavailable here. Use mosaic halt or mosaic resume from the instance CLI.

+

+-

Console

    Projects

    Loading projects…

    Select a project to filter sessions. Waiting on you always shows all projects.

    ++

    Model proxy

    Console

      Projects

      Loading projects…

      Select a project to filter sessions. Waiting on you always shows all projects.

      +

      Control board

      Loading board…

      +

      Held roles –

      The stack's seats: each role a run holds on the bus, from the bus agents read. Machine sessions are under Show machine sessions.

      Reading the held roles.
      + +diff --git a/packages/webui/src/public/proxy.html b/packages/webui/src/public/proxy.html +new file mode 100644 +index 000000000..16bbc0a77 +--- /dev/null ++++ b/packages/webui/src/public/proxy.html +@@ -0,0 +1,26 @@ ++ ++ ++ ++Model proxy · Mosaic Console ++ ++ ++ ++
      MosaicConsoleBack to Console
      ++
      ++

      Model proxy

      Accounts used by this instance, their priority, and the client keys assigned to agents.

      ++

      Reading accounts…

      ++ ++

      Accounts

      ++

      Higher priority is used first. Account names are stable identifiers; credential values are never displayed.

      ++
      AccountProviderStatusPriority
      ++

      Sign in to an account

      ++
      Subscription account
      ++

      ++

      ++
      Finish sign-in

      If the sign-in tab does not return, copy its full address and paste it here.

      ++

      Add an API key

      ++
      z.ai

      The key is sent once and cleared from this field. It will never be shown again.

      ++

      Client keys

      Only key IDs are shown. Hire and fire manage agent keys.

      ++
      Key IDManaged byAgentIssuedState
      ++

      Usage

      Usage reporting will appear here when its view is available.

      ++
      +diff --git a/packages/webui/src/public/proxy.js b/packages/webui/src/public/proxy.js +new file mode 100644 +index 000000000..2507bef4f +--- /dev/null ++++ b/packages/webui/src/public/proxy.js +@@ -0,0 +1,108 @@ ++// P4 page: credentials and OAuth state exist only during the current operation. ++// No browser storage, request/body logging, key reveal, or automatic write retry. ++(() => { ++ 'use strict'; ++ const $ = id => document.getElementById(id); ++ let canWrite = false, login = null, poll = null, busy = false; ++ const message = (text, loginOnly = false) => { $(loginOnly ? 'proxy-login-message' : 'proxy-message').textContent = text; }; ++ const cell = value => { const td = document.createElement('td'); td.textContent = value ?? '—'; return td; }; ++ async function call(path, args) { ++ const response = await fetch('/api/proxy/' + path, { cache: 'no-store', credentials: 'same-origin', ++ method: args === undefined ? 'GET' : 'POST', ++ ...(args === undefined ? {} : { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(args) }), ++ }); ++ const value = await response.json(); ++ if (!response.ok) { ++ if (response.status === 401) throw new Error('Sign in to the Console again.'); ++ if (value.error === 'not-configured') throw new Error('The model proxy is not configured for this instance.'); ++ if (value.error === 'login-state-unknown') throw new Error('This sign-in has ended. Start a new sign-in.'); ++ if (value.error === 'apikey-invalid') throw new Error('Enter a nonempty API key, with no spaces or line breaks.'); ++ throw new Error('The operation could not be confirmed. Nothing was retried. Refresh before trying again.'); ++ } ++ return value; ++ } ++ function endLogin() { ++ clearTimeout(poll); poll = null; login = null; ++ $('proxy-authorize').removeAttribute('href'); $('proxy-authorize').hidden = true; ++ $('proxy-login-submit').elements.redirect_url.value = ''; ++ } ++ async function status() { ++ if (!login) return; ++ const active = login; ++ try { ++ const result = await call('login/status', { state: active.state }); ++ if (active !== login) return; ++ if (result.status === 'wait') { message('Waiting for account sign-in…', true); poll = setTimeout(status, 2000); return; } ++ endLogin(); ++ if (result.status === 'ok') { message('Account signed in.', true); await refresh(); } ++ else message(result.reason === 'login-timeout' ? 'Sign-in timed out. Start again.' : 'Account sign-in failed. Start again.', true); ++ } catch (error) { if (active === login) { endLogin(); message(error.message, true); } } ++ } ++ async function refresh() { ++ const [accounts, keys] = await Promise.all([call('accounts'), call('keys')]); ++ const rows = accounts.accounts.map(account => { ++ const row = document.createElement('tr'); ++ row.append(cell(account.displayName), cell(account.provider), cell(account.status)); ++ const td = document.createElement('td'), form = document.createElement('form'); ++ const input = document.createElement('input'); input.type = 'number'; input.min = '-1000'; input.max = '1000'; input.step = '1'; input.value = account.priority; ++ input.setAttribute('aria-label', `Priority for ${account.displayName}`); input.disabled = !canWrite; ++ const save = document.createElement('button'); save.type = 'submit'; save.textContent = 'Set priority'; save.disabled = !canWrite; ++ form.append(input, save); form.addEventListener('submit', async event => { ++ event.preventDefault(); if (!canWrite || busy || !input.checkValidity()) return; ++ busy = true; save.disabled = true; ++ try { await call('priority/set', { accountId: account.accountId, priority: Number(input.value) }); message('Priority saved.'); await refresh(); } ++ catch (error) { message(error.message); } ++ finally { busy = false; save.disabled = !canWrite; } ++ }); ++ td.append(form); row.append(td); return row; ++ }); ++ $('proxy-accounts').replaceChildren(...rows); ++ if (!rows.length) { const row = document.createElement('tr'); const td = cell('No accounts configured.'); td.colSpan = 4; row.append(td); $('proxy-accounts').append(row); } ++ $('proxy-keys').replaceChildren(...keys.keys.map(key => { ++ const row = document.createElement('tr'); row.append(cell(key.keyId), cell(key.management), cell(key.agent), cell(key.issuedAt), cell(key.state)); return row; ++ })); ++ } ++ $('proxy-refresh').addEventListener('click', () => refresh().then(() => message('Accounts and client keys refreshed.')).catch(e => message(e.message))); ++ $('proxy-login-start').addEventListener('submit', async event => { ++ event.preventDefault(); if (!canWrite || busy || login) return; ++ busy = true; ++ try { ++ login = await call('login/start', { provider: event.currentTarget.elements.provider.value }); ++ $('proxy-authorize').href = login.url; $('proxy-authorize').hidden = false; ++ window.open(login.url, '_blank', 'noopener,noreferrer'); ++ message('Complete account sign-in in the new tab, or use the sign-in link.', true); ++ poll = setTimeout(status, 2000); ++ } catch (error) { message(error.message, true); } ++ finally { busy = false; } ++ }); ++ $('proxy-login-submit').addEventListener('submit', async event => { ++ event.preventDefault(); if (!canWrite || busy || !login) return; ++ const input = event.currentTarget.elements.redirect_url, redirect_url = input.value; input.value = ''; ++ busy = true; ++ try { await call('login/submit', { redirect_url }); message('Address submitted. Waiting for sign-in confirmation…', true); } ++ catch (error) { message(error.message, true); } ++ finally { busy = false; } ++ }); ++ $('proxy-login-cancel').addEventListener('click', async () => { ++ if (!canWrite || busy || !login) return; ++ busy = true; clearTimeout(poll); ++ try { await call('login/cancel', { state: login.state }); endLogin(); message('Sign-in cancelled.', true); } ++ catch (error) { endLogin(); message(error.message, true); } ++ finally { busy = false; } ++ }); ++ $('proxy-apikey').addEventListener('submit', async event => { ++ event.preventDefault(); if (!canWrite || busy) return; ++ const input = event.currentTarget.elements.key, key = input.value; input.value = ''; ++ busy = true; ++ try { await call('apikey/add', { provider: 'z.ai', key }); message('API key added.'); await refresh(); } ++ catch (error) { message(error.message); } ++ finally { busy = false; } ++ }); ++ window.addEventListener('pagehide', () => { endLogin(); $('proxy-apikey').elements.key.value = ''; }); ++ fetch('/api/config', { cache: 'no-store' }).then(r => r.json()).then(config => { ++ canWrite = config.send === true && config.tester !== true && config.user !== true; ++ document.querySelectorAll('[data-write]').forEach(fieldset => { fieldset.disabled = !canWrite; }); ++ $('proxy-readonly').hidden = canWrite; ++ return refresh(); ++ }).then(() => message('Accounts and client keys loaded.')).catch(error => message(error.message)); ++})(); +diff --git a/packages/webui/src/serve.mjs b/packages/webui/src/serve.mjs +index a1fb81a52..1ff5e4ed5 100644 +--- a/packages/webui/src/serve.mjs ++++ b/packages/webui/src/serve.mjs +@@ -1,3 +1,4 @@ ++import { proxyHTTP } from './proxy-http.mjs'; + import { createServer } from 'node:http'; + import { bindTesterSocket, TESTER_ORIGIN } from './tester-socket.mjs'; + import { readFileSync, openSync, fstatSync, closeSync, constants } from 'node:fs'; +@@ -29,6 +30,8 @@ export function boardURL(value) { + const root = resolve(import.meta.dirname, 'public'); + const files = new Map([ + ['/', ['index.html', 'text/html; charset=utf-8']], ++ ['/proxy', ['proxy.html', 'text/html; charset=utf-8']], ++ ['/proxy.js', ['proxy.js', 'text/javascript; charset=utf-8']], + ...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'processing.js', 'agents.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]), + ...['shared/app.css', 'tokens.css', 'console.css', 'live.css', 'bus.css', 'shell.css', 'chat.css', 'processing.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]), + ['/editor.js', ['editor.js', 'text/javascript; charset=utf-8']], +@@ -650,7 +653,7 @@ async function libraryStatusRead(res, read, search) { + // `log` takes one line per send attempt that got past sign-in; never the body. `fieldsFile` is for + // tests; the field table is otherwise the committed packages/tasks/fields.json. + // `agentWrite` (row 194, D7) is `(verb, args) => Promise` for the Agents section's verbs and instance.request. +-export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, libraryStatus = null, libraryRevert = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) { ++export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, libraryStatus = null, libraryRevert = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, proxyCall = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) { + if (!isLoopback(host)) throw new Error('refusing to bind to non-loopback host'); + if (host === 'localhost') host = '127.0.0.1'; + if (tester && preview !== null) throw new Error('tester listener cannot use preview mode'); +@@ -668,7 +671,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF + // `userOptions` (FED R6, #1800) is `session => options`, the bus-backed options over that session + // user's own transport (console-bus.mjs). Only a signed-in listener has user sessions. + if (userOptions !== null && (sso === null || typeof userOptions !== 'function')) throw new Error('user sessions need --sso'); +- const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files }; ++ const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall, processing, upstream, files }; + const hub = federationView(federation); + // An object with none of the three is refused too: asking for the preview never serves without it. + if (preview !== null && !(preview = previewOptions({ publicHost: preview.host, authHeader: preview.header, allowUser: preview.user }))) { +@@ -678,7 +681,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF + if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso'); + const server = createServer(async (req, res) => { + // A granted user's request rebinds these below; every other request keeps the owner's. +- let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false, chatGrants = []; ++ let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall, processing, upstream, files } = owner, granted = false, chatGrants = []; + res.setHeader('x-content-type-options', 'nosniff'); + res.setHeader('referrer-policy', 'no-referrer'); + // Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and +@@ -745,7 +748,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF + if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' }); + if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' }); + ({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session)); +- reads = null; upstream = null; libraryStatus = null; libraryRevert = null; files = USER_FILES; granted = true; ++ reads = null; upstream = null; libraryStatus = null; libraryRevert = null; files = USER_FILES; granted = true; proxyCall = null; + chatGrants = sso.chatGrants?.(session) ?? []; + } + } +@@ -755,6 +758,15 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF + if (!values || values.length !== 1 || values[0] !== preview.user) return json(res, 401, { error: 'sign-in required' }); + } + const writer = tester ? { tester: true, user: 'tester', origin: TESTER_ORIGIN } : signer; ++ // P4 page and verbs are owner-only, including reads and static page assets. ++ // This runs after owner sign-in and the granted-user fence, before generic ++ // tester writes or static-file serving. No localhost preview grants access. ++ if (path === '/proxy' || path === '/proxy.js' || path.startsWith('/api/proxy/')) { ++ if (tester || granted || !named) return json(res, 403, { error: 'human-required' }); ++ if (path.startsWith('/api/proxy/')) return proxyHTTP(req, res, { path, call: proxyCall, named, tester, granted, ++ send, origin: sso ? sso.publicOrigin : `https://${preview.host}`, user: signer?.user, log }); ++ } ++ + // Row 252: Approve and Deny. The tester listener never offers them. Anywhere else without a signed-in + // session on the public name under --sso and --send, they are read-only. + if (decisionPath) { +diff --git a/packages/webui/tests/proxy-architecture.test.mjs b/packages/webui/tests/proxy-architecture.test.mjs +new file mode 100644 +index 000000000..1befd3b3d +--- /dev/null ++++ b/packages/webui/tests/proxy-architecture.test.mjs +@@ -0,0 +1,12 @@ ++// Import-parser architecture guard: adding the management client to the Console ++// would move its key/connection boundary out of the bus, and must fail this test. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import { build } from '../node_modules/esbuild/lib/main.js'; ++import { fileURLToPath } from 'node:url'; ++test('Console production module graph cannot import the model-proxy client', async () => { ++ const result = await build({ entryPoints: [new URL('../src/cli.mjs', import.meta.url), new URL('../src/serve.mjs', import.meta.url), ++ new URL('../src/console-bus.mjs', import.meta.url)].map(fileURLToPath), bundle: true, packages: 'external', ++ platform: 'node', format: 'esm', splitting: true, outdir: '/unused-proxy-architecture', write: false, metafile: true, logLevel: 'silent' }); ++ assert.equal(Object.keys(result.metafile.inputs).some(file => file.endsWith('/model-proxy/src/client.mjs')), false); ++}); +diff --git a/packages/webui/tests/proxy-browser.test.mjs b/packages/webui/tests/proxy-browser.test.mjs +new file mode 100644 +index 000000000..fd40550d8 +--- /dev/null ++++ b/packages/webui/tests/proxy-browser.test.mjs +@@ -0,0 +1,77 @@ ++// Native page behavior. HTTP authorization is covered by proxy-page.test.mjs; ++// this stand-in serves the real page/script and records only route names/counts. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import http from 'node:http'; ++import { readFileSync } from 'node:fs'; ++import { browser } from './browser.mjs'; ++async function setup(t, readonly = false) { ++ const state = { routes: [], loginStatus: 'wait', receivedKey: false, release: null, priority: 0 }; ++ const server = http.createServer(async (req, res) => { ++ const send = value => { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(value)); }; ++ if (req.url === '/api/config') return send({ send: !readonly }); ++ if (req.url.startsWith('/api/proxy/')) { ++ state.routes.push(req.url); ++ const chunks = []; for await (const c of req) chunks.push(c); ++ const args = chunks.length ? JSON.parse(Buffer.concat(chunks)) : {}; ++ if (req.url.endsWith('/accounts')) return send({ accounts: [{ accountId: 'a'.repeat(12), provider: 'claude', displayName: '', status: 'ready', priority: state.priority }] }); ++ if (req.url.endsWith('/keys')) return send({ keys: [{ keyId: 'b'.repeat(12), management: 'sentinel', agent: null, issuedAt: null, state: null }] }); ++ if (req.url.endsWith('/apikey/add')) { ++ state.receivedKey = args.key === 'p4-browser-key'; ++ await new Promise(r => { state.release = r; }); return send({ added: true }); ++ } ++ if (req.url.endsWith('/login/start')) return send({ url: 'https://claude.ai/oauth/authorize?state=p4-state', state: 'p4-state' }); ++ if (req.url.endsWith('/login/status')) return send({ status: state.loginStatus }); ++ if (req.url.endsWith('/login/submit')) return send({ status: 'wait' }); ++ if (req.url.endsWith('/login/cancel')) return send({ status: 'cancelled' }); ++ if (req.url.endsWith('/priority/set')) { state.priority = args.priority; return send({ priority: args.priority }); } ++ } ++ if (req.url === '/proxy') { res.setHeader('content-type', 'text/html'); return res.end(readFileSync(new URL('../src/public/proxy.html', import.meta.url))); } ++ if (req.url === '/proxy.js') { res.setHeader('content-type', 'text/javascript'); return res.end(readFileSync(new URL('../src/public/proxy.js', import.meta.url))); } ++ res.statusCode = 404; res.end(); ++ }); ++ await new Promise(r => server.listen(0, '127.0.0.1', r)); ++ t.after(() => { state.release?.(); return new Promise(r => server.close(r)); }); ++ const b = await browser({ machine: false }); t.after(() => b.close()); ++ await b.call('Page.addScriptToEvaluateOnNewDocument', { source: "window.openFeatures=null;window.open=(url,target,features)=>{window.openFeatures=features;return null;};" }); ++ await b.navigate(`http://127.0.0.1:${server.address().port}/proxy`); ++ const wait = expression => b.evaluate(`(async()=>{for(let n=0;n<200;n++){if(${expression})return true;await new Promise(r=>setTimeout(r,25));}throw Error('proxy UI condition timed out');})()`); ++ await wait('document.querySelector("#proxy-message").textContent.includes("loaded")'); ++ return { b, state, wait }; ++} ++test('proxy browser clears write-only key immediately, sends once and renders data as text', { timeout: 60000 }, async t => { ++ const { b, state, wait } = await setup(t); ++ assert.equal(await b.evaluate('window.injected===true'), false); ++ assert.equal(await b.evaluate('document.querySelectorAll("#proxy-accounts img").length'), 0); ++ assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").type'), 'password'); ++ assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").autocomplete'), 'off'); ++ await b.evaluate('document.querySelector("#proxy-apikey input").value="p4-browser-key";document.querySelector("#proxy-apikey").requestSubmit();true'); ++ await wait('document.querySelector("#proxy-apikey input").value===""'); ++ for (let n = 0; n < 100 && !state.release; n++) await new Promise(r => setTimeout(r, 20)); ++ assert.equal(state.receivedKey, true); ++ state.release(); await wait('document.querySelector("#proxy-message").textContent.includes("added")'); ++ assert.equal(state.routes.filter(r => r.endsWith('/apikey/add')).length, 1); ++ assert.equal(await b.evaluate('localStorage.length+sessionStorage.length'), 0); ++ assert.equal(await b.evaluate('document.body.textContent.includes("p4-browser-key")'), false); ++}); ++test('proxy browser OAuth opens safely, callback wait stays pending, status ok completes', { timeout: 60000 }, async t => { ++ const { b, state, wait } = await setup(t); ++ await b.evaluate('document.querySelector("#proxy-login-start").requestSubmit();true'); ++ await wait('!document.querySelector("#proxy-authorize").hidden'); ++ assert.equal(await b.evaluate('window.openFeatures'), 'noopener,noreferrer'); ++ assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").rel'), 'noopener noreferrer'); ++ await b.evaluate('document.querySelector("#proxy-login-submit input").value="http://localhost/callback?state=p4-state&code=p4-code";document.querySelector("#proxy-login-submit").requestSubmit();true'); ++ await wait('document.querySelector("#proxy-login-message").textContent.includes("Waiting")'); ++ assert.equal(await b.evaluate('document.querySelector("#proxy-login-message").textContent.includes("signed in")'), false); ++ assert.equal(await b.evaluate('document.querySelector("#proxy-login-submit input").value===""'), true); ++ state.loginStatus = 'ok'; ++ await wait('document.querySelector("#proxy-login-message").textContent==="Account signed in."'); ++ assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").hasAttribute("href")'), false); ++ assert.equal(await b.evaluate('localStorage.length+sessionStorage.length'), 0); ++}); ++test('proxy browser read-only disables account changes', { timeout: 60000 }, async t => { ++ const { b, state } = await setup(t, true); ++ assert.equal(await b.evaluate('[...document.querySelectorAll("[data-write]")].every(e=>e.disabled)'), true); ++ assert.equal(await b.evaluate('document.querySelector("#proxy-accounts input").disabled'), true); ++ assert.equal(state.routes.some(r => r.endsWith('/apikey/add') || r.endsWith('/login/start')), false); ++}); +diff --git a/packages/webui/tests/proxy-canary.test.mjs b/packages/webui/tests/proxy-canary.test.mjs +new file mode 100644 +index 000000000..152ddfbad +--- /dev/null ++++ b/packages/webui/tests/proxy-canary.test.mjs +@@ -0,0 +1,67 @@ ++// P4 named mutant: API key echoed in a response, error, log or durable record. ++// Real HTTP -> human capability -> broker -> page -> management client -> fake. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import { request } from 'node:http'; ++import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs'; ++import { tmpdir } from 'node:os'; ++import { join } from 'node:path'; ++import { fakeManagement } from '../../bus/tests/proxy-page-fixture.mjs'; ++import { Store } from '../../bus/src/store.mjs'; ++import { Broker } from '../../bus/src/broker.mjs'; ++import { startServer } from '../src/serve.mjs'; ++const scan = root => readdirSync(root, { withFileTypes: true }).flatMap(e => e.isDirectory() ? scan(join(root, e.name)) : [readFileSync(join(root, e.name))]); ++test('API-key canary reaches fake once and zero responses/logs/receipts/store files', async t => { ++ const canary = 'p4-canary-key'; ++ const fake = await fakeManagement(t); ++ const root = mkdtempSync(join(tmpdir(), 'p4-canary-')); ++ const store = new Store(root); ++ t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); ++ const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args), ++ businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } }); ++ const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true }); ++ const logs = [], output = []; ++ const server = await startServer({ port: 0, preview: { host: 'console.example', header: 'x-user', user: 'owner' }, ++ send: async () => ({}), proxyCall: (verb, args) => b.request(cap, { verb, args }), log: line => logs.push(line) }); ++ t.after(() => new Promise(r => server.close(r))); ++ const call = (path, data) => new Promise((resolve, reject) => { ++ const req = request({ hostname: '127.0.0.1', port: server.address().port, path, method: data ? 'POST' : 'GET', ++ headers: { host: 'console.example', 'x-user': 'owner', origin: 'https://console.example', 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' } }, res => { ++ let text = ''; res.on('data', c => text += c); res.on('end', () => { output.push(text); resolve(res.statusCode); }); ++ }); req.on('error', reject); req.end(data ? JSON.stringify(data) : undefined); ++ }); ++ // Capture process output during the operation as well as structured log ++ // callbacks. Even a deliberate log-leak mutant must never print its canary. ++ const stdout = process.stdout.write, stderr = process.stderr.write; ++ const capture = (chunk, ...rest) => { output.push(Buffer.from(chunk)); const cb = rest.at(-1); if (typeof cb === 'function') cb(); return true; }; ++ let added, accounts; ++ try { ++ process.stdout.write = capture; process.stderr.write = capture; ++ added = await call('/api/proxy/apikey/add', { provider: 'z.ai', key: canary }); ++ accounts = await call('/api/proxy/accounts'); ++ } finally { process.stdout.write = stdout; process.stderr.write = stderr; } ++ assert.equal(added, 200); assert.equal(accounts, 200); ++ assert.equal(fake.state.receives, 1); ++ assert.equal(fake.state.groups[0].keys[0]['api-key'] === canary, true); ++ for (const bytes of [...output, ...logs, JSON.stringify(fake.state.receipts), ...scan(root)]) ++ assert.equal(Buffer.from(bytes).includes(Buffer.from(canary)), false, 'credential value escaped'); ++}); ++ ++test('OAuth URLs, state and code stay out of broker records and management receipts', async t => { ++ const fake = await fakeManagement(t); ++ const root = mkdtempSync(join(tmpdir(), 'p4-oauth-')); ++ const store = new Store(root); ++ t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); }); ++ const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args), ++ businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } }); ++ const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true }); ++ const call = (verb, args = {}) => b.request(cap, { verb, args }); ++ const login = await call('proxy.login.start', { provider: 'claude' }); ++ const callback = 'http://localhost/callback?state=' + login.state + '&code=p4-private-code'; ++ assert.deepEqual(await call('proxy.login.submit', { redirect_url: callback }), { status: 'wait' }); ++ assert.deepEqual(await call('proxy.login.status', { state: login.state }), { status: 'wait' }); ++ for (const value of [login.url, login.state, callback, 'p4-private-code']) { ++ assert.equal(JSON.stringify(fake.state.receipts).includes(value), false); ++ for (const bytes of scan(root)) assert.equal(bytes.includes(Buffer.from(value)), false); ++ } ++}); +diff --git a/packages/webui/tests/proxy-page.test.mjs b/packages/webui/tests/proxy-page.test.mjs +new file mode 100644 +index 000000000..002f1c6be +--- /dev/null ++++ b/packages/webui/tests/proxy-page.test.mjs +@@ -0,0 +1,115 @@ ++// P4: real HTTP boundary; mutant: proxy route moved ahead of granted-user fence. ++import test from 'node:test'; ++import assert from 'node:assert/strict'; ++import { request } from 'node:http'; ++import { startServer } from '../src/serve.mjs'; ++import { consoleBus } from '../src/console-bus.mjs'; ++const preview = { host: 'console.example', header: 'x-user', user: 'owner' }; ++const headers = { host: preview.host, 'x-user': 'owner', origin: 'https://console.example', ++ 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' }; ++const routes = [ ++ ['accounts', 'GET', 'proxy.accounts', {}], ['keys', 'GET', 'proxy.keys', {}], ++ ['login/start', 'POST', 'proxy.login.start', { provider: 'claude' }], ++ ['login/submit', 'POST', 'proxy.login.submit', { redirect_url: 'http://localhost/callback?state=fixture&code=fake' }], ++ ['login/status', 'POST', 'proxy.login.status', { state: 'fixture' }], ++ ['login/cancel', 'POST', 'proxy.login.cancel', { state: 'fixture' }], ++ ['apikey/add', 'POST', 'proxy.apikey.add', { provider: 'z.ai', key: 'p4-canary-key' }], ++ ['priority/set', 'POST', 'proxy.priority.set', { accountId: 'a'.repeat(12), priority: 5 }], ++]; ++const call = (s, path, method = 'GET', value, hs = headers) => new Promise((resolve, reject) => { ++ const req = request({ hostname: '127.0.0.1', port: s.address().port, path, method, headers: hs }, res => { ++ let text = ''; res.on('data', c => text += c); res.on('end', () => resolve({ status: res.statusCode, text })); ++ }); ++ req.on('error', reject); req.end(value === undefined ? undefined : JSON.stringify(value)); ++}); ++async function setup(t, options = {}) { ++ const calls = [], logs = []; ++ const s = await startServer({ port: 0, preview, send: async () => ({}), ++ proxyCall: async (verb, args) => { calls.push([verb, args]); return { ok: true }; }, ++ log: l => logs.push(l), ...options }); ++ t.after(() => new Promise(r => s.close(r))); ++ return { s, calls, logs }; ++} ++test('owner proxy routes each make exactly one allowlisted call', async t => { ++ const { s, calls, logs } = await setup(t); ++ for (const [path, method, verb, args] of routes) { ++ const r = await call(s, '/api/proxy/' + path, method, method === 'POST' ? args : undefined); ++ assert.equal(r.status, 200, path); ++ assert.deepEqual(calls.at(-1), [verb, args]); ++ } ++ assert.equal(calls.length, routes.length); ++ for (const value of ['p4-canary-key', 'fixture', 'redirect_url']) assert.equal(logs.join('\n').includes(value), false); ++}); ++for (const [name, hs, code] of [ ++ ['signed-out', { ...headers, 'x-user': 'other' }, 401], ++ ['loopback', { ...headers, host: '127.0.0.1' }, 403], ++ ['cross-origin', { ...headers, origin: 'https://other.example' }, 403], ++ ['missing Fetch Metadata', Object.fromEntries(Object.entries(headers).filter(([k]) => k !== 'sec-fetch-site')), 403], ++]) test(`proxy write refuses ${name}`, async t => { ++ const { s, calls } = await setup(t); ++ assert.equal((await call(s, '/api/proxy/apikey/add', 'POST', { provider: 'z.ai', key: 'fixture-key' }, hs)).status, code); ++ assert.deepEqual(calls, []); ++}); ++test('all proxy writes are read-only without --send', async t => { ++ const { s, calls } = await setup(t, { send: null }); ++ for (const [path, method, , args] of routes.filter(r => r[1] === 'POST')) ++ assert.equal((await call(s, '/api/proxy/' + path, method, args)).status, 403, path); ++ assert.deepEqual(calls, []); ++}); ++test('granted user never reaches owner page or any proxy route', async t => { ++ const sso = { publicHost: preview.host, publicOrigin: headers.origin, formTargets: () => [], ++ session: () => ({ name: 'Alice', user: 'alice' }) }; ++ const { s, calls } = await setup(t, { preview: null, sso, userOptions: () => { throw Error('must not bind'); } }); ++ assert.equal((await call(s, '/proxy')).status, 403); ++ for (const [path, method, , args] of routes) ++ assert.equal((await call(s, '/api/proxy/' + path, method, method === 'POST' ? args : undefined)).status, 403, path); ++ assert.deepEqual(calls, []); ++}); ++test('native page offers write-only key input and no panel script', async t => { ++ const { s } = await setup(t); ++ const r = await call(s, '/proxy'); ++ assert.equal(r.status, 200); ++ assert.match(r.text, /type="password"/); ++ assert.match(r.text, /autocomplete="off"/); ++ assert.doesNotMatch(r.text, /management\.html|iframe/); ++}); ++test('Console caller rejects every non-proxy verb', async () => { ++ const calls = []; ++ const o = consoleBus(async (...a) => { calls.push(a); return {}; }, { send: true }); ++ assert.equal(typeof o.proxyCall, 'function'); ++ await assert.rejects(o.proxyCall('message.send', {})); ++ assert.deepEqual(calls, []); ++}); ++ ++test('tester socket refuses proxy page, reads and writes before any owner call', async t => { ++ const { mkdtempSync, rmSync } = await import('node:fs'); ++ const { tmpdir } = await import('node:os'); ++ const { join } = await import('node:path'); ++ const { startTesterSocket } = await import('../src/tester-socket.mjs'); ++ const { testerRequest } = await import('../../cli/src/tester-http.mjs'); ++ const root = mkdtempSync(join(tmpdir(), 'p4-tester-')); ++ t.after(() => rmSync(root, { recursive: true, force: true })); ++ let calls = 0; ++ const s = await startTesterSocket({ dataRoot: root, proxyCall: async () => { calls++; return {}; }, log: () => {} }); ++ t.after(() => new Promise(r => s.close(r))); ++ const page = await testerRequest({ dataRoot: root, path: '/proxy' }); ++ assert.equal(page.status, 403); ++ for (const [path, method, , args] of routes) { ++ const r = await testerRequest({ dataRoot: root, path: '/api/proxy/' + path, method, body: method === 'POST' ? args : undefined }); ++ assert.equal(r.status, 403, path); ++ } ++ assert.equal(calls, 0); ++}); ++ ++test('malformed API-key JSON never appears in errors or logs', async t => { ++ const { s, calls, logs } = await setup(t); ++ const canary = 'p4-canary-key'; ++ const r = await new Promise((resolve, reject) => { ++ const req = request({ hostname: '127.0.0.1', port: s.address().port, path: '/api/proxy/apikey/add', method: 'POST', headers }, res => { ++ let text = ''; res.on('data', c => text += c); res.on('end', () => resolve({ status: res.statusCode, text })); ++ }); req.on('error', reject); req.end('{"key":"' + canary); ++ }); ++ assert.equal(r.status, 400); ++ assert.equal(r.text.includes(canary), false); assert.equal(logs.join('\n').includes(canary), false); ++ assert.deepEqual(calls, []); ++}); diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index 4d177c989..0291728c7 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -1696,3 +1696,4 @@ are never rewritten or removed; corrections are new entries. 2026-10-25 | Sage 1ef1e4f8 | row284 landing #1789 | Gate GREEN 00:09:04Z unchanged, landed ce0b6ed41 pushed 00:09:52Z; #1789 closed c31655; queue note 4071, done 4072 (7c8dc2033); DEFERRED Webby N1 to the next library read-path row; row 332 PROXY_CODEX_API ruled openai-responses (sage-4331), P3 holds for row 331. 2026-10-24 | poe | row 330 / #1828 Console proxy page, base 78b69e1e6 | Started requirements recovery and design; test slot held pending Sage CLEAR. 2026-10-25 | poe | row330 / #1828 authorized focused block | 88 focused tests green after expected account RED; offline acceptance failed before first account checkpoint, nice16 observed; stopped and returned slot to Sage, no rerun or mutants. +2026-10-25 | poe | row330 / #1828 approved test-first404 block | Intended RED observed, exact GET404 catch applied,99 focused passes, pinned acceptance and4mutants/controls passed; slot returned, review packet prepared, P2/full gate pending. diff --git a/packages/model-proxy/src/client.mjs b/packages/model-proxy/src/client.mjs index 7440bc3c7..c7d72f234 100644 --- a/packages/model-proxy/src/client.mjs +++ b/packages/model-proxy/src/client.mjs @@ -70,6 +70,9 @@ export class ManagementClient { const answer = await this.#request(method, endpoint, payload, value => { status = value; }); if (expectedStatus !== null && status !== expectedStatus) refuse('proxy-client-refused'); return answer; + } catch (error) { + if (method === 'GET' && endpoint === '/v8/management/config/api-keys/claude' && status === 404) return []; + throw error; } finally { this.#receipt({ call: `${method} ${endpoint.split('?')[0]}`, status, keyIds: [] }); } }