Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2986b9ead | ||
|
|
3df440d076 | ||
|
|
1aac8bcd7a | ||
|
|
69811540ce | ||
|
|
9e94575c5e | ||
|
|
a5251007bd | ||
|
|
4a2745a9da | ||
|
|
542b970d25 | ||
|
|
48c7c3283a | ||
|
|
40dcbabcbb |
No files matched your search
@@ -5585,3 +5585,9 @@ claim. Receipts and limitations in agents/lena/work/queue-296/BUILD.md and run-r
|
||||
2026-10-24T23:37:46Z | Sage | Row 329 (#1827, P1 pinned model proxy, Magica) LANDED 61ebf2c34, pushed 23:31:27Z 10-24. Before: refactor at 669cdf42c. After: Magica's 4 commits (94cde633e, c65fb34ee, d1f6f2b19, 61ebf2c34) cherry-picked onto 669cdf42c, diff vs the gated tree limited to drift paths, range-diff in scratch. Gate: one unchanged full landing gate in /mnt/storage/scratch/sage-r329l/wt (gate.sh 8b4978887 c8a9bcf94), started 23:08:12Z at load 14.35, GATE GREEN 23:29:14Z at load 8.67, no rerun, no test or timeout edit (mos-1634). Host note: two Codex workers cleaned T3 worktrees from about 23:14Z (mos-1639, Vikunja 632). Approvals: Dewey r2 c31643 rev 4047, Boyd r2 c31642 rev 4048. #1827 comment 31650, closed. Vikunja 237 comment 1109 (stays open for P2-P7); C4 P10 red filed as Vikunja 631. Queue: note rev 4063, done rev 4064, committed b24d917ec. DEFERRED: N2 and R2-N1 bound to row 331, N1 bound to P7. Overlap: row 284 (packages/bus/src/broker.mjs) and row 157 (scripts/mosaic-config.mjs) rebase onto 61ebf2c34 before their gates.
|
||||
2026-10-25T00:15:48Z | Sage | Row 284 (#1789, C2-05c library.get scope, Della) LANDED ce0b6ed41, pushed 00:09:52Z 10-25. Before: refactor at 9e2689dff (row 329 landing docs). After: fast-forward to Della's 7 rebased commits 78b932feb..ce0b6ed41; 8 paths, manifest-r4 de505cfd matches the landed tree. Gate: one unchanged full landing gate in /mnt/storage/scratch/sage-r284l/wt (the 329 gate.sh with D and the compose project renamed), started 23:51:19Z at load 1.62, GATE GREEN 00:09:04Z at load 5.20, no rerun, no test or timeout edit. Counts: bus 1020/0, business 283/0, cli 999 pass 1 allowed skip (FED R7), harness 177/0, library 156/0, seat 218/0, webui 458/0, every scripts/test-*.sh rc 0; queue 34 and task 87/93 skips are the standing ones. Della's whole-file runs on ce0b6ed41: library-lookups 12/0, library-tools 5/0, query 34/0. Approval: Webby round 2 delta c31654 rev 4070 (round 1 rev 4062). Overlap sweep: rows 13, 157, 309, gyro/row310 and fethry/row332-p1 clean. #1789 comment 31655, closed. No Vikunja task for the row. Queue: note rev 4071, done rev 4072, committed 7c8dc2033. DEFERRED: Webby N1 (whole-tree listing per get) bound to the next library read-path row; N2 needs nothing; N3 is this gate.
|
||||
2026-10-25T00:18:35Z | Sage | Follow-up tasks for mos-1647 item 3 (Q109): Vikunja 633 (P7 cutover, CLIProxyAPI 8.0.42 startup Grok-npm and antigravity version refreshes, from row 332 isolated runs) and 634 (row 284 Webby N1, library get lists the whole tree). DEFERRED gains the 633 entry and the 634 number for row 284 N1. Filing script and receipt in agents/sage/work/vikunja-setup/.
|
||||
|
||||
2026-10-24 Poe — row 330 / #1828 start: native Console proxy page and human-only bus verbs per Q250. Before: P1 client/config landed, no native page. Intended evidence: fake-management security/refusal tests, four named mutants, pinned offline upgrade acceptance, Gladstone/Filbert review. Tests remain held pending Sage CLEAR; P2 rebase required before gate. Notes: agents/poe/work/queue-330/BUILD.md.
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,453 @@
|
||||
# Row 330 / P4 Console proxy page — #1828
|
||||
|
||||
2026-10-24 Poe. Base: refactor 78b69e1e6; branch poe/row330.
|
||||
|
||||
## Requirements and boundaries
|
||||
|
||||
Replacement brief blob 4c005dc4936c, heading line 17. At 78b69e1e6 the
|
||||
0ce22dde6 line references remain unchanged: verbs 27–36; accounts 37–44;
|
||||
OAuth 45–61; upstream API keys 62–74; priority 75–78; client keys 79–83;
|
||||
usage 84–85; Console routes/page 86–98; upgrade check 99–110;
|
||||
tests 112–132; order 142–143; gate and named mutants 145–150.
|
||||
Rows document Rules for every row remain at line 12.
|
||||
|
||||
No live proxy/config/credential access. No host proxy calls. Work uses its own
|
||||
node_modules. No tests until Sage CLEAR; run list derived from imports before
|
||||
whole-file runs. P2 must land and be rebased before gate; P4 lands after P2.
|
||||
Gladstone first review; Filbert landing review. No refactor push authority.
|
||||
|
||||
## Proposed implementation contract
|
||||
|
||||
Verbs: proxy.accounts, proxy.login.start, proxy.login.submit,
|
||||
proxy.login.status, proxy.login.cancel, proxy.apikey.add,
|
||||
proxy.priority.set, proxy.keys. The broker checks human transport before
|
||||
configuration, including granted users (human-required). Missing proxy:
|
||||
not-configured. Management connection failure: proxy-down, no retry.
|
||||
|
||||
Accounts return only accountId, provider, displayName, status, priority.
|
||||
accountId hashes the proxy credential identifier (12 hex SHA256), never a token.
|
||||
Provider/status values must be constrained and names must be derived from safe
|
||||
metadata; arbitrary credential field values must not be reflected. Exact pinned
|
||||
8.0.42 endpoint/field mapping still to be confirmed on the offline stand-in.
|
||||
|
||||
Login sessions live in bus memory, scoped to the owning business/human, with
|
||||
300-second default expiry. A pasted URL is bounded, parsed and matched to a
|
||||
pending state started by this bus. Finished/expired/unknown state refuses
|
||||
login-state-unknown; malformed URL refuses callback-url-invalid. Only status ok
|
||||
finishes sign-in; callback acceptance/wait never does. No URLs, states or codes
|
||||
in logs/receipts/errors. Start allows claude/codex only; API-key add allows z.ai
|
||||
only. API keys are write-only printable ASCII, bounded, never echoed.
|
||||
|
||||
Owner-only /proxy page; /api/proxy/accounts and /api/proxy/keys reads;
|
||||
/api/proxy/login/start, /submit, /status, /cancel; /api/proxy/apikey/add;
|
||||
/api/proxy/priority/set. Each maps to exactly one allowlisted bus call.
|
||||
Writes require named host, owner sign-in, own Origin, same-origin Fetch Metadata
|
||||
and --send. Tester, granted user and loopback refuse. Console holds no client,
|
||||
proxy settings or management key. Usage placeholder accepts P5's later view;
|
||||
P4 creates no usage source.
|
||||
|
||||
## Test plan (not yet run)
|
||||
|
||||
Management client: pinned endpoint/method/body contract, loopback-only auth,
|
||||
redirect refusal, bounded response, safe receipts, no retry.
|
||||
Bus: all eight verbs against fake server; separate refusal cases for missing
|
||||
config, tester/agent/granted user human-required, both provider allowlists,
|
||||
unknown/expired/finished state, invalid callback URL, each API-key invalid case,
|
||||
invalid priority/account id, and proxy-down. Explicit wait-is-not-ok test.
|
||||
Accounts: strict allowlist, ids from identifier, toxic-field fake, API-key account
|
||||
and priority round-trip. Keys: ids only, sentinel and foreign-key labels, P2 join.
|
||||
Console: owner success; granted/tester/loopback/signed-out/cross-origin/missing
|
||||
Fetch Metadata refusals; every write refuses without --send; caller allowlist;
|
||||
no webui import of management client or proxy config access.
|
||||
Canary: fake management receives API key exactly once, and all responses, logs,
|
||||
receipts, errors and store files contain zero copies. OAuth secrets likewise.
|
||||
Named mutants: unfiltered account field; skipped callback state check; granted
|
||||
user accepted at /api/proxy/; API-key echoed by error/log.
|
||||
Full model-proxy, bus, webui (MOSAIC_REQUIRE_EDITOR_BUILD=1), touched test files;
|
||||
Sage landing gate all packages except queue and scripts/test-*.sh. Upgrade check
|
||||
under unshare -rn on PIN 8.0.42; output must land here. No runs claimed yet.
|
||||
|
||||
## Design clarification (Sage sage-4347)
|
||||
|
||||
Committed researcher/P1 artifacts may be read in this checkout. Read v8.0.27
|
||||
management reference and P1 BUILD/REVIEW. P1 confirms 8.0.42 key list is an array
|
||||
and OAuth redirect URLs localhost:54545/callback (Claude) and
|
||||
localhost:1455/auth/callback (Codex). P1 does not record credential response
|
||||
fields, z.ai upstream config schema or credentials/fields PATCH payload.
|
||||
Those remain unconfirmed, not guessed as a pinned contract.
|
||||
|
||||
Expiry behavior: status for an expired pending state returns
|
||||
{status:"error", reason:"login-timeout"}, deletes the state and makes no proxy
|
||||
call. It is not signed in; later status/submit/cancel refuses login-state-unknown.
|
||||
No code or upstream error body is returned. Timeout can be provided as a trusted
|
||||
host setting, default300s; no page/agent verb may widen it.
|
||||
|
||||
Setup: npm ci --ignore-scripts --no-audit --no-fund completed in worktree root
|
||||
(128 packages) and packages/webui (2 packages), both launched at nice19 with
|
||||
fresh shell-created short TMPDIR. No canonical npm or node_modules links.
|
||||
Canonical queue next completed with view-unknown warning; move returned
|
||||
ok poe-330-start rev4077 row330 briefed→in-progress. No repair performed.
|
||||
|
||||
## Pre-run list (no execution)
|
||||
|
||||
Static esbuild import parsing of the initial RED entrypoints (no tests executed)
|
||||
produced red-import-graph.json and red-run-list.txt. Whole-file list:
|
||||
packages/model-proxy/tests/page-client.test.mjs;
|
||||
packages/bus/tests/proxy-page.test.mjs;
|
||||
packages/bus/tests/proxy-page-state.test.mjs;
|
||||
packages/webui/tests/proxy-page.test.mjs.
|
||||
The new state test's dynamic URL import points at the not-yet-implemented
|
||||
packages/bus/src/proxy-page.mjs; expected first RED is missing implementation.
|
||||
No case1-accept file is selected. Probe.sh is also slot work, on a random
|
||||
loopback port inside unshare-rn, using only the verified own fixture executable.
|
||||
No test or binary probe has run yet. The three open pinned contracts remain
|
||||
probe-pending. Priority tests currently choose integer range -1000..1000.
|
||||
|
||||
Sage sage-4352 authorizes P1 fetch into this fresh fixture. Fetch succeeded at
|
||||
nice19, verified archive, executable and panel hashes against PIN8.0.42, and
|
||||
installed only under /mnt/storage/scratch/tmp/76TDtz/download/opt/cliproxyapi/8.0.42.
|
||||
Receipt fetch.txt contains paths only. No host executable was used or inspected.
|
||||
|
||||
2026-10-24T04:04Z: node --check on all four new test files and probe.mjs,
|
||||
bash -n probe.sh, and git diff --check passed. Syntax checks only: no test
|
||||
module or pinned executable ran. No production implementation changed yet;
|
||||
RED-first requirement and Sage CLEAR slot gate remain in force. Canonical
|
||||
queue start is complete; waiting for the authorized test slot, not a Jason
|
||||
decision. No commit or push made.
|
||||
|
||||
## First RED and pinned contract probe (sage-4391 CLEAR)
|
||||
|
||||
2026-10-24T04:24Z. Four whole files ran serially, absolute paths, a fresh
|
||||
shell-created short TMPDIR per run, nice19 recorded in each receipt. No old
|
||||
test file ran and no test was rerun. All four exited1 as expected:
|
||||
|
||||
- page-client: 0pass/3fail. OAuth receipt test fails because oauthStart is
|
||||
undefined; upstream refusal/canary test because putUpstreamGroups is undefined;
|
||||
redirect test because getCredentials is undefined. Receipt red-page-client.txt.
|
||||
- broker proxy-page: 0pass/41fail. For each of the eight verbs, the owner gets
|
||||
unknown-verb instead of a page response, tester gets tester-refused instead of
|
||||
human-required, agent gets unknown-verb instead of human-required, granted user
|
||||
gets access-refused instead of human-required, and missing configuration gets
|
||||
unknown-verb instead of not-configured. Canary error test gets unknown-verb
|
||||
instead of proxy-down. All are the missing P4 dispatch boundary, not a fixture
|
||||
setup failure. Receipt red-broker.txt enumerates every failing test.
|
||||
- proxy-page-state: 0pass/17fail. Every named provider/key/callback/state/expiry/
|
||||
cancellation/priority case fails ERR_MODULE_NOT_FOUND for the not-yet-created
|
||||
packages/bus/src/proxy-page.mjs. Receipt red-state.txt enumerates each case.
|
||||
- webui proxy-page: 6pass/3fail. Owner routes return404; native page returns404;
|
||||
Console caller proxyCall is undefined. Signed-out, loopback, cross-origin,
|
||||
absent Fetch Metadata, no-send and granted-user boundary cases pass through
|
||||
existing general refusals. Receipt red-webui.txt.
|
||||
|
||||
probe.sh exited0 under unshare-rn, random loopback port, own config and made-up
|
||||
short key, verified PIN8.0.42 binary, nice19. Child exited and namespace closed.
|
||||
No host proxy or real instance state was used. probe.txt is shape-only output:
|
||||
|
||||
- GET /credentials:200 {files:[], observed_at:string}.
|
||||
- GET /oauth/auth-url?provider=claude and provider=codex:200 with url/state/status
|
||||
strings. DELETE /oauth/session?state=[redacted]:200 cancelled/status.
|
||||
- PUT /config/api-keys/claude with
|
||||
[{name,base-url,priority,keys:[{api-key}]}]:200 status/config-version.
|
||||
Used z.ai's Claude-compatible endpoint; no request reached that endpoint.
|
||||
- GET /config/api-keys/claude: array of groups, name/base-url/priority; keys
|
||||
include api-key and a proxy-generated auth_index. Values were not printed.
|
||||
- GET /credentials after API-key add remains files:[]: API-key credentials are
|
||||
not listed here. Accounts must join the provider config's safe metadata.
|
||||
- The script's priority PATCH branch required a credential row; no row existed,
|
||||
so it did NOT run. Credentials field payload and populated OAuth response fields
|
||||
remain unconfirmed. Reported rather than treating rc0 as full contract proof.
|
||||
|
||||
Slot returned to Sage after this report. GREEN, mutants and further binary probes
|
||||
require separate CLEAR. No implementation run, commit or push claimed.
|
||||
|
||||
## Probe 2 observed contract (sage-4394 CLEAR)
|
||||
|
||||
2026-10-24T04:25Z. probe2.sh exited0 under unshare-rn, own random loopback
|
||||
port/config/short made-up key, nice19. Fixture child exited; EXIT trap removed
|
||||
the entire shell-created fixture TMPDIR, including fabricated credential/config.
|
||||
Only verified fetched binary outside this temporary runtime root was retained.
|
||||
Receipt probe2.txt contains types/shapes only, no credential values.
|
||||
|
||||
POST /v8/management/credentials?name=[fixture filename] with JSON
|
||||
{type,access_token,refresh_token,expired,email} returns200 {status:string}.
|
||||
No multipart fallback was needed. Populated GET /credentials returns
|
||||
{files:[...],observed_at:string}. Credential row fields/types:
|
||||
account/string, account_type/string, auth_index/string, cooldowns/array,
|
||||
created_at/string, disabled/boolean, email/string, failed/number, id/string,
|
||||
label/string, modtime/string, name/string, path/string, provider/string,
|
||||
quota/object, recent_requests/array, runtime_only/boolean, size/number,
|
||||
source/string, status/string, status_message/string, success/number,
|
||||
type/string, unavailable/boolean, updated_at/string. priority/number appears
|
||||
after setting it. Fields other than the explicit public projection are dropped.
|
||||
|
||||
PATCH /credentials/fields observations:
|
||||
- {id,priority}400; {id,fields:{priority}}400.
|
||||
- {name:<OAuth id>,priority}200; {name:<OAuth name>,priority}200.
|
||||
- {name:<OAuth name>,fields:{priority}}200, but200 alone does not prove it changed
|
||||
priority (this may be an ignored unknown field); use top-level priority.
|
||||
- {auth_index,priority}400; {auth_index,fields:{priority}}400 for both OAuth
|
||||
and API-key identifiers.
|
||||
- {name:<auth_index>,priority}404 for both OAuth and API key.
|
||||
Final OAuth list has numeric priority and changed to one of the requested values.
|
||||
API-key group priority did not change. Consequently priority uses top-level
|
||||
{name,priority} for OAuth files; API-key priority must update its provider config,
|
||||
not credentials/fields. A future upgrade acceptance must verify both read-backs.
|
||||
|
||||
Slot returned after probe2. No GREEN or mutant run authorized yet.
|
||||
|
||||
## Candidate implementation (not GREEN-verified)
|
||||
|
||||
Written after the first RED run: fixed client operations and receipts, human-only
|
||||
broker dispatch before generic tester/user gates, bus-owned pending-state machine,
|
||||
strict public account projection, native owner-only HTTP routes and /proxy page.
|
||||
No management client/config access in the Console's new code. Reads use the owner
|
||||
human transport and writes independently require --send/Origin/Fetch Metadata.
|
||||
|
||||
Generated display names use the constrained provider and credential-id hash,
|
||||
rather than upstream label/email/group-name strings. This is a consequential
|
||||
security/usability tradeoff: the toxic-all-fields fake cannot reflect a credential,
|
||||
but the list identifies accounts by stable ids rather than email addresses.
|
||||
|
||||
P2 join remains provisional until P2 lands and this branch rebases. Current
|
||||
runtime seam projects model status metadata by business and does not read key
|
||||
files. New refusal tests for priority-unconfirmed (read-back mismatch) and
|
||||
priority-write-refused (PATCH other than200) are written FIRST; their source fixes
|
||||
await the next authorized first RED run. Provider config read-only auth_index
|
||||
must not be echoed in a config write; its preservation test is also pending.
|
||||
|
||||
Sage sage-4394 explicitly authorizes committing/pushing incremental candidate
|
||||
commits before GREEN on poe/row330; refactor landing remains gated on all green
|
||||
checks and reviews. No candidate commit is a verification/completion claim.
|
||||
|
||||
## Prepared next slot (not run)
|
||||
|
||||
Sage sage-4400 accepts the two priority paths. New code-specific first-RED tests
|
||||
are in proxy-page-accounts.test.mjs for priority-unconfirmed and
|
||||
priority-write-refused. This file must run first, then those source fixes may
|
||||
be written; no assertion is loosened. No second RED has occurred.
|
||||
|
||||
Focused list is eight whole files in focused-run-list.txt, generated by esbuild's
|
||||
import parser (focused-import-graph.json). Additional tests exercise actual
|
||||
Chromium UI behavior and the parsed Console import boundary. The canary now
|
||||
intercepts process stdout/stderr during the API-key operation, as well as
|
||||
structured HTTP/client logs and durable bus files. Deliberate log mutants cannot
|
||||
print their made-up key. Four slot-only named mutants are prepared in mutants.mjs;
|
||||
no mutant has executed. The upgrade check is prepared in acceptance.sh/mjs;
|
||||
it has not run. New refusal source fixes remain pending first RED.
|
||||
|
||||
Broader import inventory: gate-imports.mjs statically parses package test imports,
|
||||
traces paths to changed production modules, and adds all mandatory model-proxy,
|
||||
bus and webui test files. Exact paths are gate-run-list.txt; summary includes
|
||||
additional collateral files. case1-accept.test.mjs is explicitly excluded even
|
||||
if imported, per mos-1612. No text search of fixture payloads selects a run.
|
||||
Webui's eventual full gate requires editor build and MOSAIC_REQUIRE_EDITOR_BUILD=1.
|
||||
These inventory commands parse source only; they do not import/execute tests.
|
||||
|
||||
Sage-authorized incremental candidate40dcbabcb pushed successfully to poe/row330.
|
||||
No PR or review request yet, no refactor modification/push, no live change.
|
||||
|
||||
2026-10-24T04:42Z refactor input check requested by sage-4411:
|
||||
`git diff --name-only 78b69e1e6 775bbe236` names45 paths; intersection with
|
||||
focused-import-graph.json's111 parsed inputs is empty. Exact receipts:
|
||||
refactor-775bbe236-paths.txt and refactor-775bbe236-input-check.json.
|
||||
|
||||
Correction to the shorthand report poe-330-0011: the generated broader inventory
|
||||
contains280 selected files, not281. Mandatory199 (model-proxy/bus/webui), plus81
|
||||
additional import dependents. case1-accept remains excluded. Preserve the exact
|
||||
list, not the mistaken shorthand count. The large full graph is kept off Git;
|
||||
gate-import-summary.json cites its scratch path and SHA256. A compact per-file
|
||||
import witness and the reproducible parser script land instead.
|
||||
|
||||
Sage sage-4411 accepts first-RED account test → source fix → eight focused whole
|
||||
files → offline acceptance → four mutants. No CLEAR yet: slot order is Bubba288,
|
||||
Magica331 GREEN, then this block. Full gate follows P2 landing/rebase/key join.
|
||||
|
||||
## Focused block sage-4430 — stopped at offline acceptance failure
|
||||
|
||||
2026-10-25T05:39Z precheck on candidate48c7c3283:
|
||||
`git diff --name-only 78b69e1e6 e1d8756b5` names47 paths; none overlaps the111
|
||||
focused parsed inputs. refactor-e1d8756b5-input-check.json records the check.
|
||||
|
||||
First RED of proxy-page-accounts.test.mjs:6pass/4expected failures, rc1. Failed
|
||||
API priority write included derived auth_index; mismatch refusal was proxy-down;
|
||||
PATCH201 and400 refusals were proxy-down. Receipt red-accounts.txt.
|
||||
|
||||
Source fix only, no test edits: remove derived auth_index from provider config
|
||||
writes; require exactly200 for priority PATCH/PUT; preserve priority-write-refused
|
||||
for refused/redirected writes and priority-unconfirmed for a mismatching read-back.
|
||||
Console forwards those safe refusal codes. No raw errors/keys introduced.
|
||||
|
||||
Eight focused whole files then passed88/88 with0skip, serialized, absolute paths,
|
||||
one shell-created short TMPDIR per run. Every shell wrapper recorded nice19:
|
||||
page-client3; broker41; state17; accounts10; webui HTTP11; canary2; browser3;
|
||||
architecture1. Receipts green-*.txt. These are focused checks, not full suites
|
||||
or landing evidence. No old test file ran and no assertion/timeout changed.
|
||||
|
||||
Offline acceptance.sh on the same own PIN8.0.42 fetched executable exited1.
|
||||
Raw receipt upgrade-8.0.42.txt is only:
|
||||
|
||||
P4 offline upgrade version=8.0.42 nice=16
|
||||
P4 upgrade FAILED code=acceptance-failed
|
||||
|
||||
This was the FIRST unexpected red. It occurred before the initial empty-accounts
|
||||
checkpoint. The catch intentionally strips exception detail, so the failed
|
||||
assertion/location is not known. Node reported16 despite the absolute19 shell
|
||||
wrapper; no claim that the binary acceptance ran at19. The private namespace
|
||||
child was stopped and the shell trap deleted its own fixture root as designed.
|
||||
No host proxy, real key or real data root was involved.
|
||||
|
||||
Stopped immediately and reported poe-330-0014 to Sage; slot returned for
|
||||
Magica331r2. No rerun, post-failure source fix, test edit or timeout change.
|
||||
Four mutants were NOT RUN. Before this failure, their runner was prepared to
|
||||
restore each source and run its whole-file positive control at nice19.
|
||||
Next acceptance diagnosis needs Sage's ruling/CLEAR. Full suites, P2 rebase/key
|
||||
join, mutants and Gladstone/Filbert reviews remain outstanding.
|
||||
|
||||
## Diagnostic-only preparation — sage-4432
|
||||
|
||||
2026-10-25T05:45Z. Prepared a separate acceptance.mjs diagnostic, with no run:
|
||||
fixed stage labels binary-start, management-ready, store-open, broker-bound,
|
||||
initial-accounts; observed Node/own fixture-child priorities; whitelisted last
|
||||
management client failure code; child exit code/signal; sanitized catch class/code.
|
||||
No exception message, actual/expected, request/response body, URL or key is printed.
|
||||
A child-exit observation reads no priority once that child is known to have exited.
|
||||
|
||||
No priority setter was added. The shell wrapper and every assertion, retry count,
|
||||
pause and timeout remain unchanged, as required to repeat the failed conditions.
|
||||
Static comparison confirms assertion lines and timeout expressions unchanged;
|
||||
git diff --check passes. No acceptance, test, proxy binary or mutant executed.
|
||||
This is a diagnostic commit only, not a product fix. Slot remains with Sage;
|
||||
next run waits for CLEAR after288 gate rerun and Magica331r2. The full focused /
|
||||
acceptance / mutant block after diagnosis still requires Sage's CLEAR.
|
||||
|
||||
## Scheduling observation preparation — sage-4438 / mos-1675
|
||||
|
||||
2026-10-25T06:09Z. Slot order changed: Magica331r2, Sage's short288 diagnostic,
|
||||
then this row's diagnostic; wait for Sage CLEAR. Added only numeric scheduling
|
||||
policy observations from /proc/<own Node pid>/sched and the still-live fixture
|
||||
child's sched file. No raw proc content, other process, scheduling setter or
|
||||
host configuration access/change. Diagnostic module not executed.
|
||||
|
||||
Context supplied by Sage (not independently measured here): ananicy-cpp's node
|
||||
rule assigns BG_CPUIO (nice16, sched idle, ioclass idle), and bash's Doc-View rule
|
||||
sets nice-4. Sage reports local ASR/ffmpeg load05:41–05:50Z overlapping the failed
|
||||
acceptance. These facts explain why a launch wrapper's19 is insufficient proof
|
||||
of the Node process's later policy. Causation of the acceptance failure remains
|
||||
unestablished until the authorized stage diagnostic. No assertion, timeout,
|
||||
product code, fixture condition or priority/policy setter changed.
|
||||
|
||||
## One diagnostic reproduction — sage-4440 CLEAR
|
||||
|
||||
2026-10-25T06:14:19Z–06:14:23Z. Ran acceptance.sh exactly once on unchanged
|
||||
candidatea5251007b, with no source/assertion/timeout/priority edits in the slot.
|
||||
Start uptime/load:11 days16:34,2.90/5.40/9.26. End:11 days16:34,3.15/5.41/9.24.
|
||||
Receipts diagnostic-start-uptime.txt, diagnostic-8.0.42.txt and
|
||||
diagnostic-end-uptime.txt preserve exact output.
|
||||
|
||||
Reproduced rc1. Reached binary-start, management-ready, store-open, then:
|
||||
{stage:store-open,errorClass:Error,code:acceptance-failed}. Did not reach
|
||||
broker-bound or initial-accounts. Store-open is observed AFTER Store construction;
|
||||
remaining interval includes ProxyPage/Broker construction and human binding.
|
||||
No failed assertion location or root cause is claimed from the sanitized code.
|
||||
Readiness succeeded. lastClientErrorCode proxy-client-down records an earlier
|
||||
transient startup refusal, not a readiness failure.
|
||||
|
||||
Node and the live fixture child both observed nice16 and numeric scheduling
|
||||
policy5 at all checkpoints. This establishes scheduling conditions, not cause.
|
||||
Cleanup observed fixture child exit0 with no signal; its private temp root was
|
||||
removed by the existing trap. No live proxy/key/data access occurred.
|
||||
|
||||
Stopped after that one run and reported poe-330-0019; SLOT BACK to Sage for288
|
||||
whole-gate rerun, then Magica331r3. No focused suites, mutants or second diagnostic.
|
||||
No product or diagnostic source change after this result. Receipt-only commit
|
||||
follows outside the returned slot; next investigative step awaits Sage's ruling.
|
||||
|
||||
## Source-established fixture correction — sage-4442 option1
|
||||
|
||||
2026-10-25T06:16Z, source reading only, no run. Root cause: acceptance.mjs:74
|
||||
(at9e94575c5) passes path.join(root,'bus') to new Store. Fixture preparation at
|
||||
:24–:27 creates root/proxy/auth, root/home and config.yaml, but no root/bus.
|
||||
packages/bus/src/store.mjs:163 immediately lstatSync(dataRoot), requiring an
|
||||
existing directory; :166 then creates its OWN bus child. Thus this acceptance
|
||||
passes a nonexistent data root and gets filesystem ENOENT before Store can open.
|
||||
ENOENT was not in the diagnostic allowlist and was sanitized to acceptance-failed.
|
||||
The smallest fix is new Store(root), the existing fresh fixture data root,
|
||||
matching the focused fixtures. No production behavior or authority changes.
|
||||
|
||||
Correction to poe-330-0019 and the prior diagnostic interpretation: the
|
||||
store-open observation does NOT prove successful Store construction. observe()
|
||||
in the catch emits the same stage, so the observed line can be the catch's line.
|
||||
The successful management-ready checkpoint remains valid; there was no later
|
||||
broker-bound. Our earlier post-Store inference was wrong. Source resolves this
|
||||
without another diagnostic: the missing fixture directory explains the precise
|
||||
failure and its sanitized Error/code independently of scheduling/load.
|
||||
|
||||
Also read ProxyPage constructor validation (:27–:30), Broker business validation
|
||||
(:373–:389, :558), and bindHuman (:1482–:1487). The page settings, business id/
|
||||
human/authority/arbiters and the CLI/outsideAgent:true record meet those checks.
|
||||
No human-boundary workaround is needed or made.
|
||||
|
||||
Prepared a separate single-line acceptance fixture fix, plus this append-only
|
||||
explanation. No assertion, timeout, diagnostic extension, product change or
|
||||
priority/policy setter. No diagnostic, acceptance, test or mutant execution;
|
||||
next run waits for Sage CLEAR after the288 gate.
|
||||
|
||||
## Full focused block retry — sage-4446 CLEAR, stopped at initial accounts
|
||||
|
||||
2026-10-25T06:41Z–06:42Z on unchanged69811540c. All eight focused files ran
|
||||
whole, serialized with fresh short TMPDIR and absolute nice19 wrappers;88/88
|
||||
passed,0skip. Each step has UTC timestamp and uptime start/end in
|
||||
block2-<test-path>.txt.start/.end, with complete test receipt block2-*.txt.
|
||||
No old test file, assertion edit or timeout change.
|
||||
|
||||
Offline acceptance started06:42:18Z and ended06:42:22Z, uptime11days17:02,
|
||||
load5.98/15.98/16.44 both endpoints. Reached management-ready, store-open,
|
||||
broker-bound, then initial-accounts. Failed rc1 with code proxy-down,
|
||||
errorClass Error, before the initial-empty assertion checkpoint. Thus the
|
||||
previous missing Store root failure is traversed, but the native account-list
|
||||
boundary now refuses. No root-cause inference or source fix yet.
|
||||
|
||||
Node observed nice19/policy0 through the initial-accounts entry, and nice16/
|
||||
policy5 at the failure. Fixture child observed nice19/policy0 until cleanup,
|
||||
then cleanexit0 with no signal. Idle scheduling is accepted under mos-1678;
|
||||
these observations do not establish a scheduling cause. The remembered
|
||||
proxy-client-down remains the earlier transient readiness failure.
|
||||
|
||||
Stopped at this FIRST unexpected red and reported poe-330-0022, SLOT BACK for
|
||||
Magica331r3. No acceptance rerun, focused-file retry, product/fixture edit or
|
||||
mutant execution. New receipt block2-acceptance.txt preserves sanitized output;
|
||||
private fixture root removed by the existing trap. Receipt-only commit follows;
|
||||
next step requires Sage's ruling/CLEAR. Full gate/P2 join/reviews still pending.
|
||||
|
||||
## Empty-provider source reading and diagnostic — sage-4448/4451/4452
|
||||
|
||||
2026-10-25T06:47Z, no run. Available committed upstream reference is8.0.27,
|
||||
not a pinned8.0.42 implementation source. management-api-v8.0.27.md:25–27 says
|
||||
GET does not add omitted runtime defaults; it does not specify the HTTP status
|
||||
or body shape for a missing provider group. P1 and our previous probes measured
|
||||
provider GET only after adding a group. Baseline-empty shape remains unproven.
|
||||
|
||||
page-client.test.mjs:7–14 defaults to200/{status:ok}, but no case calls
|
||||
getUpstreamGroups. The actual accounts fake proxy-page-fixture.mjs:7,:18–19
|
||||
returns200/[] for no groups. If the real binary returns another valid shape,
|
||||
client.mjs/proxy-page.mjs has a product contract finding, not a fixture bug.
|
||||
Reported to Sage before making this diagnostic; no product fix is assumed.
|
||||
|
||||
Prepared a separate diagnostic-only change: immediately before initial account
|
||||
assertion, call client.getUpstreamGroups once directly and print only HTTP
|
||||
status, type, array flag and count (array length or object top-level key count).
|
||||
Per narrower sage-4452/mos-1683 it prints NO key names and NO values. Null is
|
||||
reported as type null/count0; no response body is printed. On client error only
|
||||
a fixed allowlisted client code is printed, never a raw error/message/body.
|
||||
Status comes from the existing safe management receipt, not another request.
|
||||
|
||||
No assertions, timeouts, retries, scheduling settings, product source or test
|
||||
fixtures changed. No module/test/binary/diagnostic executed. Wait for Sage's
|
||||
review and CLEAR after the288 processing diagnostic before one acceptance run.
|
||||
|
||||
2026-10-25T06:49Z sage-4454 review addition: diagnostic error output now includes
|
||||
httpStatus from the same existing receipt written by ManagementClient finally,
|
||||
using the identical receipt-count guard as success. No new request or raw
|
||||
exception data; this distinguishes404 from5xx without names or values. Separate
|
||||
one-line diagnostic commit, no module/test/binary execution. Hold for CLEAR after
|
||||
Sage's288 processing pair; no independent load-based run decision.
|
||||
@@ -0,0 +1,157 @@
|
||||
// Offline upgrade contract: real bus verbs, pinned proxy, no real credentials.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { getPriority } from 'node:os';
|
||||
import { initialConfig } from '../../../../packages/model-proxy/src/settings.mjs';
|
||||
import { PIN } from '../../../../packages/model-proxy/src/common.mjs';
|
||||
import { ManagementClient } from '../../../../packages/model-proxy/src/client.mjs';
|
||||
import { ProxyPage } from '../../../../packages/bus/src/proxy-page.mjs';
|
||||
import { Broker } from '../../../../packages/bus/src/broker.mjs';
|
||||
import { Store } from '../../../../packages/bus/src/store.mjs';
|
||||
const [root, binary] = process.argv.slice(2);
|
||||
if (process.env.P4_UPGRADE_ISOLATED !== 'yes' || !root?.startsWith('/mnt/storage/scratch/tmp/')) throw Error('wrapper required');
|
||||
if (createHash('sha256').update(fs.readFileSync(binary)).digest('hex') !== PIN.binary.sha256) throw Error('binary does not match PIN');
|
||||
const reserve = net.createServer(); await new Promise(r => reserve.listen(0, '127.0.0.1', r));
|
||||
const port = reserve.address().port; await new Promise(r => reserve.close(r));
|
||||
if (port === 8317) throw Error('forbidden port');
|
||||
const management = 'p4-management', canary = 'p4-upstream';
|
||||
const config = initialConfig({ dataRoot: root, port, key: management, sentinel: 'msk-fixture-sentinel-x' });
|
||||
config.management['disable-control-panel'] = true; config.observability.logs['logging-to-file'] = false;
|
||||
fs.mkdirSync(path.join(root, 'proxy/auth'), { recursive: true, mode: 0o700 });
|
||||
fs.mkdirSync(path.join(root, 'home'));
|
||||
fs.writeFileSync(path.join(root, 'config.yaml'), JSON.stringify(config), { mode: 0o600 });
|
||||
const child = spawn(binary, ['-config', path.join(root, 'config.yaml'), '-local-model'], {
|
||||
cwd: root, env: { HOME: path.join(root, 'home'), PATH: '/usr/bin:/bin' }, stdio: 'ignore',
|
||||
});
|
||||
// Diagnostic-only (sage-4432): observe the same run conditions, never change
|
||||
// priority or print exception values, fixture credentials or upstream content.
|
||||
let stage = 'binary-start', lastClientErrorCode = null;
|
||||
const clientErrorCodes = ['proxy-client-down', 'proxy-client-response', 'proxy-client-refused',
|
||||
'proxy-client-redirect', 'proxy-client-invalid'];
|
||||
const errorCodes = [...clientErrorCodes, 'ERR_ASSERTION', 'ERR_SQLITE_ERROR', 'SQLITE_ERROR',
|
||||
'proxy-down', 'not-configured', 'human-required', 'invalid-request', 'invalid-business',
|
||||
'priority-invalid', 'priority-unconfirmed', 'priority-write-refused', 'login-state-unknown'];
|
||||
function schedulingPolicy(pid) {
|
||||
try {
|
||||
// Only our Node process or the still-live child we spawned. Never print
|
||||
// raw /proc content; retain just Linux's numeric scheduling-policy field.
|
||||
const policy = fs.readFileSync(`/proc/${pid}/sched`, 'utf8').match(/^policy\s*:\s*(\d+)\s*$/m)?.[1];
|
||||
return policy === undefined ? null : Number(policy);
|
||||
} catch { return null; }
|
||||
}
|
||||
function observe() {
|
||||
let nodePriority = null, childPriority = null, childSchedulingPolicy = null;
|
||||
try { nodePriority = getPriority(); } catch {}
|
||||
if (Number.isInteger(child.pid) && child.exitCode === null && child.signalCode === null) {
|
||||
try { childPriority = getPriority(child.pid); } catch {}
|
||||
childSchedulingPolicy = schedulingPolicy(child.pid);
|
||||
}
|
||||
console.log('P4 diagnostic ' + JSON.stringify({ stage, nodePriority, childPriority, lastClientErrorCode,
|
||||
nodeSchedulingPolicy: schedulingPolicy(process.pid), childSchedulingPolicy,
|
||||
childExitCode: Number.isInteger(child.exitCode) ? child.exitCode : null,
|
||||
childSignal: /^SIG[A-Z0-9]{1,16}$/.test(child.signalCode ?? '') ? child.signalCode : null }));
|
||||
}
|
||||
child.once('exit', observe);
|
||||
let store;
|
||||
const pause = ms => new Promise(r => setTimeout(r, ms));
|
||||
const receipts = [], answers = [];
|
||||
const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${port}`, key: management, receipt: r => receipts.push(r) });
|
||||
try {
|
||||
console.log('P4 offline upgrade version=' + PIN.version + ' nice=' + getPriority());
|
||||
observe();
|
||||
let ready = false;
|
||||
for (let n = 0; n < 100; n++) {
|
||||
try { await client.getCredentials(); ready = true; break; }
|
||||
catch (error) { lastClientErrorCode = clientErrorCodes.includes(error.code) ? error.code : 'unclassified'; await pause(50); }
|
||||
}
|
||||
assert.equal(ready, true, 'fixture listener starts');
|
||||
stage = 'management-ready'; observe();
|
||||
stage = 'store-open';
|
||||
store = new Store(root);
|
||||
observe();
|
||||
const page = new ProxyPage({ client, instanceTag: 'fixture' });
|
||||
const broker = new Broker({ store, proxyPage: (...args) => page.request(...args), businesses: {
|
||||
demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } },
|
||||
} });
|
||||
const cap = broker.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
|
||||
stage = 'broker-bound'; observe();
|
||||
const call = async (verb, args = {}) => { const answer = await broker.request(cap, { verb, args }); answers.push(answer); return answer; };
|
||||
stage = 'initial-accounts'; observe();
|
||||
// Diagnostic-only (sage-4448/4452, mos-1683): one direct empty-provider
|
||||
// read, types/counts only. Never print field names, values or error text.
|
||||
const receiptCount = receipts.length;
|
||||
try {
|
||||
const groups = await client.getUpstreamGroups();
|
||||
const isArray = Array.isArray(groups);
|
||||
console.log('P4 empty-provider diagnostic ' + JSON.stringify({
|
||||
httpStatus: receipts.length === receiptCount + 1 ? receipts.at(-1).status : null,
|
||||
type: groups === null ? 'null' : typeof groups, isArray,
|
||||
count: isArray ? groups.length : groups !== null && typeof groups === 'object' ? Object.keys(groups).length : 0,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.log('P4 empty-provider diagnostic ' + JSON.stringify({
|
||||
httpStatus: receipts.length === receiptCount + 1 ? receipts.at(-1).status : null,
|
||||
code: clientErrorCodes.includes(error.code) ? error.code : 'proxy-client-error',
|
||||
}));
|
||||
}
|
||||
assert.deepEqual((await call('proxy.accounts')).accounts, []);
|
||||
console.log('proxy.accounts initial empty; GET /credentials + GET /config/api-keys/claude');
|
||||
for (const provider of ['claude', 'codex']) {
|
||||
const login = await call('proxy.login.start', { provider });
|
||||
assert.equal(typeof login.url, 'string'); assert.equal(typeof login.state, 'string');
|
||||
console.log('proxy.login.start ' + provider + ' GET /oauth/auth-url; fields=url,state only');
|
||||
const redirect = provider === 'claude' ? 'http://localhost:54545/callback' : 'http://localhost:1455/auth/callback';
|
||||
const submitted = await call('proxy.login.submit', { redirect_url: redirect + '?state=' + encodeURIComponent(login.state) + '&code=p4-fake-code' });
|
||||
assert.notEqual(submitted.status, 'ok');
|
||||
const status = await call('proxy.login.status', { state: login.state });
|
||||
assert.notEqual(status.status, 'ok');
|
||||
console.log('proxy.login.submit POST /oauth/callback; fake code never ok; GET /oauth/status=' + status.status);
|
||||
if (status.status === 'wait') {
|
||||
assert.equal((await call('proxy.login.cancel', { state: login.state })).status, 'cancelled');
|
||||
console.log('proxy.login.cancel DELETE /oauth/session confirmed');
|
||||
}
|
||||
}
|
||||
await call('proxy.apikey.add', { provider: 'z.ai', key: canary }); await pause(200);
|
||||
const accounts = (await call('proxy.accounts')).accounts;
|
||||
assert.equal(accounts.length, 1); assert.equal(accounts[0].provider, 'z.ai');
|
||||
assert.deepEqual(Object.keys(accounts[0]).sort(), ['accountId', 'displayName', 'priority', 'provider', 'status']);
|
||||
console.log('proxy.apikey.add PUT /config/api-keys/claude; account fields=accountId,provider,displayName,status,priority; id from auth_index');
|
||||
await call('proxy.priority.set', { accountId: accounts[0].accountId, priority: 7 });
|
||||
assert.equal((await call('proxy.accounts')).accounts[0].priority, 7);
|
||||
console.log('proxy.priority.set API-key PUT /config/api-keys/claude; priority read-back=7');
|
||||
const keys = await call('proxy.keys');
|
||||
assert.equal(keys.keys.length, 1); assert.equal(keys.keys[0].management, 'sentinel'); assert.match(keys.keys[0].keyId, /^[a-f0-9]{12}$/);
|
||||
console.log('proxy.keys GET /config/access/api-keys; fields=keyId,management,agent,issuedAt,state; sentinel labelled; values absent');
|
||||
// Fixture-only upload exercises the OAuth priority API relied on by the page.
|
||||
const uploaded = await fetch(`http://127.0.0.1:${port}/v8/management/credentials?name=p4-fixture.json`, {
|
||||
method: 'POST', headers: { Authorization: `Bearer ${management}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'claude', access_token: 'p4-access', refresh_token: 'p4-refresh', expired: '2099-01-01T00:00:00Z' }),
|
||||
signal: AbortSignal.timeout(5000), redirect: 'error',
|
||||
});
|
||||
assert.equal(uploaded.status, 200); await uploaded.body?.cancel(); await pause(200);
|
||||
const oauth = (await call('proxy.accounts')).accounts.find(a => a.provider === 'claude');
|
||||
assert.ok(oauth);
|
||||
await call('proxy.priority.set', { accountId: oauth.accountId, priority: 8 });
|
||||
assert.equal((await call('proxy.accounts')).accounts.find(a => a.accountId === oauth.accountId).priority, 8);
|
||||
console.log('proxy.priority.set OAuth PATCH /credentials/fields body={name,priority}; priority read-back=8');
|
||||
for (const value of [canary, management, 'p4-access', 'p4-refresh', 'p4-fake-code']) {
|
||||
assert.equal(JSON.stringify(receipts).includes(value), false, 'secret absent from receipts');
|
||||
assert.equal(JSON.stringify(store.all('SELECT body FROM events')).includes(value), false, 'secret absent from bus events');
|
||||
assert.equal(JSON.stringify(answers).includes(value), false, 'credential/code absent from bus replies');
|
||||
}
|
||||
console.log('PASS offline native-page bus contract and secret-free receipts');
|
||||
} catch (error) {
|
||||
// Never print assertion actual/expected values or a raw upstream exception.
|
||||
observe();
|
||||
const errorClass = ['Error', 'AssertionError', 'ProxyError', 'BusError', 'TypeError', 'RangeError'].includes(error.name) ? error.name : 'Error';
|
||||
console.error('P4 upgrade FAILED ' + JSON.stringify({ stage, errorClass,
|
||||
code: errorCodes.includes(error.code) ? error.code : 'acceptance-failed' }));
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
store?.close(); child.kill('SIGTERM');
|
||||
await Promise.race([new Promise(r => child.once('exit', r)), pause(3000).then(() => child.kill('SIGKILL'))]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# P4 PIN upgrade acceptance; outside scripts/test-*.sh, requires Sage's slot.
|
||||
set -euo pipefail
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
BINARY=${1:?P1 verified fetched executable required}
|
||||
export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)
|
||||
trap 'rm -rf -- "$TMPDIR"' EXIT
|
||||
timeout -k 10 120 nice -n $((19 - $(nice))) unshare -rn bash -c '
|
||||
set -e
|
||||
ip link set lo up
|
||||
export P4_UPGRADE_ISOLATED=yes
|
||||
exec node "$1/acceptance.mjs" "$TMPDIR" "$2"
|
||||
' bash "$HERE" "$BINARY"
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:22Z
|
||||
01:42:22 up 11 days, 17:02, 2 users, load average: 5.98, 15.98, 16.44
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:18Z
|
||||
01:42:18 up 11 days, 17:02, 2 users, load average: 5.98, 15.98, 16.44
|
||||
@@ -0,0 +1,9 @@
|
||||
P4 offline upgrade version=8.0.42 nice=19
|
||||
P4 diagnostic {"stage":"binary-start","nodePriority":19,"childPriority":19,"lastClientErrorCode":null,"nodeSchedulingPolicy":0,"childSchedulingPolicy":0,"childExitCode":null,"childSignal":null}
|
||||
P4 diagnostic {"stage":"management-ready","nodePriority":19,"childPriority":19,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":0,"childSchedulingPolicy":0,"childExitCode":null,"childSignal":null}
|
||||
P4 diagnostic {"stage":"store-open","nodePriority":19,"childPriority":19,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":0,"childSchedulingPolicy":0,"childExitCode":null,"childSignal":null}
|
||||
P4 diagnostic {"stage":"broker-bound","nodePriority":19,"childPriority":19,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":0,"childSchedulingPolicy":0,"childExitCode":null,"childSignal":null}
|
||||
P4 diagnostic {"stage":"initial-accounts","nodePriority":19,"childPriority":19,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":0,"childSchedulingPolicy":0,"childExitCode":null,"childSignal":null}
|
||||
P4 diagnostic {"stage":"initial-accounts","nodePriority":16,"childPriority":19,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":0,"childExitCode":null,"childSignal":null}
|
||||
P4 upgrade FAILED {"stage":"initial-accounts","errorClass":"Error","code":"proxy-down"}
|
||||
P4 diagnostic {"stage":"initial-accounts","nodePriority":16,"childPriority":null,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":null,"childExitCode":0,"childSignal":null}
|
||||
@@ -0,0 +1,19 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/L2p5A3
|
||||
✔ accounts have exactly the allowlisted public fields, id from credential identifier (22.174496ms)
|
||||
✔ token-like data in every upstream field never reaches public accounts (4.314601ms)
|
||||
✔ API key account is derived from proxy auth_index, never api-key, and can set priority (9.723941ms)
|
||||
✔ OAuth priority uses top-level name/priority and confirms it through list (3.608642ms)
|
||||
✔ priority-unconfirmed: accepted API-key write with unchanged read-back refuses (3.746827ms)
|
||||
✔ priority-write-refused: OAuth PATCH status 201 (2.247959ms)
|
||||
✔ priority-write-refused: OAuth PATCH status 400 (2.965091ms)
|
||||
✔ priority-invalid: unknown account refuses before any write (1.743249ms)
|
||||
✔ proxy-down: a closed management listener refuses once (1.421441ms)
|
||||
✔ client keys return ids, sentinel/foreign labels and only scoped record metadata (2.327823ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 143.18039
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.53, 16.42, 16.59
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.66, 16.61, 16.65
|
||||
@@ -0,0 +1,26 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/TEAh1u
|
||||
✔ proxy.login.start: provider-not-allowed (27.220361ms)
|
||||
✔ proxy.apikey.add: provider-not-allowed (0.344999ms)
|
||||
✔ apikey-invalid empty (0.294884ms)
|
||||
✔ apikey-invalid too long (0.274ms)
|
||||
✔ apikey-invalid charset (0.30855ms)
|
||||
✔ callback-url-invalid not URL (1.222458ms)
|
||||
✔ callback-url-invalid oversize (0.413224ms)
|
||||
✔ callback-url-invalid repeated state (0.285131ms)
|
||||
✔ login-state-unknown callback never reaches proxy (0.345734ms)
|
||||
✔ login state is scoped to the human and business (0.467282ms)
|
||||
✔ accepted callback wait is pending; only a status ok completes and retires state (0.843249ms)
|
||||
✔ expiry reports login-timeout once with no management call and retires state (0.267807ms)
|
||||
✔ cancel calls DELETE and retires state (0.258213ms)
|
||||
✔ upstream OAuth error body never crosses bus response (0.231392ms)
|
||||
✔ priority-invalid {"accountId":"bad","priority":1} (0.201431ms)
|
||||
✔ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1.5} (0.174276ms)
|
||||
✔ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1001} (0.170986ms)
|
||||
ℹ tests 17
|
||||
ℹ suites 0
|
||||
ℹ pass 17
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 106.766018
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.66, 16.61, 16.65
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.66, 16.61, 16.65
|
||||
@@ -0,0 +1,50 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/coQR8v
|
||||
✔ proxy.accounts: owner calls page once with business/human scope (19.314895ms)
|
||||
✔ proxy.accounts: tester human-required (14.382028ms)
|
||||
✔ proxy.accounts: agent human-required (13.366875ms)
|
||||
✔ proxy.accounts: user human-required (12.734936ms)
|
||||
✔ proxy.accounts: not-configured (13.206913ms)
|
||||
✔ proxy.login.start: owner calls page once with business/human scope (12.601593ms)
|
||||
✔ proxy.login.start: tester human-required (12.793022ms)
|
||||
✔ proxy.login.start: agent human-required (12.576573ms)
|
||||
✔ proxy.login.start: user human-required (13.04681ms)
|
||||
✔ proxy.login.start: not-configured (13.419448ms)
|
||||
✔ proxy.login.submit: owner calls page once with business/human scope (12.695382ms)
|
||||
✔ proxy.login.submit: tester human-required (13.507149ms)
|
||||
✔ proxy.login.submit: agent human-required (13.188966ms)
|
||||
✔ proxy.login.submit: user human-required (13.722535ms)
|
||||
✔ proxy.login.submit: not-configured (12.60293ms)
|
||||
✔ proxy.login.status: owner calls page once with business/human scope (12.159557ms)
|
||||
✔ proxy.login.status: tester human-required (12.884978ms)
|
||||
✔ proxy.login.status: agent human-required (13.227723ms)
|
||||
✔ proxy.login.status: user human-required (13.638448ms)
|
||||
✔ proxy.login.status: not-configured (13.288314ms)
|
||||
✔ proxy.login.cancel: owner calls page once with business/human scope (13.68684ms)
|
||||
✔ proxy.login.cancel: tester human-required (14.477894ms)
|
||||
✔ proxy.login.cancel: agent human-required (18.618454ms)
|
||||
✔ proxy.login.cancel: user human-required (15.764118ms)
|
||||
✔ proxy.login.cancel: not-configured (14.155249ms)
|
||||
✔ proxy.apikey.add: owner calls page once with business/human scope (13.346653ms)
|
||||
✔ proxy.apikey.add: tester human-required (14.275591ms)
|
||||
✔ proxy.apikey.add: agent human-required (15.041527ms)
|
||||
✔ proxy.apikey.add: user human-required (13.715987ms)
|
||||
✔ proxy.apikey.add: not-configured (13.608844ms)
|
||||
✔ proxy.priority.set: owner calls page once with business/human scope (12.499478ms)
|
||||
✔ proxy.priority.set: tester human-required (14.103153ms)
|
||||
✔ proxy.priority.set: agent human-required (14.57306ms)
|
||||
✔ proxy.priority.set: user human-required (14.02396ms)
|
||||
✔ proxy.priority.set: not-configured (13.77083ms)
|
||||
✔ proxy.keys: owner calls page once with business/human scope (11.740894ms)
|
||||
✔ proxy.keys: tester human-required (12.726783ms)
|
||||
✔ proxy.keys: agent human-required (14.12361ms)
|
||||
✔ proxy.keys: user human-required (13.283755ms)
|
||||
✔ proxy.keys: not-configured (12.67668ms)
|
||||
✔ API-key canary never enters broker events or unknown error output (13.619581ms)
|
||||
ℹ tests 41
|
||||
ℹ suites 0
|
||||
ℹ pass 41
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 696.597349
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.66, 16.61, 16.65
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:06Z
|
||||
01:42:06 up 11 days, 17:01, 2 users, load average: 6.66, 16.61, 16.65
|
||||
@@ -0,0 +1,12 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/RE2vwS
|
||||
✔ OAuth management calls keep URL and state out of receipts (19.749276ms)
|
||||
✔ upstream key goes only in request body and upstream refusal body is discarded (4.186524ms)
|
||||
✔ redirected management calls are not followed or retried (2.121196ms)
|
||||
ℹ tests 3
|
||||
ℹ suites 0
|
||||
ℹ pass 3
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 98.033185
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:06Z
|
||||
01:42:06 up 11 days, 17:01, 2 users, load average: 6.66, 16.61, 16.65
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:06Z
|
||||
01:42:06 up 11 days, 17:01, 2 users, load average: 6.66, 16.61, 16.65
|
||||
@@ -0,0 +1,10 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/UVjb2T
|
||||
✔ Console production module graph cannot import the model-proxy client (70.585338ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 1
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 137.462924
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:12Z
|
||||
01:42:12 up 11 days, 17:02, 2 users, load average: 6.53, 16.42, 16.59
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:11Z
|
||||
01:42:11 up 11 days, 17:02, 2 users, load average: 6.53, 16.42, 16.59
|
||||
@@ -0,0 +1,12 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/WLROJU
|
||||
✔ proxy browser clears write-only key immediately, sends once and renders data as text (603.091893ms)
|
||||
✔ proxy browser OAuth opens safely, callback wait stays pending, status ok completes (2601.033669ms)
|
||||
✔ proxy browser read-only disables account changes (550.580882ms)
|
||||
ℹ tests 3
|
||||
ℹ suites 0
|
||||
ℹ pass 3
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3846.295222
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:11Z
|
||||
01:42:11 up 11 days, 17:02, 2 users, load average: 6.53, 16.42, 16.59
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:08Z
|
||||
01:42:08 up 11 days, 17:01, 2 users, load average: 6.53, 16.42, 16.59
|
||||
@@ -0,0 +1,11 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/kmRLQ1
|
||||
✔ API-key canary reaches fake once and zero responses/logs/receipts/store files (51.811486ms)
|
||||
✔ OAuth URLs, state and code stay out of broker records and management receipts (19.046885ms)
|
||||
ℹ tests 2
|
||||
ℹ suites 0
|
||||
ℹ pass 2
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 240.643186
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:08Z
|
||||
01:42:08 up 11 days, 17:01, 2 users, load average: 6.53, 16.42, 16.59
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.53, 16.42, 16.59
|
||||
@@ -0,0 +1,20 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/X0Vp78
|
||||
✔ owner proxy routes each make exactly one allowlisted call (27.305714ms)
|
||||
✔ proxy write refuses signed-out (2.96232ms)
|
||||
✔ proxy write refuses loopback (1.79702ms)
|
||||
✔ proxy write refuses cross-origin (1.340864ms)
|
||||
✔ proxy write refuses missing Fetch Metadata (1.47209ms)
|
||||
✔ all proxy writes are read-only without --send (3.481474ms)
|
||||
✔ granted user never reaches owner page or any proxy route (4.998721ms)
|
||||
✔ native page offers write-only key input and no panel script (2.07878ms)
|
||||
✔ Console caller rejects every non-proxy verb (0.837371ms)
|
||||
✔ tester socket refuses proxy page, reads and writes before any owner call (7.459873ms)
|
||||
✔ malformed API-key JSON never appears in errors or logs (1.621405ms)
|
||||
ℹ tests 11
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 221.712154
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.53, 16.42, 16.59
|
||||
@@ -0,0 +1,2 @@
|
||||
2026-10-25T06:42:07Z
|
||||
01:42:07 up 11 days, 17:01, 2 users, load average: 6.53, 16.42, 16.59
|
||||
@@ -0,0 +1,6 @@
|
||||
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 upgrade FAILED {"stage":"store-open","errorClass":"Error","code":"acceptance-failed"}
|
||||
P4 diagnostic {"stage":"store-open","nodePriority":16,"childPriority":null,"lastClientErrorCode":"proxy-client-down","nodeSchedulingPolicy":5,"childSchedulingPolicy":null,"childExitCode":0,"childSignal":null}
|
||||
@@ -0,0 +1 @@
|
||||
01:14:23 up 11 days, 16:34, 2 users, load average: 3.15, 5.41, 9.24
|
||||
@@ -0,0 +1 @@
|
||||
01:14:19 up 11 days, 16:34, 2 users, load average: 2.90, 5.40, 9.26
|
||||
@@ -0,0 +1,2 @@
|
||||
fetch nice 19
|
||||
{"dir":"/mnt/storage/scratch/tmp/76TDtz/download/opt/cliproxyapi/8.0.42","binary":"/mnt/storage/scratch/tmp/76TDtz/download/opt/cliproxyapi/8.0.42/cli-proxy-api","panel":"/mnt/storage/scratch/tmp/76TDtz/download/opt/cliproxyapi/8.0.42/static/management.html"}
|
||||
@@ -0,0 +1 @@
|
||||
/mnt/storage/scratch/tmp/76TDtz
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,17 @@
|
||||
import { build } from '../../../../packages/webui/node_modules/esbuild/lib/main.js';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
const entries = [
|
||||
'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',
|
||||
];
|
||||
const result = await build({ entryPoints: entries, bundle: true, platform: 'node', format: 'esm', splitting: true,
|
||||
packages: 'external', write: false, outdir: '/unused-p4-focused', metafile: true, logLevel: 'silent' });
|
||||
writeFileSync(new URL('./focused-import-graph.json', import.meta.url), JSON.stringify(result.metafile.inputs, null, 2) + '\n');
|
||||
writeFileSync(new URL('./focused-run-list.txt', import.meta.url), entries.join('\n') + '\n');
|
||||
console.log('Static parsed inputs=' + Object.keys(result.metafile.inputs).length + ' test entries=' + entries.length);
|
||||
@@ -0,0 +1,8 @@
|
||||
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
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"fullGraphPath": "/mnt/storage/scratch/tmp/9YZd4m/p4-import-graph.json",
|
||||
"fullGraphSha256": "f91e682e0301d88efedc2a9c61a57c8fd65ace432f50e76e37eb37d0a1c7a396",
|
||||
"parsedInputs": 677,
|
||||
"selected": 280,
|
||||
"mandatory": 199,
|
||||
"imported": 256,
|
||||
"excludedEvenIfImported": "packages/cli/tests/case1-accept.test.mjs",
|
||||
"additional": [
|
||||
"packages/business/tests/case1-business-file.test.mjs",
|
||||
"packages/cli/tests/access-boot.test.mjs",
|
||||
"packages/cli/tests/access.test.mjs",
|
||||
"packages/cli/tests/app.test.mjs",
|
||||
"packages/cli/tests/boot-status.test.mjs",
|
||||
"packages/cli/tests/case1-hostcheck.test.mjs",
|
||||
"packages/cli/tests/case2-install.test.mjs",
|
||||
"packages/cli/tests/case2-persona-import.test.mjs",
|
||||
"packages/cli/tests/case2-unit.test.mjs",
|
||||
"packages/cli/tests/channel-cli.test.mjs",
|
||||
"packages/cli/tests/channel-recovery.test.mjs",
|
||||
"packages/cli/tests/cli.test.mjs",
|
||||
"packages/cli/tests/compactions-launch.test.mjs",
|
||||
"packages/cli/tests/config.test.mjs",
|
||||
"packages/cli/tests/credentials.test.mjs",
|
||||
"packages/cli/tests/federation-acceptance.test.mjs",
|
||||
"packages/cli/tests/follow.test.mjs",
|
||||
"packages/cli/tests/git-socket.test.mjs",
|
||||
"packages/cli/tests/grants-scope.test.mjs",
|
||||
"packages/cli/tests/grants.test.mjs",
|
||||
"packages/cli/tests/host.test.mjs",
|
||||
"packages/cli/tests/hosts.test.mjs",
|
||||
"packages/cli/tests/idle-stop-race.test.mjs",
|
||||
"packages/cli/tests/instances.test.mjs",
|
||||
"packages/cli/tests/launch-account.test.mjs",
|
||||
"packages/cli/tests/launch-limit-checks.test.mjs",
|
||||
"packages/cli/tests/launch-limits.test.mjs",
|
||||
"packages/cli/tests/launch-routes.test.mjs",
|
||||
"packages/cli/tests/launcher.test.mjs",
|
||||
"packages/cli/tests/lifecycle.test.mjs",
|
||||
"packages/cli/tests/limits-tokens.test.mjs",
|
||||
"packages/cli/tests/limits.test.mjs",
|
||||
"packages/cli/tests/mark-read.test.mjs",
|
||||
"packages/cli/tests/model-proxy.test.mjs",
|
||||
"packages/cli/tests/names.test.mjs",
|
||||
"packages/cli/tests/notifier.test.mjs",
|
||||
"packages/cli/tests/optional-tracker.test.mjs",
|
||||
"packages/cli/tests/other-tree.test.mjs",
|
||||
"packages/cli/tests/processing-cli.test.mjs",
|
||||
"packages/cli/tests/project-name-repo.test.mjs",
|
||||
"packages/cli/tests/project.test.mjs",
|
||||
"packages/cli/tests/publish-socket.test.mjs",
|
||||
"packages/cli/tests/reply-limits.test.mjs",
|
||||
"packages/cli/tests/revoke-window.test.mjs",
|
||||
"packages/cli/tests/session-clear.test.mjs",
|
||||
"packages/cli/tests/session-cli.test.mjs",
|
||||
"packages/cli/tests/session-launch.test.mjs",
|
||||
"packages/cli/tests/stack-copy.test.mjs",
|
||||
"packages/cli/tests/stack-instances.test.mjs",
|
||||
"packages/cli/tests/tester-http.test.mjs",
|
||||
"packages/cli/tests/tester-reads.test.mjs",
|
||||
"packages/cli/tests/tester-threads.test.mjs",
|
||||
"packages/cli/tests/tester.test.mjs",
|
||||
"packages/cli/tests/thread-cli.test.mjs",
|
||||
"packages/cli/tests/thread-launch.test.mjs",
|
||||
"packages/cli/tests/thread-revoke.test.mjs",
|
||||
"packages/cli/tests/trackers-boot.test.mjs",
|
||||
"packages/cli/tests/transport.test.mjs",
|
||||
"packages/cli/tests/usage.test.mjs",
|
||||
"packages/cli/tests/verbs.test.mjs",
|
||||
"packages/harness/tests/citation-runner.test.mjs",
|
||||
"packages/harness/tests/claude-session.test.mjs",
|
||||
"packages/harness/tests/creative.test.mjs",
|
||||
"packages/harness/tests/pi-session.test.mjs",
|
||||
"packages/harness/tests/raise-domain.test.mjs",
|
||||
"packages/harness/tests/runner.test.mjs",
|
||||
"packages/harness/tests/skills.test.mjs",
|
||||
"packages/harness/tests/spec-tools.test.mjs",
|
||||
"packages/harness/tests/tools.test.mjs",
|
||||
"packages/processing-control/tests/task.test.mjs",
|
||||
"packages/seat/tests/sandbox.test.mjs",
|
||||
"packages/seat/tests/session.test.mjs",
|
||||
"packages/tasks/tests/adapter.test.mjs",
|
||||
"packages/tasks/tests/comment-restart.test.mjs",
|
||||
"packages/tasks/tests/fields.test.mjs",
|
||||
"packages/tasks/tests/processing.test.mjs",
|
||||
"packages/tasks/tests/shapes.test.mjs",
|
||||
"packages/tasks/tests/startup.test.mjs",
|
||||
"packages/tasks/tests/sync.test.mjs",
|
||||
"packages/tasks/tests/tree.test.mjs",
|
||||
"packages/tasks/tests/verbs.test.mjs"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,39 @@
|
||||
// Parse real static/dynamic literal imports with esbuild, never fixture-text grep.
|
||||
import { build } from '../../../../packages/webui/node_modules/esbuild/lib/main.js';
|
||||
import { readdirSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
function walk(dir) { return readdirSync(dir, { withFileTypes: true }).flatMap(e => e.name === 'node_modules' ? [] : e.isDirectory() ? walk(path.join(dir, e.name)) : e.name.endsWith('.test.mjs') ? [path.join(dir, e.name)] : []); }
|
||||
const tests = walk('packages');
|
||||
const result = await build({ entryPoints: tests, bundle: true, platform: 'node', format: 'esm', splitting: true, packages: 'external',
|
||||
write: false, outdir: '/unused-p4-graph', metafile: true, logLevel: 'silent' });
|
||||
const graph = result.metafile.inputs;
|
||||
const changed = new Set(['packages/model-proxy/src/client.mjs', 'packages/bus/src/broker.mjs', 'packages/bus/src/runtime.mjs',
|
||||
'packages/bus/src/proxy-page.mjs', 'packages/webui/src/cli.mjs', 'packages/webui/src/serve.mjs', 'packages/webui/src/console-bus.mjs', 'packages/webui/src/proxy-http.mjs']);
|
||||
function reaches(file, seen = new Set()) {
|
||||
if (changed.has(file)) return true;
|
||||
if (seen.has(file)) return false; seen.add(file);
|
||||
return (graph[file]?.imports ?? []).some(i => !i.external && reaches(i.path, seen));
|
||||
}
|
||||
const forbidden = 'packages/cli/tests/case1-accept.test.mjs';
|
||||
const mandatory = tests.filter(p => /^packages\/(model-proxy|bus|webui)\/tests\//.test(p));
|
||||
const imported = tests.filter(p => reaches(p));
|
||||
const selected = [...new Set([...mandatory, ...imported])].filter(p => p !== forbidden).sort();
|
||||
const scratch = execFileSync('mktemp', ['-d', '/mnt/storage/scratch/tmp/XXXXXX'], { encoding: 'utf8' }).trim();
|
||||
const fullGraphPath = path.join(scratch, 'p4-import-graph.json'), fullGraph = JSON.stringify(graph, null, 2) + '\n';
|
||||
writeFileSync(fullGraphPath, fullGraph);
|
||||
function witness(file, seen = new Set()) {
|
||||
if (changed.has(file)) return [file];
|
||||
if (seen.has(file)) return null; seen.add(file);
|
||||
for (const i of graph[file]?.imports ?? []) {
|
||||
if (i.external) continue;
|
||||
const rest = witness(i.path, seen); if (rest) return [file, ...rest];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
writeFileSync(new URL('./gate-import-witness.json', import.meta.url), JSON.stringify(Object.fromEntries(selected.map(p => [p, witness(p) ?? ['mandatory suite']])), null, 2) + '\n');
|
||||
writeFileSync(new URL('./gate-run-list.txt', import.meta.url), selected.join('\n') + '\n');
|
||||
console.log(JSON.stringify({ fullGraphPath, fullGraphSha256: createHash('sha256').update(fullGraph).digest('hex'), parsedInputs: Object.keys(graph).length, selected: selected.length, mandatory: mandatory.length,
|
||||
imported: imported.length, excludedEvenIfImported: forbidden,
|
||||
additional: selected.filter(p => !mandatory.includes(p)) }, null, 2));
|
||||
@@ -0,0 +1,280 @@
|
||||
packages/bus/tests/access-revoke.test.mjs
|
||||
packages/bus/tests/account-migration.test.mjs
|
||||
packages/bus/tests/address.test.mjs
|
||||
packages/bus/tests/agents.test.mjs
|
||||
packages/bus/tests/approval-binding.test.mjs
|
||||
packages/bus/tests/broker.test.mjs
|
||||
packages/bus/tests/business.test.mjs
|
||||
packages/bus/tests/channel-proof.test.mjs
|
||||
packages/bus/tests/chief-of-staff.test.mjs
|
||||
packages/bus/tests/citation-availability.test.mjs
|
||||
packages/bus/tests/citation-guard.test.mjs
|
||||
packages/bus/tests/citation-runtime.test.mjs
|
||||
packages/bus/tests/comment-cursors.test.mjs
|
||||
packages/bus/tests/comment-migration.test.mjs
|
||||
packages/bus/tests/console-decide.test.mjs
|
||||
packages/bus/tests/creative.test.mjs
|
||||
packages/bus/tests/credential-claim.test.mjs
|
||||
packages/bus/tests/credential-status.test.mjs
|
||||
packages/bus/tests/credential-store.test.mjs
|
||||
packages/bus/tests/credential-verbs.test.mjs
|
||||
packages/bus/tests/credentials.test.mjs
|
||||
packages/bus/tests/csuite-messaging.test.mjs
|
||||
packages/bus/tests/daily-quota-runtime.test.mjs
|
||||
packages/bus/tests/decision-close.test.mjs
|
||||
packages/bus/tests/decision-route.test.mjs
|
||||
packages/bus/tests/decisions.test.mjs
|
||||
packages/bus/tests/end-launch.test.mjs
|
||||
packages/bus/tests/fire-revokes-grants.test.mjs
|
||||
packages/bus/tests/gitea-grant-docker.test.mjs
|
||||
packages/bus/tests/gitea-grant.test.mjs
|
||||
packages/bus/tests/gitea-migration.test.mjs
|
||||
packages/bus/tests/grant-enroll.test.mjs
|
||||
packages/bus/tests/grant-executor.test.mjs
|
||||
packages/bus/tests/grant-host.test.mjs
|
||||
packages/bus/tests/grant-migration.test.mjs
|
||||
packages/bus/tests/grant-scope-runtime.test.mjs
|
||||
packages/bus/tests/grant-scope.test.mjs
|
||||
packages/bus/tests/hard-waits.test.mjs
|
||||
packages/bus/tests/host-run.test.mjs
|
||||
packages/bus/tests/host-tools.test.mjs
|
||||
packages/bus/tests/human.test.mjs
|
||||
packages/bus/tests/instances-verb.test.mjs
|
||||
packages/bus/tests/interpreter.test.mjs
|
||||
packages/bus/tests/land.test.mjs
|
||||
packages/bus/tests/launch-notice.test.mjs
|
||||
packages/bus/tests/launcher-released.test.mjs
|
||||
packages/bus/tests/library-credential.test.mjs
|
||||
packages/bus/tests/library-lookups.test.mjs
|
||||
packages/bus/tests/library-sync.test.mjs
|
||||
packages/bus/tests/library.test.mjs
|
||||
packages/bus/tests/lifecycle-revoke.test.mjs
|
||||
packages/bus/tests/lifecycle-verbs.test.mjs
|
||||
packages/bus/tests/limit-checks.test.mjs
|
||||
packages/bus/tests/limit-stop.test.mjs
|
||||
packages/bus/tests/limits-runtime.test.mjs
|
||||
packages/bus/tests/limits.test.mjs
|
||||
packages/bus/tests/message-citation.test.mjs
|
||||
packages/bus/tests/message-filter.test.mjs
|
||||
packages/bus/tests/messages.test.mjs
|
||||
packages/bus/tests/messaging.test.mjs
|
||||
packages/bus/tests/model-proxy.test.mjs
|
||||
packages/bus/tests/name-migration.test.mjs
|
||||
packages/bus/tests/names.test.mjs
|
||||
packages/bus/tests/peer-transport.test.mjs
|
||||
packages/bus/tests/peercred.test.mjs
|
||||
packages/bus/tests/plugins-verb.test.mjs
|
||||
packages/bus/tests/principal.test.mjs
|
||||
packages/bus/tests/process.test.mjs
|
||||
packages/bus/tests/processing-runtime.test.mjs
|
||||
packages/bus/tests/processing-timeout.test.mjs
|
||||
packages/bus/tests/processing-web.test.mjs
|
||||
packages/bus/tests/processing.test.mjs
|
||||
packages/bus/tests/project-await.test.mjs
|
||||
packages/bus/tests/project-files.test.mjs
|
||||
packages/bus/tests/project-runtime.test.mjs
|
||||
packages/bus/tests/proxy-page-accounts.test.mjs
|
||||
packages/bus/tests/proxy-page-state.test.mjs
|
||||
packages/bus/tests/proxy-page.test.mjs
|
||||
packages/bus/tests/publish.test.mjs
|
||||
packages/bus/tests/reload.test.mjs
|
||||
packages/bus/tests/replies.test.mjs
|
||||
packages/bus/tests/requirements.test.mjs
|
||||
packages/bus/tests/review.test.mjs
|
||||
packages/bus/tests/revoke-window.test.mjs
|
||||
packages/bus/tests/room-check.test.mjs
|
||||
packages/bus/tests/rooms.test.mjs
|
||||
packages/bus/tests/routes-verb.test.mjs
|
||||
packages/bus/tests/schema.test.mjs
|
||||
packages/bus/tests/session-clear.test.mjs
|
||||
packages/bus/tests/session-compactions.test.mjs
|
||||
packages/bus/tests/sessions.test.mjs
|
||||
packages/bus/tests/single-use.test.mjs
|
||||
packages/bus/tests/spec-migration.test.mjs
|
||||
packages/bus/tests/spec-review.test.mjs
|
||||
packages/bus/tests/spec-socket.test.mjs
|
||||
packages/bus/tests/spec.test.mjs
|
||||
packages/bus/tests/staff-request.test.mjs
|
||||
packages/bus/tests/state.test.mjs
|
||||
packages/bus/tests/store.test.mjs
|
||||
packages/bus/tests/task-page.test.mjs
|
||||
packages/bus/tests/task-read.test.mjs
|
||||
packages/bus/tests/tasks.test.mjs
|
||||
packages/bus/tests/tester-threads.test.mjs
|
||||
packages/bus/tests/tester-writes.test.mjs
|
||||
packages/bus/tests/tester.test.mjs
|
||||
packages/bus/tests/thread-claims.test.mjs
|
||||
packages/bus/tests/thread-migration.test.mjs
|
||||
packages/bus/tests/thread-shares.test.mjs
|
||||
packages/bus/tests/threads.test.mjs
|
||||
packages/bus/tests/transport.test.mjs
|
||||
packages/bus/tests/usage-broker.test.mjs
|
||||
packages/bus/tests/usage-migration.test.mjs
|
||||
packages/bus/tests/usage-runtime.test.mjs
|
||||
packages/bus/tests/usage.test.mjs
|
||||
packages/bus/tests/user-principal.test.mjs
|
||||
packages/bus/tests/viewer.test.mjs
|
||||
packages/bus/tests/web.test.mjs
|
||||
packages/bus/tests/workflow-roles.test.mjs
|
||||
packages/business/tests/case1-business-file.test.mjs
|
||||
packages/cli/tests/access-boot.test.mjs
|
||||
packages/cli/tests/access.test.mjs
|
||||
packages/cli/tests/app.test.mjs
|
||||
packages/cli/tests/boot-status.test.mjs
|
||||
packages/cli/tests/case1-hostcheck.test.mjs
|
||||
packages/cli/tests/case2-install.test.mjs
|
||||
packages/cli/tests/case2-persona-import.test.mjs
|
||||
packages/cli/tests/case2-unit.test.mjs
|
||||
packages/cli/tests/channel-cli.test.mjs
|
||||
packages/cli/tests/channel-recovery.test.mjs
|
||||
packages/cli/tests/cli.test.mjs
|
||||
packages/cli/tests/compactions-launch.test.mjs
|
||||
packages/cli/tests/config.test.mjs
|
||||
packages/cli/tests/credentials.test.mjs
|
||||
packages/cli/tests/federation-acceptance.test.mjs
|
||||
packages/cli/tests/follow.test.mjs
|
||||
packages/cli/tests/git-socket.test.mjs
|
||||
packages/cli/tests/grants-scope.test.mjs
|
||||
packages/cli/tests/grants.test.mjs
|
||||
packages/cli/tests/host.test.mjs
|
||||
packages/cli/tests/hosts.test.mjs
|
||||
packages/cli/tests/idle-stop-race.test.mjs
|
||||
packages/cli/tests/instances.test.mjs
|
||||
packages/cli/tests/launch-account.test.mjs
|
||||
packages/cli/tests/launch-limit-checks.test.mjs
|
||||
packages/cli/tests/launch-limits.test.mjs
|
||||
packages/cli/tests/launch-routes.test.mjs
|
||||
packages/cli/tests/launcher.test.mjs
|
||||
packages/cli/tests/lifecycle.test.mjs
|
||||
packages/cli/tests/limits-tokens.test.mjs
|
||||
packages/cli/tests/limits.test.mjs
|
||||
packages/cli/tests/mark-read.test.mjs
|
||||
packages/cli/tests/model-proxy.test.mjs
|
||||
packages/cli/tests/names.test.mjs
|
||||
packages/cli/tests/notifier.test.mjs
|
||||
packages/cli/tests/optional-tracker.test.mjs
|
||||
packages/cli/tests/other-tree.test.mjs
|
||||
packages/cli/tests/processing-cli.test.mjs
|
||||
packages/cli/tests/project-name-repo.test.mjs
|
||||
packages/cli/tests/project.test.mjs
|
||||
packages/cli/tests/publish-socket.test.mjs
|
||||
packages/cli/tests/reply-limits.test.mjs
|
||||
packages/cli/tests/revoke-window.test.mjs
|
||||
packages/cli/tests/session-clear.test.mjs
|
||||
packages/cli/tests/session-cli.test.mjs
|
||||
packages/cli/tests/session-launch.test.mjs
|
||||
packages/cli/tests/stack-copy.test.mjs
|
||||
packages/cli/tests/stack-instances.test.mjs
|
||||
packages/cli/tests/tester-http.test.mjs
|
||||
packages/cli/tests/tester-reads.test.mjs
|
||||
packages/cli/tests/tester-threads.test.mjs
|
||||
packages/cli/tests/tester.test.mjs
|
||||
packages/cli/tests/thread-cli.test.mjs
|
||||
packages/cli/tests/thread-launch.test.mjs
|
||||
packages/cli/tests/thread-revoke.test.mjs
|
||||
packages/cli/tests/trackers-boot.test.mjs
|
||||
packages/cli/tests/transport.test.mjs
|
||||
packages/cli/tests/usage.test.mjs
|
||||
packages/cli/tests/verbs.test.mjs
|
||||
packages/harness/tests/citation-runner.test.mjs
|
||||
packages/harness/tests/claude-session.test.mjs
|
||||
packages/harness/tests/creative.test.mjs
|
||||
packages/harness/tests/pi-session.test.mjs
|
||||
packages/harness/tests/raise-domain.test.mjs
|
||||
packages/harness/tests/runner.test.mjs
|
||||
packages/harness/tests/skills.test.mjs
|
||||
packages/harness/tests/spec-tools.test.mjs
|
||||
packages/harness/tests/tools.test.mjs
|
||||
packages/model-proxy/tests/client.test.mjs
|
||||
packages/model-proxy/tests/config-unit.test.mjs
|
||||
packages/model-proxy/tests/fetch.test.mjs
|
||||
packages/model-proxy/tests/management-key-encoding.test.mjs
|
||||
packages/model-proxy/tests/page-client.test.mjs
|
||||
packages/model-proxy/tests/runner.test.mjs
|
||||
packages/model-proxy/tests/settings.test.mjs
|
||||
packages/processing-control/tests/task.test.mjs
|
||||
packages/seat/tests/sandbox.test.mjs
|
||||
packages/seat/tests/session.test.mjs
|
||||
packages/tasks/tests/adapter.test.mjs
|
||||
packages/tasks/tests/comment-restart.test.mjs
|
||||
packages/tasks/tests/fields.test.mjs
|
||||
packages/tasks/tests/processing.test.mjs
|
||||
packages/tasks/tests/shapes.test.mjs
|
||||
packages/tasks/tests/startup.test.mjs
|
||||
packages/tasks/tests/sync.test.mjs
|
||||
packages/tasks/tests/tree.test.mjs
|
||||
packages/tasks/tests/verbs.test.mjs
|
||||
packages/webui/tests/agents-browser.test.mjs
|
||||
packages/webui/tests/agents.test.mjs
|
||||
packages/webui/tests/assistant.test.mjs
|
||||
packages/webui/tests/board-held.test.mjs
|
||||
packages/webui/tests/browser-edge.test.mjs
|
||||
packages/webui/tests/browser.test.mjs
|
||||
packages/webui/tests/bus-browser.test.mjs
|
||||
packages/webui/tests/bus.test.mjs
|
||||
packages/webui/tests/channel-chat.test.mjs
|
||||
packages/webui/tests/chat-browser.test.mjs
|
||||
packages/webui/tests/chat-instances.test.mjs
|
||||
packages/webui/tests/chat-replies-browser.test.mjs
|
||||
packages/webui/tests/chat-sessions-access.test.mjs
|
||||
packages/webui/tests/chat.test.mjs
|
||||
packages/webui/tests/conversation.test.mjs
|
||||
packages/webui/tests/decide-browser.test.mjs
|
||||
packages/webui/tests/decide-sso.test.mjs
|
||||
packages/webui/tests/decision-http.test.mjs
|
||||
packages/webui/tests/discord.test.mjs
|
||||
packages/webui/tests/docs-browser.test.mjs
|
||||
packages/webui/tests/editor-browser.test.mjs
|
||||
packages/webui/tests/editor-provenance.test.mjs
|
||||
packages/webui/tests/editor.test.mjs
|
||||
packages/webui/tests/granted-user.test.mjs
|
||||
packages/webui/tests/history-return-flow.test.mjs
|
||||
packages/webui/tests/instances-browser.test.mjs
|
||||
packages/webui/tests/instances.test.mjs
|
||||
packages/webui/tests/library-cli.test.mjs
|
||||
packages/webui/tests/library-routes.test.mjs
|
||||
packages/webui/tests/library-status.test.mjs
|
||||
packages/webui/tests/limits-browser.test.mjs
|
||||
packages/webui/tests/limits-route.test.mjs
|
||||
packages/webui/tests/markdown-preview.test.mjs
|
||||
packages/webui/tests/messages.test.mjs
|
||||
packages/webui/tests/pinned-copy.test.mjs
|
||||
packages/webui/tests/preview.test.mjs
|
||||
packages/webui/tests/processing-browser.test.mjs
|
||||
packages/webui/tests/processing-ui.test.mjs
|
||||
packages/webui/tests/processing.test.mjs
|
||||
packages/webui/tests/project-file.test.mjs
|
||||
packages/webui/tests/project-name-repo.test.mjs
|
||||
packages/webui/tests/proof-cli.test.mjs
|
||||
packages/webui/tests/proof.test.mjs
|
||||
packages/webui/tests/proxy-architecture.test.mjs
|
||||
packages/webui/tests/proxy-browser.test.mjs
|
||||
packages/webui/tests/proxy-canary.test.mjs
|
||||
packages/webui/tests/proxy-page.test.mjs
|
||||
packages/webui/tests/reads.test.mjs
|
||||
packages/webui/tests/relaunch.test.mjs
|
||||
packages/webui/tests/return-flow.test.mjs
|
||||
packages/webui/tests/room-browser.test.mjs
|
||||
packages/webui/tests/room-http.test.mjs
|
||||
packages/webui/tests/routes-browser.test.mjs
|
||||
packages/webui/tests/routes.test.mjs
|
||||
packages/webui/tests/send-browser.test.mjs
|
||||
packages/webui/tests/send.test.mjs
|
||||
packages/webui/tests/serve.test.mjs
|
||||
packages/webui/tests/service.test.mjs
|
||||
packages/webui/tests/settings-form.test.mjs
|
||||
packages/webui/tests/settings-write.test.mjs
|
||||
packages/webui/tests/shell.test.mjs
|
||||
packages/webui/tests/sso-browser.test.mjs
|
||||
packages/webui/tests/sso.test.mjs
|
||||
packages/webui/tests/tester-browser.test.mjs
|
||||
packages/webui/tests/tester-cli.test.mjs
|
||||
packages/webui/tests/tester-listener.test.mjs
|
||||
packages/webui/tests/tester-socket.test.mjs
|
||||
packages/webui/tests/thread-claims-browser.test.mjs
|
||||
packages/webui/tests/thread-claims.test.mjs
|
||||
packages/webui/tests/thread-sharing.test.mjs
|
||||
packages/webui/tests/usage-browser.test.mjs
|
||||
packages/webui/tests/usage.test.mjs
|
||||
packages/webui/tests/user-option-swap.test.mjs
|
||||
packages/webui/tests/views.test.mjs
|
||||
@@ -0,0 +1,19 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/nmbhic
|
||||
✔ accounts have exactly the allowlisted public fields, id from credential identifier (16.726005ms)
|
||||
✔ token-like data in every upstream field never reaches public accounts (3.146433ms)
|
||||
✔ API key account is derived from proxy auth_index, never api-key, and can set priority (7.145341ms)
|
||||
✔ OAuth priority uses top-level name/priority and confirms it through list (2.559503ms)
|
||||
✔ priority-unconfirmed: accepted API-key write with unchanged read-back refuses (2.751129ms)
|
||||
✔ priority-write-refused: OAuth PATCH status 201 (1.645703ms)
|
||||
✔ priority-write-refused: OAuth PATCH status 400 (2.192791ms)
|
||||
✔ priority-invalid: unknown account refuses before any write (1.245093ms)
|
||||
✔ proxy-down: a closed management listener refuses once (1.049636ms)
|
||||
✔ client keys return ids, sentinel/foreign labels and only scoped record metadata (1.589625ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 10
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 106.068199
|
||||
@@ -0,0 +1,10 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/zzWg8y
|
||||
✔ Console production module graph cannot import the model-proxy client (260.158583ms)
|
||||
ℹ tests 1
|
||||
ℹ suites 0
|
||||
ℹ pass 1
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 332.064995
|
||||
@@ -0,0 +1,50 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/W3UTF4
|
||||
✔ proxy.accounts: owner calls page once with business/human scope (42.216353ms)
|
||||
✔ proxy.accounts: tester human-required (27.14418ms)
|
||||
✔ proxy.accounts: agent human-required (15.631107ms)
|
||||
✔ proxy.accounts: user human-required (16.036791ms)
|
||||
✔ proxy.accounts: not-configured (15.881775ms)
|
||||
✔ proxy.login.start: owner calls page once with business/human scope (15.676932ms)
|
||||
✔ proxy.login.start: tester human-required (15.957058ms)
|
||||
✔ proxy.login.start: agent human-required (16.769099ms)
|
||||
✔ proxy.login.start: user human-required (15.27276ms)
|
||||
✔ proxy.login.start: not-configured (18.544344ms)
|
||||
✔ proxy.login.submit: owner calls page once with business/human scope (14.653081ms)
|
||||
✔ proxy.login.submit: tester human-required (17.613806ms)
|
||||
✔ proxy.login.submit: agent human-required (19.653375ms)
|
||||
✔ proxy.login.submit: user human-required (21.882116ms)
|
||||
✔ proxy.login.submit: not-configured (18.504561ms)
|
||||
✔ proxy.login.status: owner calls page once with business/human scope (15.65383ms)
|
||||
✔ proxy.login.status: tester human-required (16.405308ms)
|
||||
✔ proxy.login.status: agent human-required (15.696776ms)
|
||||
✔ proxy.login.status: user human-required (18.693813ms)
|
||||
✔ proxy.login.status: not-configured (16.320036ms)
|
||||
✔ proxy.login.cancel: owner calls page once with business/human scope (13.937442ms)
|
||||
✔ proxy.login.cancel: tester human-required (17.41389ms)
|
||||
✔ proxy.login.cancel: agent human-required (17.722047ms)
|
||||
✔ proxy.login.cancel: user human-required (19.611299ms)
|
||||
✔ proxy.login.cancel: not-configured (15.608602ms)
|
||||
✔ proxy.apikey.add: owner calls page once with business/human scope (15.651765ms)
|
||||
✔ proxy.apikey.add: tester human-required (17.09525ms)
|
||||
✔ proxy.apikey.add: agent human-required (15.021593ms)
|
||||
✔ proxy.apikey.add: user human-required (15.257844ms)
|
||||
✔ proxy.apikey.add: not-configured (17.124537ms)
|
||||
✔ proxy.priority.set: owner calls page once with business/human scope (14.876553ms)
|
||||
✔ proxy.priority.set: tester human-required (21.351968ms)
|
||||
✔ proxy.priority.set: agent human-required (16.665676ms)
|
||||
✔ proxy.priority.set: user human-required (19.67346ms)
|
||||
✔ proxy.priority.set: not-configured (15.524648ms)
|
||||
✔ proxy.keys: owner calls page once with business/human scope (16.043305ms)
|
||||
✔ proxy.keys: tester human-required (16.990648ms)
|
||||
✔ proxy.keys: agent human-required (15.927183ms)
|
||||
✔ proxy.keys: user human-required (15.10558ms)
|
||||
✔ proxy.keys: not-configured (19.013178ms)
|
||||
✔ API-key canary never enters broker events or unknown error output (18.503151ms)
|
||||
ℹ tests 41
|
||||
ℹ suites 0
|
||||
ℹ pass 41
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 940.684074
|
||||
@@ -0,0 +1,12 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/AwJzc2
|
||||
✔ proxy browser clears write-only key immediately, sends once and renders data as text (752.942588ms)
|
||||
✔ proxy browser OAuth opens safely, callback wait stays pending, status ok completes (2519.227325ms)
|
||||
✔ proxy browser read-only disables account changes (456.893472ms)
|
||||
ℹ tests 3
|
||||
ℹ suites 0
|
||||
ℹ pass 3
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 3803.115926
|
||||
@@ -0,0 +1,11 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/weXu2n
|
||||
✔ API-key canary reaches fake once and zero responses/logs/receipts/store files (38.031356ms)
|
||||
✔ OAuth URLs, state and code stay out of broker records and management receipts (17.551154ms)
|
||||
ℹ tests 2
|
||||
ℹ suites 0
|
||||
ℹ pass 2
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 193.476026
|
||||
@@ -0,0 +1,12 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/67qzFx
|
||||
✔ OAuth management calls keep URL and state out of receipts (16.663673ms)
|
||||
✔ upstream key goes only in request body and upstream refusal body is discarded (3.261477ms)
|
||||
✔ redirected management calls are not followed or retried (1.670589ms)
|
||||
ℹ tests 3
|
||||
ℹ suites 0
|
||||
ℹ pass 3
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 84.19507
|
||||
@@ -0,0 +1,26 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/S7l9sd
|
||||
✔ proxy.login.start: provider-not-allowed (23.09791ms)
|
||||
✔ proxy.apikey.add: provider-not-allowed (0.273041ms)
|
||||
✔ apikey-invalid empty (0.236399ms)
|
||||
✔ apikey-invalid too long (0.22639ms)
|
||||
✔ apikey-invalid charset (0.184503ms)
|
||||
✔ callback-url-invalid not URL (0.382802ms)
|
||||
✔ callback-url-invalid oversize (0.295737ms)
|
||||
✔ callback-url-invalid repeated state (0.23565ms)
|
||||
✔ login-state-unknown callback never reaches proxy (0.284037ms)
|
||||
✔ login state is scoped to the human and business (0.380293ms)
|
||||
✔ accepted callback wait is pending; only a status ok completes and retires state (0.659442ms)
|
||||
✔ expiry reports login-timeout once with no management call and retires state (0.226445ms)
|
||||
✔ cancel calls DELETE and retires state (0.223356ms)
|
||||
✔ upstream OAuth error body never crosses bus response (0.203702ms)
|
||||
✔ priority-invalid {"accountId":"bad","priority":1} (0.168544ms)
|
||||
✔ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1.5} (0.143284ms)
|
||||
✔ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1001} (0.125518ms)
|
||||
ℹ tests 17
|
||||
ℹ suites 0
|
||||
ℹ pass 17
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 77.098574
|
||||
@@ -0,0 +1,20 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/1br07F
|
||||
✔ owner proxy routes each make exactly one allowlisted call (20.950437ms)
|
||||
✔ proxy write refuses signed-out (2.244476ms)
|
||||
✔ proxy write refuses loopback (1.305524ms)
|
||||
✔ proxy write refuses cross-origin (1.036876ms)
|
||||
✔ proxy write refuses missing Fetch Metadata (1.14764ms)
|
||||
✔ all proxy writes are read-only without --send (2.738207ms)
|
||||
✔ granted user never reaches owner page or any proxy route (3.888014ms)
|
||||
✔ native page offers write-only key input and no panel script (1.538835ms)
|
||||
✔ Console caller rejects every non-proxy verb (0.667881ms)
|
||||
✔ tester socket refuses proxy page, reads and writes before any owner call (5.437812ms)
|
||||
✔ malformed API-key JSON never appears in errors or logs (1.287969ms)
|
||||
ℹ tests 11
|
||||
ℹ suites 0
|
||||
ℹ pass 11
|
||||
ℹ fail 0
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 184.38183
|
||||
@@ -0,0 +1,9 @@
|
||||
// Static import graph only: esbuild parses source; no test module executes.
|
||||
import { build } from '../../../../packages/webui/node_modules/esbuild/lib/main.js';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
const entries = ['packages/model-proxy/tests/page-client.test.mjs', 'packages/bus/tests/proxy-page.test.mjs', 'packages/bus/tests/proxy-page-state.test.mjs', 'packages/webui/tests/proxy-page.test.mjs'];
|
||||
const result = await build({ entryPoints: entries, bundle: true, platform: 'node', format: 'esm', packages: 'external',
|
||||
write: false, outdir: '/unused-p4-graph', metafile: true, logLevel: 'silent' });
|
||||
writeFileSync(new URL('./red-import-graph.json', import.meta.url), JSON.stringify(result.metafile.inputs, null, 2) + '\n');
|
||||
for (const file of entries) console.log(file);
|
||||
console.log('Static import graph inputs:', Object.keys(result.metafile.inputs).length);
|
||||
@@ -0,0 +1,53 @@
|
||||
// Slot-only four named mutants. Each child gets its own short TMPDIR and nice19.
|
||||
// Never writes raw child output: canary mutations must not put secrets in receipts.
|
||||
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const root = fileURLToPath(new URL('../../../../', import.meta.url));
|
||||
const page = 'packages/bus/src/proxy-page.mjs', serve = 'packages/webui/src/serve.mjs';
|
||||
const cases = [
|
||||
{ name: 'accounts-unfiltered-field', file: page, before: 'return { accountId, provider, displayName, status, priority };',
|
||||
after: 'return { ...row };', test: 'packages/bus/tests/proxy-page-accounts.test.mjs' },
|
||||
{ name: 'callback-skips-state', file: page, before: "const state = url.searchParams.get('state'); this.#session(state, scope);",
|
||||
after: "const state = url.searchParams.get('state');", test: 'packages/bus/tests/proxy-page-state.test.mjs' },
|
||||
{ name: 'proxy-route-accepts-granted-user', file: serve, before: "if (session?.user !== undefined) {",
|
||||
after: "if (session?.user !== undefined && path.startsWith('/api/proxy/')) return json(res, 200, { accepted: true });\n if (session?.user !== undefined) {",
|
||||
test: 'packages/webui/tests/proxy-page.test.mjs' },
|
||||
{ name: 'apikey-logged', file: page, before: 'await this.#client.putUpstreamGroups([...groups, group]);',
|
||||
after: 'process.stderr.write(args.key); await this.#client.putUpstreamGroups([...groups, group]);',
|
||||
test: 'packages/webui/tests/proxy-canary.test.mjs' },
|
||||
];
|
||||
function run(test) {
|
||||
const temp = execFileSync('mktemp', ['-d', '/mnt/storage/scratch/tmp/XXXXXX'], { encoding: 'utf8' }).trim();
|
||||
try {
|
||||
const command = `exec timeout -k 10 180 nice -n $((19 - $(nice))) bash -c 'printf "nice=%s\\n" "$(nice)"; exec node --test "$1"' bash "$1"`;
|
||||
const result = spawnSync('bash', ['-c', command, 'bash', root + test],
|
||||
{ cwd: root, env: { ...process.env, TMPDIR: temp }, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
|
||||
const out = (result.stdout ?? '') + (result.stderr ?? '');
|
||||
return { exit: result.status, nice: Number(out.match(/^nice=(\d+)/m)?.[1]),
|
||||
failures: Number(out.match(/(?:# fail|ℹ fail)\s+(\d+)/)?.[1] ?? -1),
|
||||
passes: Number(out.match(/(?:# pass|ℹ pass)\s+(\d+)/)?.[1] ?? -1),
|
||||
failingTests: [...out.matchAll(/^✖ (.+?) \([0-9.]+ms\)$/gm)].map(m => m[1]).filter((v, i, all) => all.indexOf(v) === i) };
|
||||
} finally { rmSync(temp, { recursive: true, force: true }); }
|
||||
}
|
||||
let failed = false;
|
||||
for (const mutant of cases) {
|
||||
const file = root + mutant.file, original = readFileSync(file, 'utf8');
|
||||
if (original.split(mutant.before).length !== 2) throw Error('mutant anchor not unique: ' + mutant.name);
|
||||
let result;
|
||||
try {
|
||||
writeFileSync(file, original.replace(mutant.before, mutant.after));
|
||||
result = run(mutant.test);
|
||||
} finally { writeFileSync(file, original); }
|
||||
const killed = result.exit === 1 && result.failures > 0 && result.nice === 19;
|
||||
const restored = readFileSync(file, 'utf8') === original;
|
||||
console.log(JSON.stringify({ mutant: mutant.name, test: mutant.test, ...result, killed, restored,
|
||||
originalSha256: createHash('sha256').update(original).digest('hex') }));
|
||||
if (!killed || !restored) { failed = true; break; }
|
||||
const control = run(mutant.test);
|
||||
const positive = control.exit === 0 && control.failures === 0 && control.passes > 0 && control.nice === 19;
|
||||
console.log(JSON.stringify({ mutant: mutant.name, positiveControl: true, test: mutant.test, ...control, positive }));
|
||||
if (!positive) { failed = true; break; }
|
||||
}
|
||||
process.exitCode = failed ? 1 : 0;
|
||||
@@ -0,0 +1,87 @@
|
||||
// No upstream network. Prints only shapes and allowlisted non-secret metadata.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { getPriority } from 'node:os';
|
||||
import { initialConfig } from '../../../../packages/model-proxy/src/settings.mjs';
|
||||
import { PIN } from '../../../../packages/model-proxy/src/common.mjs';
|
||||
const [root, binary] = process.argv.slice(2);
|
||||
if (process.env.P4_ISOLATED !== 'yes' || !root?.startsWith('/mnt/storage/scratch/tmp/')) throw Error('wrapper required');
|
||||
if (createHash('sha256').update(fs.readFileSync(binary)).digest('hex') !== PIN.binary.sha256) throw Error('hash mismatch');
|
||||
const reserve = net.createServer();
|
||||
await new Promise(r => reserve.listen(0, '127.0.0.1', r));
|
||||
const port = reserve.address().port;
|
||||
await new Promise(r => reserve.close(r));
|
||||
if (port === 8317) throw Error('forbidden port selected');
|
||||
const key = 'p4-management';
|
||||
const config = initialConfig({ dataRoot: root, port, key, sentinel: 'p4-sentinel' });
|
||||
config.management['disable-control-panel'] = true;
|
||||
config.observability.logs['logging-to-file'] = false;
|
||||
fs.mkdirSync(path.join(root, 'proxy/auth'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'home'));
|
||||
fs.writeFileSync(path.join(root, 'config.yaml'), JSON.stringify(config), { mode: 0o600 });
|
||||
const child = spawn(binary, ['-config', path.join(root, 'config.yaml'), '-local-model'], {
|
||||
cwd: root, env: { HOME: path.join(root, 'home'), PATH: '/usr/bin:/bin' }, stdio: 'ignore',
|
||||
});
|
||||
const pause = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function request(endpoint, method = 'GET', body) {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/v8/management${endpoint}`, {
|
||||
method, headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
||||
body: body === undefined ? undefined : JSON.stringify(body), redirect: 'error', signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
let value; try { value = await response.json(); } catch { value = null; }
|
||||
return { status: response.status, value };
|
||||
}
|
||||
// Structure-only output, not raw credential responses or upstream errors.
|
||||
function shape(value, depth = 0) {
|
||||
if (depth > 5) return '...';
|
||||
if (Array.isArray(value)) return { array: value.length, items: value.slice(0, 2).map(v => shape(v, depth + 1)) };
|
||||
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, shape(v, depth + 1)]));
|
||||
return typeof value;
|
||||
}
|
||||
try {
|
||||
console.log('P4 pinned contract probe version', PIN.version, 'nice', getPriority());
|
||||
let ready = false;
|
||||
for (let n = 0; n < 100; n++) { try { await request('/credentials'); ready = true; break; } catch { await pause(50); } }
|
||||
if (!ready) throw Error('fixture listener absent');
|
||||
const list = await request('/credentials');
|
||||
console.log('GET /credentials', list.status, JSON.stringify(shape(list.value)));
|
||||
for (const provider of ['claude', 'codex']) {
|
||||
const r = await request('/oauth/auth-url?provider=' + provider);
|
||||
console.log('GET /oauth/auth-url provider=' + provider, r.status, JSON.stringify(shape(r.value)),
|
||||
'valid URL/state', typeof r.value?.url === 'string', typeof r.value?.state === 'string');
|
||||
if (typeof r.value?.state === 'string') {
|
||||
const cancelled = await request('/oauth/session?state=' + encodeURIComponent(r.value.state), 'DELETE');
|
||||
console.log('DELETE /oauth/session state=[redacted]', cancelled.status, JSON.stringify(shape(cancelled.value)));
|
||||
}
|
||||
}
|
||||
// Probe-pending best reading of config example: z.ai uses Claude-compatible group.
|
||||
const groups = [{ name: 'z.ai', 'base-url': 'https://api.z.ai/api/anthropic', priority: 0,
|
||||
keys: [{ 'api-key': 'p4-upstream' }] }];
|
||||
const added = await request('/config/api-keys/claude', 'PUT', groups);
|
||||
console.log('PUT /config/api-keys/claude body=[{name,base-url,priority,keys:[{api-key}]}]',
|
||||
added.status, JSON.stringify(shape(added.value)));
|
||||
await pause(300);
|
||||
const accounts = await request('/credentials');
|
||||
console.log('GET /credentials after add', accounts.status, JSON.stringify(shape(accounts.value)));
|
||||
const configGroups = await request('/config/api-keys/claude');
|
||||
console.log('GET /config/api-keys/claude', configGroups.status, JSON.stringify(shape(configGroups.value)));
|
||||
const rows = Array.isArray(accounts.value) ? accounts.value : accounts.value?.files ?? accounts.value?.credentials ?? [];
|
||||
console.log('credential safe fields', JSON.stringify(rows.map(r => ({
|
||||
identifierField: ['id', 'name', 'auth_index'].filter(k => typeof r[k] === 'string'),
|
||||
provider: ['claude', 'codex', 'anthropic', 'openai-compatibility'].includes(r.provider) ? r.provider : '[other]',
|
||||
status: ['ready', 'active', 'error', 'disabled', 'unavailable'].includes(r.status) ? r.status : '[other]',
|
||||
priority: Number.isSafeInteger(r.priority) ? r.priority : null,
|
||||
}))));
|
||||
if (rows[0]) {
|
||||
for (const body of [{ name: rows[0].name, priority: 7 }, { name: rows[0].name, fields: { priority: 7 } }]) {
|
||||
const result = await request('/credentials/fields', 'PATCH', body);
|
||||
console.log('PATCH /credentials/fields keys=' + Object.keys(body).join(','), result.status, JSON.stringify(shape(result.value)));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
child.kill('SIGTERM');
|
||||
await Promise.race([new Promise(r => child.once('exit', r)), pause(3000).then(() => child.kill('SIGKILL'))]);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Offline pinned contract probe, slot required; no host proxy access.
|
||||
set -euo pipefail
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
BINARY=${1:?verified P1 fetched binary required}
|
||||
export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)
|
||||
timeout -k 10 100 nice -n $((19 - $(nice))) unshare -rn bash -c '
|
||||
set -e
|
||||
ip link set lo up
|
||||
export P4_ISOLATED=yes
|
||||
exec node "$1/probe.mjs" "$TMPDIR" "$2"
|
||||
' bash "$HERE" "$BINARY"
|
||||
@@ -0,0 +1,10 @@
|
||||
P4 pinned contract probe version 8.0.42 nice 19
|
||||
GET /credentials 200 {"files":{"array":0,"items":[]},"observed_at":"string"}
|
||||
GET /oauth/auth-url provider=claude 200 {"state":"string","status":"string","url":"string"} valid URL/state true true
|
||||
DELETE /oauth/session state=[redacted] 200 {"cancelled":"boolean","status":"string"}
|
||||
GET /oauth/auth-url provider=codex 200 {"state":"string","status":"string","url":"string"} valid URL/state true true
|
||||
DELETE /oauth/session state=[redacted] 200 {"cancelled":"boolean","status":"string"}
|
||||
PUT /config/api-keys/claude body=[{name,base-url,priority,keys:[{api-key}]}] 200 {"config-version":"number","status":"string"}
|
||||
GET /credentials after add 200 {"files":{"array":0,"items":[]},"observed_at":"string"}
|
||||
GET /config/api-keys/claude 200 {"array":1,"items":[{"base-url":"string","keys":{"array":1,"items":[{"api-key":"string","auth_index":"string"}]},"name":"string","priority":"number"}]}
|
||||
credential safe fields []
|
||||
@@ -0,0 +1,114 @@
|
||||
// No upstream network. Prints only shapes and allowlisted non-secret metadata.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import net from 'node:net';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { getPriority } from 'node:os';
|
||||
import { initialConfig } from '../../../../packages/model-proxy/src/settings.mjs';
|
||||
import { PIN } from '../../../../packages/model-proxy/src/common.mjs';
|
||||
const [root, binary] = process.argv.slice(2);
|
||||
if (process.env.P4_ISOLATED !== 'yes' || !root?.startsWith('/mnt/storage/scratch/tmp/')) throw Error('wrapper required');
|
||||
if (createHash('sha256').update(fs.readFileSync(binary)).digest('hex') !== PIN.binary.sha256) throw Error('hash mismatch');
|
||||
const reserve = net.createServer();
|
||||
await new Promise(r => reserve.listen(0, '127.0.0.1', r));
|
||||
const port = reserve.address().port;
|
||||
await new Promise(r => reserve.close(r));
|
||||
if (port === 8317) throw Error('forbidden port selected');
|
||||
const key = 'p4-management';
|
||||
const config = initialConfig({ dataRoot: root, port, key, sentinel: 'p4-sentinel' });
|
||||
config.management['disable-control-panel'] = true;
|
||||
config.observability.logs['logging-to-file'] = false;
|
||||
fs.mkdirSync(path.join(root, 'proxy/auth'), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, 'home'));
|
||||
fs.writeFileSync(path.join(root, 'config.yaml'), JSON.stringify(config), { mode: 0o600 });
|
||||
const child = spawn(binary, ['-config', path.join(root, 'config.yaml'), '-local-model'], {
|
||||
cwd: root, env: { HOME: path.join(root, 'home'), PATH: '/usr/bin:/bin' }, stdio: 'ignore',
|
||||
});
|
||||
const pause = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function request(endpoint, method = 'GET', body) {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/v8/management${endpoint}`, {
|
||||
method, headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
||||
body: body === undefined ? undefined : JSON.stringify(body), redirect: 'error', signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
let value; try { value = await response.json(); } catch { value = null; }
|
||||
return { status: response.status, value };
|
||||
}
|
||||
// Structure-only output, not raw credential responses or upstream errors.
|
||||
function shape(value, depth = 0) {
|
||||
if (depth > 5) return '...';
|
||||
if (Array.isArray(value)) return { array: value.length, items: value.slice(0, 2).map(v => shape(v, depth + 1)) };
|
||||
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, shape(v, depth + 1)]));
|
||||
return typeof value;
|
||||
}
|
||||
try {
|
||||
console.log('P4 pinned contract probe2 version', PIN.version, 'nice', getPriority());
|
||||
let ready = false;
|
||||
for (let n = 0; n < 100; n++) { try { await request('/credentials'); ready = true; break; } catch { await pause(50); } }
|
||||
if (!ready) throw Error('fixture listener absent');
|
||||
const list = await request('/credentials');
|
||||
console.log('GET /credentials', list.status, JSON.stringify(shape(list.value)));
|
||||
// Probe-pending best reading of config example: z.ai uses Claude-compatible group.
|
||||
const groups = [{ name: 'z.ai', 'base-url': 'https://api.z.ai/api/anthropic', priority: 0,
|
||||
keys: [{ 'api-key': 'p4-upstream' }] }];
|
||||
const added = await request('/config/api-keys/claude', 'PUT', groups);
|
||||
console.log('PUT /config/api-keys/claude body=[{name,base-url,priority,keys:[{api-key}]}]',
|
||||
added.status, JSON.stringify(shape(added.value)));
|
||||
await pause(300);
|
||||
const accounts = await request('/credentials');
|
||||
console.log('GET /credentials after add', accounts.status, JSON.stringify(shape(accounts.value)));
|
||||
const configGroups = await request('/config/api-keys/claude');
|
||||
console.log('GET /config/api-keys/claude', configGroups.status, JSON.stringify(shape(configGroups.value)));
|
||||
const rows = Array.isArray(accounts.value) ? accounts.value : accounts.value?.files ?? accounts.value?.credentials ?? [];
|
||||
console.log('credential safe fields', JSON.stringify(rows.map(r => ({
|
||||
identifierField: ['id', 'name', 'auth_index'].filter(k => typeof r[k] === 'string'),
|
||||
provider: ['claude', 'codex', 'anthropic', 'openai-compatibility'].includes(r.provider) ? r.provider : '[other]',
|
||||
status: ['ready', 'active', 'error', 'disabled', 'unavailable'].includes(r.status) ? r.status : '[other]',
|
||||
priority: Number.isSafeInteger(r.priority) ? r.priority : null,
|
||||
}))));
|
||||
// Raw JSON credential upload, per the v0-compatible credentials upload contract.
|
||||
const credential = { type: 'claude', access_token: 'p4-access', refresh_token: 'p4-refresh',
|
||||
expired: '2099-01-01T00:00:00Z', email: '[email protected]' };
|
||||
const upload = await request('/credentials?name=p4-oauth.json', 'POST', credential);
|
||||
console.log('POST /credentials?name=[fixture] body={type,access_token,refresh_token,expired,email}',
|
||||
upload.status, JSON.stringify(shape(upload.value)));
|
||||
if (upload.status >= 300) {
|
||||
const form = new FormData(); form.append('file', new Blob([JSON.stringify(credential)], { type: 'application/json' }), 'p4-oauth.json');
|
||||
const response = await fetch(`http://127.0.0.1:${port}/v8/management/credentials`, {
|
||||
method: 'POST', headers: { Authorization: `Bearer ${key}` }, body: form, signal: AbortSignal.timeout(5000), redirect: 'error',
|
||||
});
|
||||
let value; try { value = await response.json(); } catch { value = null; }
|
||||
console.log('POST /credentials multipart file=[fixture]', response.status, JSON.stringify(shape(value)));
|
||||
}
|
||||
await pause(300);
|
||||
const populated = await request('/credentials');
|
||||
console.log('GET /credentials populated', populated.status, JSON.stringify(shape(populated.value)));
|
||||
const files = populated.value?.files ?? [];
|
||||
const upstreamId = configGroups.value?.[0]?.keys?.[0]?.auth_index;
|
||||
const targets = [
|
||||
...files.flatMap(row => ['id', 'name', 'auth_index'].filter(field => typeof row[field] === 'string').map(field => ({ field, value: row[field], kind: 'oauth' }))),
|
||||
...(typeof upstreamId === 'string' ? [{ field: 'auth_index', value: upstreamId, kind: 'api-key' }] : []),
|
||||
];
|
||||
for (const target of targets) {
|
||||
const candidates = [
|
||||
{ [target.field]: target.value, priority: 7 },
|
||||
{ [target.field]: target.value, fields: { priority: 8 } },
|
||||
{ name: target.value, priority: 9 },
|
||||
];
|
||||
for (const body of candidates) {
|
||||
const result = await request('/credentials/fields', 'PATCH', body);
|
||||
console.log('PATCH /credentials/fields target=' + target.kind + ':' + target.field + ' shape=' + Object.keys(body).join(','),
|
||||
result.status, JSON.stringify(shape(result.value)));
|
||||
}
|
||||
}
|
||||
const after = await request('/credentials');
|
||||
console.log('GET /credentials after PATCH', after.status, JSON.stringify(shape(after.value)),
|
||||
'priorityChanged', (after.value?.files ?? []).some(row => [7, 8, 9].includes(row.priority)));
|
||||
const configAfter = await request('/config/api-keys/claude');
|
||||
console.log('GET provider groups after PATCH', configAfter.status, JSON.stringify(shape(configAfter.value)),
|
||||
'priorityChanged', (configAfter.value ?? []).some(g => [7, 8, 9].includes(g.priority) || (g.keys ?? []).some(k => [7, 8, 9].includes(k.priority))));
|
||||
|
||||
} finally {
|
||||
child.kill('SIGTERM');
|
||||
await Promise.race([new Promise(r => child.once('exit', r)), pause(3000).then(() => child.kill('SIGKILL'))]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Offline pinned contract probe, slot required; no host proxy access.
|
||||
set -euo pipefail
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
BINARY=${1:?verified P1 fetched binary required}
|
||||
export TMPDIR=$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)
|
||||
trap 'rm -rf -- "$TMPDIR"' EXIT
|
||||
timeout -k 10 100 nice -n $((19 - $(nice))) unshare -rn bash -c '
|
||||
set -e
|
||||
ip link set lo up
|
||||
export P4_ISOLATED=yes
|
||||
exec node "$1/probe2.mjs" "$TMPDIR" "$2"
|
||||
' bash "$HERE" "$BINARY"
|
||||
@@ -0,0 +1,22 @@
|
||||
P4 pinned contract probe2 version 8.0.42 nice 19
|
||||
GET /credentials 200 {"files":{"array":0,"items":[]},"observed_at":"string"}
|
||||
PUT /config/api-keys/claude body=[{name,base-url,priority,keys:[{api-key}]}] 200 {"config-version":"number","status":"string"}
|
||||
GET /credentials after add 200 {"files":{"array":0,"items":[]},"observed_at":"string"}
|
||||
GET /config/api-keys/claude 200 {"array":1,"items":[{"base-url":"string","keys":{"array":1,"items":[{"api-key":"string","auth_index":"string"}]},"name":"string","priority":"number"}]}
|
||||
credential safe fields []
|
||||
POST /credentials?name=[fixture] body={type,access_token,refresh_token,expired,email} 200 {"status":"string"}
|
||||
GET /credentials populated 200 {"files":{"array":1,"items":[{"account":"string","account_type":"string","auth_index":"string","cooldowns":{"array":0,"items":[]},"created_at":"string","disabled":"boolean","email":"string","failed":"number","id":"string","label":"string","modtime":"string","name":"string","path":"string","provider":"string","quota":{"signals":{}},"recent_requests":{"array":20,"items":[{"time":"string","success":"number","failed":"number"},{"time":"string","success":"number","failed":"number"}]},"runtime_only":"boolean","size":"number","source":"string","status":"string","status_message":"string","success":"number","type":"string","unavailable":"boolean","updated_at":"string"}]},"observed_at":"string"}
|
||||
PATCH /credentials/fields target=oauth:id shape=id,priority 400 {"error":"string"}
|
||||
PATCH /credentials/fields target=oauth:id shape=id,fields 400 {"error":"string"}
|
||||
PATCH /credentials/fields target=oauth:id shape=name,priority 200 {"status":"string"}
|
||||
PATCH /credentials/fields target=oauth:name shape=name,priority 200 {"status":"string"}
|
||||
PATCH /credentials/fields target=oauth:name shape=name,fields 200 {"status":"string"}
|
||||
PATCH /credentials/fields target=oauth:name shape=name,priority 200 {"status":"string"}
|
||||
PATCH /credentials/fields target=oauth:auth_index shape=auth_index,priority 400 {"error":"string"}
|
||||
PATCH /credentials/fields target=oauth:auth_index shape=auth_index,fields 400 {"error":"string"}
|
||||
PATCH /credentials/fields target=oauth:auth_index shape=name,priority 404 {"error":"string"}
|
||||
PATCH /credentials/fields target=api-key:auth_index shape=auth_index,priority 400 {"error":"string"}
|
||||
PATCH /credentials/fields target=api-key:auth_index shape=auth_index,fields 400 {"error":"string"}
|
||||
PATCH /credentials/fields target=api-key:auth_index shape=name,priority 404 {"error":"string"}
|
||||
GET /credentials after PATCH 200 {"files":{"array":1,"items":[{"account":"string","account_type":"string","auth_index":"string","cooldowns":{"array":0,"items":[]},"created_at":"string","disabled":"boolean","email":"string","failed":"number","id":"string","label":"string","modtime":"string","name":"string","path":"string","priority":"number","provider":"string","quota":{"signals":{}},"recent_requests":{"array":20,"items":[{"time":"string","success":"number","failed":"number"},{"time":"string","success":"number","failed":"number"}]},"runtime_only":"boolean","size":"number","source":"string","status":"string","status_message":"string","success":"number","type":"string","unavailable":"boolean","updated_at":"string"}]},"observed_at":"string"} priorityChanged true
|
||||
GET provider groups after PATCH 200 {"array":1,"items":[{"base-url":"string","keys":{"array":1,"items":[{"api-key":"string","auth_index":"string"}]},"name":"string","priority":"number"}]} priorityChanged false
|
||||
@@ -0,0 +1,123 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/pt6DmK
|
||||
✔ accounts have exactly the allowlisted public fields, id from credential identifier (17.187313ms)
|
||||
✔ token-like data in every upstream field never reaches public accounts (3.45079ms)
|
||||
✖ API key account is derived from proxy auth_index, never api-key, and can set priority (6.1703ms)
|
||||
✔ OAuth priority uses top-level name/priority and confirms it through list (3.250505ms)
|
||||
✖ priority-unconfirmed: accepted API-key write with unchanged read-back refuses (2.950156ms)
|
||||
✖ priority-write-refused: OAuth PATCH status 201 (2.781711ms)
|
||||
✖ priority-write-refused: OAuth PATCH status 400 (2.04605ms)
|
||||
✔ priority-invalid: unknown account refuses before any write (1.909709ms)
|
||||
✔ proxy-down: a closed management listener refuses once (1.102581ms)
|
||||
✔ client keys return ids, sentinel/foreign labels and only scoped record metadata (1.844816ms)
|
||||
ℹ tests 10
|
||||
ℹ suites 0
|
||||
ℹ pass 6
|
||||
ℹ fail 4
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 113.531979
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/proxy-page-accounts.test.mjs:30:1
|
||||
✖ API key account is derived from proxy auth_index, never api-key, and can set priority (6.1703ms)
|
||||
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:79:7)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-accounts.test.mjs:38:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
code: 'proxy-down'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-accounts.test.mjs:49:1
|
||||
✖ priority-unconfirmed: accepted API-key write with unchanged read-back refuses (2.950156ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
Comparison {
|
||||
+ code: 'proxy-down'
|
||||
- code: 'priority-unconfirmed'
|
||||
}
|
||||
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-accounts.test.mjs:53:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 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:79:7)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async waitForActual (node:assert:615:5)
|
||||
at async strict.rejects (node:assert:738:25)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-accounts.test.mjs:53:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
|
||||
expected: { code: 'priority-unconfirmed' },
|
||||
operator: 'rejects',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-accounts.test.mjs:55:34
|
||||
✖ priority-write-refused: OAuth PATCH status 201 (2.781711ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
Comparison {
|
||||
+ code: 'proxy-down'
|
||||
- code: 'priority-write-refused'
|
||||
}
|
||||
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-accounts.test.mjs:57:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 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:79:7)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async waitForActual (node:assert:615:5)
|
||||
at async strict.rejects (node:assert:738:25)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-accounts.test.mjs:57:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
|
||||
expected: { code: 'priority-write-refused' },
|
||||
operator: 'rejects',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-accounts.test.mjs:55:34
|
||||
✖ priority-write-refused: OAuth PATCH status 400 (2.04605ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal:
|
||||
+ actual - expected
|
||||
|
||||
Comparison {
|
||||
+ code: 'proxy-down'
|
||||
- code: 'priority-write-refused'
|
||||
}
|
||||
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-accounts.test.mjs:57:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 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:79:7)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async waitForActual (node:assert:615:5)
|
||||
at async strict.rejects (node:assert:738:25)
|
||||
at async TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-accounts.test.mjs:57:3)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7),
|
||||
expected: { code: 'priority-write-refused' },
|
||||
operator: 'rejects',
|
||||
diff: 'simple'
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,74 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/CRNBdQ
|
||||
✖ OAuth management calls keep URL and state out of receipts (9.455556ms)
|
||||
✖ upstream key goes only in request body and upstream refusal body is discarded (2.01764ms)
|
||||
✖ redirected management calls are not followed or retried (1.212233ms)
|
||||
ℹ tests 3
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 90.353377
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/model-proxy/tests/page-client.test.mjs:21:1
|
||||
✖ OAuth management calls keep URL and state out of receipts (9.455556ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'undefined'
|
||||
- 'function'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/model-proxy/tests/page-client.test.mjs:23:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'undefined',
|
||||
expected: 'function',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/model-proxy/tests/page-client.test.mjs:35:1
|
||||
✖ upstream key goes only in request body and upstream refusal body is discarded (2.01764ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'undefined'
|
||||
- 'function'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/model-proxy/tests/page-client.test.mjs:37:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'undefined',
|
||||
expected: 'function',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/model-proxy/tests/page-client.test.mjs:44:1
|
||||
✖ redirected management calls are not followed or retried (1.212233ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'undefined'
|
||||
- 'function'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/model-proxy/tests/page-client.test.mjs:46:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'undefined',
|
||||
expected: 'function',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
packages/model-proxy/tests/page-client.test.mjs
|
||||
packages/bus/tests/proxy-page.test.mjs
|
||||
packages/bus/tests/proxy-page-state.test.mjs
|
||||
packages/webui/tests/proxy-page.test.mjs
|
||||
Static import graph inputs: 101
|
||||
@@ -0,0 +1,317 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/eiwzVG
|
||||
✖ proxy.login.start: provider-not-allowed (4.926207ms)
|
||||
✖ proxy.apikey.add: provider-not-allowed (0.296109ms)
|
||||
✖ apikey-invalid empty (0.245874ms)
|
||||
✖ apikey-invalid too long (0.268842ms)
|
||||
✖ apikey-invalid charset (0.241913ms)
|
||||
✖ callback-url-invalid not URL (0.315678ms)
|
||||
✖ callback-url-invalid oversize (0.231909ms)
|
||||
✖ callback-url-invalid repeated state (0.248707ms)
|
||||
✖ login-state-unknown callback never reaches proxy (0.289965ms)
|
||||
✖ login state is scoped to the human and business (0.425279ms)
|
||||
✖ accepted callback wait is pending; only a status ok completes and retires state (0.952019ms)
|
||||
✖ expiry reports login-timeout once with no management call and retires state (0.254797ms)
|
||||
✖ cancel calls DELETE and retires state (0.18904ms)
|
||||
✖ upstream OAuth error body never crosses bus response (0.195322ms)
|
||||
✖ priority-invalid {"accountId":"bad","priority":1} (0.147898ms)
|
||||
✖ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1.5} (0.122621ms)
|
||||
✖ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1001} (0.116367ms)
|
||||
ℹ tests 17
|
||||
ℹ suites 0
|
||||
ℹ pass 0
|
||||
ℹ fail 17
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 66.888172
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:26:4
|
||||
✖ proxy.login.start: provider-not-allowed (4.926207ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:26:4
|
||||
✖ proxy.apikey.add: provider-not-allowed (0.296109ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:30:3
|
||||
✖ apikey-invalid empty (0.245874ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:30:3
|
||||
✖ apikey-invalid too long (0.268842ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:30:3
|
||||
✖ apikey-invalid charset (0.241913ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:35:3
|
||||
✖ callback-url-invalid not URL (0.315678ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:35:3
|
||||
✖ callback-url-invalid oversize (0.231909ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:35:3
|
||||
✖ callback-url-invalid repeated state (0.248707ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:40:1
|
||||
✖ login-state-unknown callback never reaches proxy (0.289965ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:45:1
|
||||
✖ login state is scoped to the human and business (0.425279ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:51:1
|
||||
✖ accepted callback wait is pending; only a status ok completes and retires state (0.952019ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:60:1
|
||||
✖ expiry reports login-timeout once with no management call and retires state (0.254797ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:66:1
|
||||
✖ cancel calls DELETE and retires state (0.18904ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:72:1
|
||||
✖ upstream OAuth error body never crosses bus response (0.195322ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:78:51
|
||||
✖ priority-invalid {"accountId":"bad","priority":1} (0.147898ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:78:51
|
||||
✖ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1.5} (0.122621ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
|
||||
test at packages/bus/tests/proxy-page-state.test.mjs:78:51
|
||||
✖ priority-invalid {"accountId":"aaaaaaaaaaaa","priority":1001} (0.116367ms)
|
||||
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs' imported from /home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/tests/proxy-page-state.test.mjs
|
||||
at finalizeResolution (node:internal/modules/esm/resolve:272:11)
|
||||
at moduleResolve (node:internal/modules/esm/resolve:879:10)
|
||||
at defaultResolve (node:internal/modules/esm/resolve:1006:11)
|
||||
at #cachedDefaultResolve (node:internal/modules/esm/loader:708:20)
|
||||
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:728:38)
|
||||
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:766:56)
|
||||
at #resolve (node:internal/modules/esm/loader:690:17)
|
||||
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:610:35)
|
||||
at node:internal/modules/esm/loader:639:32
|
||||
at TracingChannel.tracePromise (node:diagnostics_channel:547:22) {
|
||||
code: 'ERR_MODULE_NOT_FOUND',
|
||||
url: 'file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/bus/src/proxy-page.mjs'
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
nice=19 TMPDIR=/mnt/storage/scratch/tmp/zEUUdd
|
||||
✖ owner proxy routes each make exactly one allowlisted call (18.933601ms)
|
||||
✔ proxy write refuses signed-out (3.077488ms)
|
||||
✔ proxy write refuses loopback (1.673695ms)
|
||||
✔ proxy write refuses cross-origin (1.552762ms)
|
||||
✔ proxy write refuses missing Fetch Metadata (1.650964ms)
|
||||
✔ all proxy writes are read-only without --send (4.286107ms)
|
||||
✔ granted user never reaches owner page or any proxy route (5.407776ms)
|
||||
✖ native page offers write-only key input and no panel script (1.561203ms)
|
||||
✖ Console caller rejects every non-proxy verb (1.418096ms)
|
||||
ℹ tests 9
|
||||
ℹ suites 0
|
||||
ℹ pass 6
|
||||
ℹ fail 3
|
||||
ℹ cancelled 0
|
||||
ℹ skipped 0
|
||||
ℹ todo 0
|
||||
ℹ duration_ms 203.343536
|
||||
|
||||
✖ failing tests:
|
||||
|
||||
test at packages/webui/tests/proxy-page.test.mjs:33:1
|
||||
✖ owner proxy routes each make exactly one allowlisted call (18.933601ms)
|
||||
AssertionError [ERR_ASSERTION]: accounts
|
||||
|
||||
404 !== 200
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/webui/tests/proxy-page.test.mjs:37:12)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
|
||||
generatedMessage: false,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 404,
|
||||
expected: 200,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/proxy-page.test.mjs:68:1
|
||||
✖ native page offers write-only key input and no panel script (1.561203ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
|
||||
404 !== 200
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/webui/tests/proxy-page.test.mjs:71:10)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.run (node:internal/test_runner/test:1409:7)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 404,
|
||||
expected: 200,
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
|
||||
test at packages/webui/tests/proxy-page.test.mjs:76:1
|
||||
✖ Console caller rejects every non-proxy verb (1.418096ms)
|
||||
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
|
||||
+ actual - expected
|
||||
|
||||
+ 'undefined'
|
||||
- 'function'
|
||||
|
||||
at TestContext.<anonymous> (file:///home/jwoltje/.t3/worktrees/mosaic-stack/poe-row330/packages/webui/tests/proxy-page.test.mjs:79:10)
|
||||
at Test.runInAsyncScope (node:async_hooks:226:14)
|
||||
at Test.run (node:internal/test_runner/test:1402:25)
|
||||
at Test.processPendingSubtests (node:internal/test_runner/test:974:18)
|
||||
at Test.postRun (node:internal/test_runner/test:1542:19)
|
||||
at Test.run (node:internal/test_runner/test:1467:12)
|
||||
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
|
||||
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
|
||||
generatedMessage: true,
|
||||
code: 'ERR_ASSERTION',
|
||||
actual: 'undefined',
|
||||
expected: 'function',
|
||||
operator: 'strictEqual',
|
||||
diff: 'simple'
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"base": "78b69e1e6",
|
||||
"refactor": "775bbe236",
|
||||
"parsedInputs": 111,
|
||||
"changedPaths": 45,
|
||||
"overlap": []
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
BUILD-LOG.md
|
||||
agents/herb/work/queue-214/BUILD.md
|
||||
agents/herb/work/queue-214/DESIGN.md
|
||||
agents/herb/work/queue-214/EVIDENCE.json
|
||||
agents/herb/work/queue-214/HASH-PINS.json
|
||||
agents/herb/work/queue-214/MANIFEST.md
|
||||
agents/herb/work/queue-214/SOURCE.json
|
||||
agents/herb/work/queue-214/granted-4286/baseline-red.log
|
||||
agents/herb/work/queue-214/granted-4286/build.log
|
||||
agents/herb/work/queue-214/granted-4286/editor-browser-green.log
|
||||
agents/herb/work/queue-214/granted-4286/editor-green.log
|
||||
agents/herb/work/queue-214/granted-4286/final-sweep.json
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-build.log
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-red.log
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-restore-build.log
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-restore-green.log
|
||||
agents/herb/work/queue-214/granted-4286/measurement.log
|
||||
agents/herb/work/queue-214/granted-4286/mutants.json
|
||||
agents/herb/work/queue-214/granted-4286/mutants.py
|
||||
agents/herb/work/queue-214/granted-4286/npm-ci.log
|
||||
agents/herb/work/queue-214/granted-4286/post-mutant-sweep.json
|
||||
agents/herb/work/queue-214/granted-4286/provenance-green.log
|
||||
agents/herb/work/queue-214/granted-4286/results.jsonl
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-build.log
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-red.log
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-restore-build.log
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-restore-green.log
|
||||
agents/herb/work/queue-214/granted-4286/settings.json
|
||||
agents/herb/work/queue-214/granted-4286/skips.json
|
||||
agents/herb/work/queue-214/granted-4286/step.py
|
||||
agents/herb/work/queue-214/granted-4286/webui-69.log
|
||||
agents/herb/work/queue-214/granted-4286/webui-files.txt
|
||||
agents/herb/work/queue-214/granted-4286/webui-whole.log
|
||||
agents/herb/work/queue-214/granted-4286/wikilink-green.log
|
||||
agents/herb/work/queue-214/measure.mjs
|
||||
agents/herb/work/queue-214/measurement.json
|
||||
agents/morgana/work/queue-193/DESIGN.md
|
||||
docs/SESSIONS.md
|
||||
docs/plans/QUEUE.md
|
||||
docs/plans/queue.json
|
||||
packages/webui/editor/BUILD.json
|
||||
packages/webui/editor/model.mjs
|
||||
packages/webui/src/public/editor.js
|
||||
packages/webui/tests/editor-wikilink.test.mjs
|
||||
packages/webui/tests/helpers/wikilink-cases.mjs
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"base": "78b69e1e6",
|
||||
"refactor": "e1d8756b5",
|
||||
"parsedInputs": 111,
|
||||
"changedPaths": 47,
|
||||
"overlap": []
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
BUILD-LOG.md
|
||||
agents/herb/work/queue-214/BUILD.md
|
||||
agents/herb/work/queue-214/DESIGN.md
|
||||
agents/herb/work/queue-214/EVIDENCE.json
|
||||
agents/herb/work/queue-214/HASH-PINS.json
|
||||
agents/herb/work/queue-214/MANIFEST.md
|
||||
agents/herb/work/queue-214/SOURCE.json
|
||||
agents/herb/work/queue-214/granted-4286/baseline-red.log
|
||||
agents/herb/work/queue-214/granted-4286/build.log
|
||||
agents/herb/work/queue-214/granted-4286/editor-browser-green.log
|
||||
agents/herb/work/queue-214/granted-4286/editor-green.log
|
||||
agents/herb/work/queue-214/granted-4286/final-sweep.json
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-build.log
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-red.log
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-restore-build.log
|
||||
agents/herb/work/queue-214/granted-4286/invalid-escaping-restore-green.log
|
||||
agents/herb/work/queue-214/granted-4286/measurement.log
|
||||
agents/herb/work/queue-214/granted-4286/mutants.json
|
||||
agents/herb/work/queue-214/granted-4286/mutants.py
|
||||
agents/herb/work/queue-214/granted-4286/npm-ci.log
|
||||
agents/herb/work/queue-214/granted-4286/post-mutant-sweep.json
|
||||
agents/herb/work/queue-214/granted-4286/provenance-green.log
|
||||
agents/herb/work/queue-214/granted-4286/results.jsonl
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-build.log
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-red.log
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-restore-build.log
|
||||
agents/herb/work/queue-214/granted-4286/serializer-removal-restore-green.log
|
||||
agents/herb/work/queue-214/granted-4286/settings.json
|
||||
agents/herb/work/queue-214/granted-4286/skips.json
|
||||
agents/herb/work/queue-214/granted-4286/step.py
|
||||
agents/herb/work/queue-214/granted-4286/webui-69.log
|
||||
agents/herb/work/queue-214/granted-4286/webui-files.txt
|
||||
agents/herb/work/queue-214/granted-4286/webui-whole.log
|
||||
agents/herb/work/queue-214/granted-4286/wikilink-green.log
|
||||
agents/herb/work/queue-214/measure.mjs
|
||||
agents/herb/work/queue-214/measurement.json
|
||||
agents/launchpad/work/queue-255/DESIGN.md
|
||||
agents/morgana/work/queue-193/DESIGN.md
|
||||
docs/SESSIONS.md
|
||||
docs/plans/2026-10-25_prd-1-18-brief.md
|
||||
docs/plans/QUEUE.md
|
||||
docs/plans/queue.json
|
||||
packages/webui/editor/BUILD.json
|
||||
packages/webui/editor/model.mjs
|
||||
packages/webui/src/public/editor.js
|
||||
packages/webui/tests/editor-wikilink.test.mjs
|
||||
packages/webui/tests/helpers/wikilink-cases.mjs
|
||||
@@ -0,0 +1,2 @@
|
||||
P4 offline upgrade version=8.0.42 nice=16
|
||||
P4 upgrade FAILED code=acceptance-failed
|
||||
@@ -1694,3 +1694,5 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-24 | magica | row329 R2 #1827 | ASCII management-key fix and docs verified: package66/66,bus readers261/261,CLI readers726/726,config55/55; no required failures/skips, slot returned. Preparing authorized ff review candidate; old tests unchanged.
|
||||
2026-10-24 | Sage 1ef1e4f8 | row329 landing #1827 (mos-1634) | Gate GREEN 23:29:14Z unchanged, landed 61ebf2c34 pushed 23:31:27Z; #1827 closed c31650; queue note 4063, done 4064 (b24d917ec); DEFERRED N2/R2-N1 to 331, N1 to P7; Vikunja 631 filed, 237 c1109; 284 and 157 must rebase.
|
||||
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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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') 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 };
|
||||
}
|
||||
@@ -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' });
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 <verified-fetched-executable>`
|
||||
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.
|
||||
@@ -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,32 @@ 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;
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
<button id="processing-resume" type="button" class="btn" disabled hidden>Resume processing</button>
|
||||
<p id="processing-unavailable" class="processing-note small muted">Processing controls are unavailable here. Use mosaic halt or mosaic resume from the instance CLI.</p>
|
||||
<p id="processing-receipt" class="processing-receipt small" aria-live="polite"></p></div>
|
||||
<h2 class="tree-title">Console</h2><ul class="s1-sections" id="sections"></ul><div id="board-tree"><h2 class="tree-title">Projects</h2><div id="projects"><p class="muted">Loading projects…</p></div><p class="small muted">Select a project to filter sessions. Waiting on you always shows all projects.</p></div></nav>
|
||||
<p><a href="/proxy">Model proxy</a></p><h2 class="tree-title">Console</h2><ul class="s1-sections" id="sections"></ul><div id="board-tree"><h2 class="tree-title">Projects</h2><div id="projects"><p class="muted">Loading projects…</p></div><p class="small muted">Select a project to filter sessions. Waiting on you always shows all projects.</p></div></nav>
|
||||
<main id="main" class="content" tabindex="-1"><div id="signed-out" class="banner ref" role="alert" tabindex="-1" hidden></div><div id="bus-down" class="banner err" role="alert" hidden></div><div class="page-head" id="board-head"><div><h1>Control board</h1><p id="status" class="small muted" role="status">Loading board…</p></div><div class="filters"><label><input id="show-machine" type="checkbox">Show machine sessions</label></div></div>
|
||||
<section id="held-section" aria-labelledby="held-title"><h2 id="held-title">Held roles <span id="held-count" class="count">–</span></h2><p class="small muted">The stack's seats: each role a run holds on the bus, from the bus <code>agents</code> read. Machine sessions are under Show machine sessions.</p><div id="held"><div class="skel-rows" aria-busy="true"><span class="sr-only">Reading the held roles.</span><span class="skel"></span><span class="skel"></span></div></div></section>
|
||||
<div id="error" class="banner err" role="alert" hidden></div>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-design="console" data-palette="harbor">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Model proxy · Mosaic Console</title>
|
||||
<link rel="stylesheet" href="/shared/app.css"><link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/console.css"><link rel="stylesheet" href="/shell.css">
|
||||
<script src="/proxy.js" defer></script></head>
|
||||
<body><a class="skip" href="#main">Skip to content</a>
|
||||
<header class="cmdbar"><a class="wordmark" href="/">Mosaic<small>Console</small></a><a href="/">Back to Console</a></header>
|
||||
<main id="main" class="content" tabindex="-1">
|
||||
<div class="page-head"><div><h1>Model proxy</h1><p>Accounts used by this instance, their priority, and the client keys assigned to agents.</p></div><button id="proxy-refresh" type="button">Refresh</button></div>
|
||||
<p id="proxy-message" role="status" aria-live="polite">Reading accounts…</p>
|
||||
<p id="proxy-readonly" class="muted" hidden>This Console is read-only. Sign-in and account changes are unavailable.</p>
|
||||
<section class="panel" aria-labelledby="proxy-accounts-title"><h2 id="proxy-accounts-title">Accounts</h2>
|
||||
<p>Higher priority is used first. Account names are stable identifiers; credential values are never displayed.</p>
|
||||
<div class="table-wrap" tabindex="0" role="region" aria-label="Accounts"><table><thead><tr><th>Account</th><th>Provider</th><th>Status</th><th>Priority</th></tr></thead><tbody id="proxy-accounts"></tbody></table></div></section>
|
||||
<section class="panel" aria-labelledby="proxy-signin-title"><h2 id="proxy-signin-title">Sign in to an account</h2>
|
||||
<form id="proxy-login-start"><fieldset data-write disabled><legend>Subscription account</legend><label>Provider <select name="provider"><option value="claude">Claude</option><option value="codex">Codex</option></select></label><button type="submit">Start sign-in</button></fieldset></form>
|
||||
<p id="proxy-login-message" role="status" aria-live="polite"></p>
|
||||
<p><a id="proxy-authorize" target="_blank" rel="noopener noreferrer" hidden>Open account sign-in in a new tab</a></p>
|
||||
<form id="proxy-login-submit"><fieldset data-write disabled><legend>Finish sign-in</legend><p>If the sign-in tab does not return, copy its full address and paste it here.</p><label>Callback address <input name="redirect_url" type="url" autocomplete="off" spellcheck="false" maxlength="8192" required></label><button type="submit">Submit address</button><button id="proxy-login-cancel" type="button">Cancel sign-in</button></fieldset></form></section>
|
||||
<section class="panel" aria-labelledby="proxy-apikey-title"><h2 id="proxy-apikey-title">Add an API key</h2>
|
||||
<form id="proxy-apikey"><fieldset data-write disabled><legend>z.ai</legend><label>API key <input name="key" type="password" autocomplete="off" spellcheck="false" maxlength="4096" required></label><p>The key is sent once and cleared from this field. It will never be shown again.</p><button type="submit">Add key</button></fieldset></form></section>
|
||||
<section class="panel" aria-labelledby="proxy-keys-title"><h2 id="proxy-keys-title">Client keys</h2><p>Only key IDs are shown. Hire and fire manage agent keys.</p>
|
||||
<div class="table-wrap" tabindex="0" role="region" aria-label="Client keys"><table><thead><tr><th>Key ID</th><th>Managed by</th><th>Agent</th><th>Issued</th><th>State</th></tr></thead><tbody id="proxy-keys"></tbody></table></div></section>
|
||||
<section class="panel" id="proxy-usage" aria-labelledby="proxy-usage-title"><h2 id="proxy-usage-title">Usage</h2><p id="proxy-usage-status">Usage reporting will appear here when its view is available.</p></section>
|
||||
</main></body></html>
|
||||
@@ -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));
|
||||
})();
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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: '<img src=x onerror="window.injected=true">', 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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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, []);
|
||||
});
|
||||
Reference in new issue
Block a user