1 line
2.0 MiB
Plaintext
1 line
2.0 MiB
Plaintext
{"repo": "androoagi/starnet", "files": {"test/sidecar.http.test.js": {"606027f69a39f104774733d291224243a9fff9ce": {"t": 1785745559.914128, "n": 95398, "h": "3d672e6f558b3348", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // Budget-legibility pass (2026-07-23): the SHIPPED defaults are now perRun $10 with every cross-run pool\n // UNGOVERNED (0). This suite's budget assertions prove ENFORCEMENT mechanics (status pools, one-click\n // resume), so pin the old governed caps via env — deterministic regardless of shipped defaults or a dev\n // machine's ambient SKYNET_BUDGET_* values.\n SKYNET_BUDGET_PER_RUN: '3', SKYNET_BUDGET_PER_AGENT: '5', SKYNET_BUDGET_PER_DAY: '40', SKYNET_BUDGET_GLOBAL: '100',\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const preflightMethods = preflight.headers.get('access-control-allow-methods') || '';\n A.ok(preflightMethods.split(',').map(s => s.trim()).indexOf('DELETE') >= 0, 'trusted preflight advertises the DELETE routes the API actually serves');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n if (rq.url.indexOf('/chat/completions') >= 0) {\n if (rq.headers.authorization !== 'Bearer probe-good-key') { rs.writeHead(401); return rs.end('rejected'); }\n rs.writeHead(200, { 'Content-Type': 'text/event-stream' });\n rs.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'OK' } }] }) + '\\n\\n');\n rs.write('data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\\n\\n');\n rs.write('data: [DONE]\\n\\n'); return rs.end();\n }\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n const wrongCandidate = await j('POST', '/api/providers/validate', { provider: 'custom', baseUrl: liveBase, key: 'probe-wrong-key', model: 'local/proven-model' });\n A.eq(wrongCandidate.body.credentialVerified, false, 'candidate-key validation rejects a key that fails the inference wire');\n const goodCandidate = await j('POST', '/api/providers/validate', { provider: 'custom', baseUrl: liveBase, key: 'probe-good-key', model: 'local/proven-model' });\n A.eq(goodCandidate.body.credentialVerified, true, 'candidate-key validation proves the exact key on the inference wire');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushOpenAiPool = await fetch(B + '/api/key', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', keyPool: ['openai-backup-a', 'openai-backup-b'] })\n });\n const openAiPoolAck = await pushOpenAiPool.json();\n A.eq(openAiPoolAck.alternateCount, 2, 'OpenAI alternate pool is accepted only under the OpenAI provider id');\n A.ok(JSON.stringify(openAiPoolAck).indexOf('openai-backup') < 0, 'alternate-pool acknowledgement never echoes a secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const channelsFile = path.join(ws, 'channels', 'secrets.json');\n const signalBeforeMalformed = fs.readFileSync(channelsFile, 'utf8');\n const signalBadJson = await fetch(B + '/api/channels/signal/disconnect', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': apiToken }, body: '{bad'\n });\n A.eq(signalBadJson.status, 400, 'Signal disconnect with malformed JSON -> 400');\n A.eq(fs.readFileSync(channelsFile, 'utf8'), signalBeforeMalformed, 'malformed Signal disconnect leaves durable configuration byte-identical');\n const signalStillSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalStillSaved.body.configured, true, 'malformed Signal disconnect does not stop or deconfigure the channel');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n // Prefix families stop only at a URL segment boundary. Sibling aliases must not inherit a real handler,\n // while the exact/query forms above and the real /api/models/<provider> child route remain live.\n for (const p of ['/api/toolsets-typo', '/api/saveXYZ', '/api/runsXYZ', '/api/subagentsXYZ', '/api/skillsXYZ', '/api/connectors/oauth/callbackevil']) {\n const alias = await fetch(B + p, { headers: tok });\n A.eq(alias.status, 404, 'segment-unsafe route alias ' + p + ' -> 404');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n const cancelBadJson = await fetch(B + '/api/cancel', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(cancelBadJson.status, 400, 'POST /api/cancel with malformed JSON -> 400');\n\n const spotifyStart = await j('POST', '/api/spotify/auth/start', { clientId: 'spotify-http-test-client' });\n A.eq(spotifyStart.status, 200, 'Spotify auth start accepts a valid client id without network I/O');\n const spotifyFile = path.join(ws, '.secrets', 'spotify.json');\n const spotifyBeforeMalformed = fs.readFileSync(spotifyFile, 'utf8');\n const spotifyBadJson = await fetch(B + '/api/spotify/auth/start', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': apiToken }, body: '{not json'\n });\n A.eq(spotifyBadJson.status, 400, 'Spotify auth start with malformed JSON -> 400 even when a client id is already saved');\n A.eq(fs.readFileSync(spotifyFile, 'utf8'), spotifyBeforeMalformed, 'malformed Spotify auth start leaves durable OAuth configuration byte-identical');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n // needle pins the GUARANTEE (a .saved item 409s before the write), not the local variable name that holds it —\n // the durable pending queue renamed the batch lookup to `live`, and a lock that breaks on a rename teaches\n // nothing about whether the guarantee survived.\n const guard = idxSrc.indexOf('.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // PENDING QUEUE (durable, cross-run): unattended runs reflect now, so a high-stakes deck can be raised with\n // nobody watching. The route is the surface that keeps it answerable — it must exist, be agent-gated, and\n // report an EMPTY deck honestly rather than 404-ing or inventing rows.\n const pend = await j('GET', '/api/memory/pending?agent=agent');\n A.eq(pend.status, 200, 'GET /api/memory/pending -> 200');\n A.eq(pend.body.agentId, 'agent', 'the pending deck is reported per agent');\n A.eq(pend.body.pending, [], 'a station with nothing awaiting a verdict reports an empty deck');\n const pendBad = await j('GET', '/api/memory/pending?agent=..%2Fetc');\n A.eq(pendBad.status, 403, 'a bad agent id is refused, not path-joined');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n /* ⛔ \"no key\" IS ONLY THE TRUTH WHEN THERE IS ALSO NO KEYLESS ENGINE. The STT ladder gained a LOCAL floor\n (tier 4) so a station whose brain is a ChatGPT/Codex or Anthropic subscription is not simply deaf — none\n of those credentials is a transcription key. Where that floor is installed, answering \"no key\" would name\n the wrong cause: the station HAS ears, this clip was just undecodable. So ask the station what it can\n actually do and hold it to the matching truth — asserting one fixed string would force a lie in whichever\n world it was not written for, and dropping to \"some reason exists\" would guard nothing. */\n const localVoice = await j('GET', '/api/local-voice/status');\n const canHearKeyless = !!(localVoice.body && localVoice.body.available);\n if (canHearKeyless) {\n A.ok(/local/i.test(sttNoKey.body.reason || ''), 'with a keyless local engine the degrade names THAT tier failing, not a missing key');\n A.ok(!/no key/i.test(sttNoKey.body.reason || ''), 'and never blames a key on a station that can transcribe without one');\n } else {\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'with no engine and no key the STT degrade names the no-key reason');\n }\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const ssmlSeen = []; // every SSML request the loopback served — carries the <voice name='…'> actually asked for\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { const t = payload.toString('utf8'); if (op === 0x1 && t.includes('Path:ssml')) { ssmlSeen.push(t); respond(); } }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n A.ok(/^edge:/.test(r2.headers.get('x-voice-provider') || ''), 'the serving tier names itself (X-Voice-Provider: edge:…)');\n\n /* ---- THE SHIPPED SHAPE: local:true where the offline engine cannot load (every installed build —\n the bundle ships no node_modules; reproduced here with the STARNET_LOCAL_VOICE=0 kill-switch).\n ⛔ THE BUG THIS LOCKS OUT (2026-07-30, found by ear): this request used to fall through into the\n KEYED provider chain, so live voice silently spoke with a different provider's identity and the\n voice picker adjusted an engine that was not there. Now the picked voice maps onto the nearest\n Edge neural — the SSML the loopback captures is the proof of which voice was actually requested,\n and the mapped name differing from Edge's own default (Christopher) is what discriminates the fix\n from the old fallthrough, with no key and no network. */\n {\n const shipWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-ship-'));\n const sb = await boot(port + 350, shipWs, 20, {\n STARNET_LOCAL_VOICE: '0', SKYNET_LOCAL_VOICE: '0',\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const SB = 'http://' + HOST + ':' + sb.port;\n try {\n const stok = await bootToken(SB, SB);\n const H = { 'Content-Type': 'application/json', 'X-StarNet-Token': stok, Origin: SB };\n const lvs = await (await fetch(SB + '/api/local-voice/status', { headers: H })).json();\n A.eq(lvs.available, false, 'the kill-switch presents the installed-build truth through the route');\n const postLocal = (t, v) => fetch(SB + '/api/tts', { method: 'POST', headers: H, body: JSON.stringify({ text: t, local: true, localVoice: v }) });\n const g = await postLocal('shipped george check', 'bm_george');\n A.eq(g.status, 200, 'local:true with the engine absent still serves (200)');\n A.eq(g.headers.get('x-voice-provider'), 'edge:en-GB-RyanNeural', 'the PICKED voice maps to its Edge neural — never the keyed chain, never Edge\\'s own default');\n A.eq(Buffer.from(await g.arrayBuffer()).equals(audioPayload), true, 'and the audio really came from the Edge floor');\n A.ok(ssmlSeen.some(s => s.indexOf(\"name='en-GB-RyanNeural'\") >= 0), 'the wire asked Edge for the mapped voice by name');\n const b = await postLocal('shipped bella check', 'af_bella');\n A.eq(b.headers.get('x-voice-provider'), 'edge:en-US-AriaNeural', 'a different pick maps differently — the picker is LIVE on an installed build, not decorative');\n const u = await postLocal('shipped unknown check', 'not-a-voice');\n A.eq(u.headers.get('x-voice-provider'), 'edge:en-US-ChristopherNeural', 'an unknown pick maps through the default voice, never to another provider');\n } finally {\n try { sb.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(shipWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "ac2017206a0a632d95e3564f91bee9c042a77036": {"t": 1785754192.1757889, "n": 95398, "h": "3d672e6f558b3348", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // Budget-legibility pass (2026-07-23): the SHIPPED defaults are now perRun $10 with every cross-run pool\n // UNGOVERNED (0). This suite's budget assertions prove ENFORCEMENT mechanics (status pools, one-click\n // resume), so pin the old governed caps via env — deterministic regardless of shipped defaults or a dev\n // machine's ambient SKYNET_BUDGET_* values.\n SKYNET_BUDGET_PER_RUN: '3', SKYNET_BUDGET_PER_AGENT: '5', SKYNET_BUDGET_PER_DAY: '40', SKYNET_BUDGET_GLOBAL: '100',\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const preflightMethods = preflight.headers.get('access-control-allow-methods') || '';\n A.ok(preflightMethods.split(',').map(s => s.trim()).indexOf('DELETE') >= 0, 'trusted preflight advertises the DELETE routes the API actually serves');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n if (rq.url.indexOf('/chat/completions') >= 0) {\n if (rq.headers.authorization !== 'Bearer probe-good-key') { rs.writeHead(401); return rs.end('rejected'); }\n rs.writeHead(200, { 'Content-Type': 'text/event-stream' });\n rs.write('data: ' + JSON.stringify({ choices: [{ delta: { content: 'OK' } }] }) + '\\n\\n');\n rs.write('data: ' + JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) + '\\n\\n');\n rs.write('data: [DONE]\\n\\n'); return rs.end();\n }\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n const wrongCandidate = await j('POST', '/api/providers/validate', { provider: 'custom', baseUrl: liveBase, key: 'probe-wrong-key', model: 'local/proven-model' });\n A.eq(wrongCandidate.body.credentialVerified, false, 'candidate-key validation rejects a key that fails the inference wire');\n const goodCandidate = await j('POST', '/api/providers/validate', { provider: 'custom', baseUrl: liveBase, key: 'probe-good-key', model: 'local/proven-model' });\n A.eq(goodCandidate.body.credentialVerified, true, 'candidate-key validation proves the exact key on the inference wire');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushOpenAiPool = await fetch(B + '/api/key', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', keyPool: ['openai-backup-a', 'openai-backup-b'] })\n });\n const openAiPoolAck = await pushOpenAiPool.json();\n A.eq(openAiPoolAck.alternateCount, 2, 'OpenAI alternate pool is accepted only under the OpenAI provider id');\n A.ok(JSON.stringify(openAiPoolAck).indexOf('openai-backup') < 0, 'alternate-pool acknowledgement never echoes a secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const channelsFile = path.join(ws, 'channels', 'secrets.json');\n const signalBeforeMalformed = fs.readFileSync(channelsFile, 'utf8');\n const signalBadJson = await fetch(B + '/api/channels/signal/disconnect', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': apiToken }, body: '{bad'\n });\n A.eq(signalBadJson.status, 400, 'Signal disconnect with malformed JSON -> 400');\n A.eq(fs.readFileSync(channelsFile, 'utf8'), signalBeforeMalformed, 'malformed Signal disconnect leaves durable configuration byte-identical');\n const signalStillSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalStillSaved.body.configured, true, 'malformed Signal disconnect does not stop or deconfigure the channel');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n // Prefix families stop only at a URL segment boundary. Sibling aliases must not inherit a real handler,\n // while the exact/query forms above and the real /api/models/<provider> child route remain live.\n for (const p of ['/api/toolsets-typo', '/api/saveXYZ', '/api/runsXYZ', '/api/subagentsXYZ', '/api/skillsXYZ', '/api/connectors/oauth/callbackevil']) {\n const alias = await fetch(B + p, { headers: tok });\n A.eq(alias.status, 404, 'segment-unsafe route alias ' + p + ' -> 404');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n const cancelBadJson = await fetch(B + '/api/cancel', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(cancelBadJson.status, 400, 'POST /api/cancel with malformed JSON -> 400');\n\n const spotifyStart = await j('POST', '/api/spotify/auth/start', { clientId: 'spotify-http-test-client' });\n A.eq(spotifyStart.status, 200, 'Spotify auth start accepts a valid client id without network I/O');\n const spotifyFile = path.join(ws, '.secrets', 'spotify.json');\n const spotifyBeforeMalformed = fs.readFileSync(spotifyFile, 'utf8');\n const spotifyBadJson = await fetch(B + '/api/spotify/auth/start', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': apiToken }, body: '{not json'\n });\n A.eq(spotifyBadJson.status, 400, 'Spotify auth start with malformed JSON -> 400 even when a client id is already saved');\n A.eq(fs.readFileSync(spotifyFile, 'utf8'), spotifyBeforeMalformed, 'malformed Spotify auth start leaves durable OAuth configuration byte-identical');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n // needle pins the GUARANTEE (a .saved item 409s before the write), not the local variable name that holds it —\n // the durable pending queue renamed the batch lookup to `live`, and a lock that breaks on a rename teaches\n // nothing about whether the guarantee survived.\n const guard = idxSrc.indexOf('.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // PENDING QUEUE (durable, cross-run): unattended runs reflect now, so a high-stakes deck can be raised with\n // nobody watching. The route is the surface that keeps it answerable — it must exist, be agent-gated, and\n // report an EMPTY deck honestly rather than 404-ing or inventing rows.\n const pend = await j('GET', '/api/memory/pending?agent=agent');\n A.eq(pend.status, 200, 'GET /api/memory/pending -> 200');\n A.eq(pend.body.agentId, 'agent', 'the pending deck is reported per agent');\n A.eq(pend.body.pending, [], 'a station with nothing awaiting a verdict reports an empty deck');\n const pendBad = await j('GET', '/api/memory/pending?agent=..%2Fetc');\n A.eq(pendBad.status, 403, 'a bad agent id is refused, not path-joined');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n /* ⛔ \"no key\" IS ONLY THE TRUTH WHEN THERE IS ALSO NO KEYLESS ENGINE. The STT ladder gained a LOCAL floor\n (tier 4) so a station whose brain is a ChatGPT/Codex or Anthropic subscription is not simply deaf — none\n of those credentials is a transcription key. Where that floor is installed, answering \"no key\" would name\n the wrong cause: the station HAS ears, this clip was just undecodable. So ask the station what it can\n actually do and hold it to the matching truth — asserting one fixed string would force a lie in whichever\n world it was not written for, and dropping to \"some reason exists\" would guard nothing. */\n const localVoice = await j('GET', '/api/local-voice/status');\n const canHearKeyless = !!(localVoice.body && localVoice.body.available);\n if (canHearKeyless) {\n A.ok(/local/i.test(sttNoKey.body.reason || ''), 'with a keyless local engine the degrade names THAT tier failing, not a missing key');\n A.ok(!/no key/i.test(sttNoKey.body.reason || ''), 'and never blames a key on a station that can transcribe without one');\n } else {\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'with no engine and no key the STT degrade names the no-key reason');\n }\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const ssmlSeen = []; // every SSML request the loopback served — carries the <voice name='…'> actually asked for\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { const t = payload.toString('utf8'); if (op === 0x1 && t.includes('Path:ssml')) { ssmlSeen.push(t); respond(); } }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n A.ok(/^edge:/.test(r2.headers.get('x-voice-provider') || ''), 'the serving tier names itself (X-Voice-Provider: edge:…)');\n\n /* ---- THE SHIPPED SHAPE: local:true where the offline engine cannot load (every installed build —\n the bundle ships no node_modules; reproduced here with the STARNET_LOCAL_VOICE=0 kill-switch).\n ⛔ THE BUG THIS LOCKS OUT (2026-07-30, found by ear): this request used to fall through into the\n KEYED provider chain, so live voice silently spoke with a different provider's identity and the\n voice picker adjusted an engine that was not there. Now the picked voice maps onto the nearest\n Edge neural — the SSML the loopback captures is the proof of which voice was actually requested,\n and the mapped name differing from Edge's own default (Christopher) is what discriminates the fix\n from the old fallthrough, with no key and no network. */\n {\n const shipWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-ship-'));\n const sb = await boot(port + 350, shipWs, 20, {\n STARNET_LOCAL_VOICE: '0', SKYNET_LOCAL_VOICE: '0',\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const SB = 'http://' + HOST + ':' + sb.port;\n try {\n const stok = await bootToken(SB, SB);\n const H = { 'Content-Type': 'application/json', 'X-StarNet-Token': stok, Origin: SB };\n const lvs = await (await fetch(SB + '/api/local-voice/status', { headers: H })).json();\n A.eq(lvs.available, false, 'the kill-switch presents the installed-build truth through the route');\n const postLocal = (t, v) => fetch(SB + '/api/tts', { method: 'POST', headers: H, body: JSON.stringify({ text: t, local: true, localVoice: v }) });\n const g = await postLocal('shipped george check', 'bm_george');\n A.eq(g.status, 200, 'local:true with the engine absent still serves (200)');\n A.eq(g.headers.get('x-voice-provider'), 'edge:en-GB-RyanNeural', 'the PICKED voice maps to its Edge neural — never the keyed chain, never Edge\\'s own default');\n A.eq(Buffer.from(await g.arrayBuffer()).equals(audioPayload), true, 'and the audio really came from the Edge floor');\n A.ok(ssmlSeen.some(s => s.indexOf(\"name='en-GB-RyanNeural'\") >= 0), 'the wire asked Edge for the mapped voice by name');\n const b = await postLocal('shipped bella check', 'af_bella');\n A.eq(b.headers.get('x-voice-provider'), 'edge:en-US-AriaNeural', 'a different pick maps differently — the picker is LIVE on an installed build, not decorative');\n const u = await postLocal('shipped unknown check', 'not-a-voice');\n A.eq(u.headers.get('x-voice-provider'), 'edge:en-US-ChristopherNeural', 'an unknown pick maps through the default voice, never to another provider');\n } finally {\n try { sb.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(shipWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "e169ab9c3a220695002aa27c7224a9a83193fe73": {"t": 1785754194.7534685, "n": 93735, "h": "35685025c04d319a", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // Budget-legibility pass (2026-07-23): the SHIPPED defaults are now perRun $10 with every cross-run pool\n // UNGOVERNED (0). This suite's budget assertions prove ENFORCEMENT mechanics (status pools, one-click\n // resume), so pin the old governed caps via env — deterministic regardless of shipped defaults or a dev\n // machine's ambient SKYNET_BUDGET_* values.\n SKYNET_BUDGET_PER_RUN: '3', SKYNET_BUDGET_PER_AGENT: '5', SKYNET_BUDGET_PER_DAY: '40', SKYNET_BUDGET_GLOBAL: '100',\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const preflightMethods = preflight.headers.get('access-control-allow-methods') || '';\n A.ok(preflightMethods.split(',').map(s => s.trim()).indexOf('DELETE') >= 0, 'trusted preflight advertises the DELETE routes the API actually serves');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const channelsFile = path.join(ws, 'channels', 'secrets.json');\n const signalBeforeMalformed = fs.readFileSync(channelsFile, 'utf8');\n const signalBadJson = await fetch(B + '/api/channels/signal/disconnect', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': apiToken }, body: '{bad'\n });\n A.eq(signalBadJson.status, 400, 'Signal disconnect with malformed JSON -> 400');\n A.eq(fs.readFileSync(channelsFile, 'utf8'), signalBeforeMalformed, 'malformed Signal disconnect leaves durable configuration byte-identical');\n const signalStillSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalStillSaved.body.configured, true, 'malformed Signal disconnect does not stop or deconfigure the channel');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n // Prefix families stop only at a URL segment boundary. Sibling aliases must not inherit a real handler,\n // while the exact/query forms above and the real /api/models/<provider> child route remain live.\n for (const p of ['/api/toolsets-typo', '/api/saveXYZ', '/api/runsXYZ', '/api/subagentsXYZ', '/api/skillsXYZ', '/api/connectors/oauth/callbackevil']) {\n const alias = await fetch(B + p, { headers: tok });\n A.eq(alias.status, 404, 'segment-unsafe route alias ' + p + ' -> 404');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n const cancelBadJson = await fetch(B + '/api/cancel', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(cancelBadJson.status, 400, 'POST /api/cancel with malformed JSON -> 400');\n\n const spotifyStart = await j('POST', '/api/spotify/auth/start', { clientId: 'spotify-http-test-client' });\n A.eq(spotifyStart.status, 200, 'Spotify auth start accepts a valid client id without network I/O');\n const spotifyFile = path.join(ws, '.secrets', 'spotify.json');\n const spotifyBeforeMalformed = fs.readFileSync(spotifyFile, 'utf8');\n const spotifyBadJson = await fetch(B + '/api/spotify/auth/start', {\n method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': apiToken }, body: '{not json'\n });\n A.eq(spotifyBadJson.status, 400, 'Spotify auth start with malformed JSON -> 400 even when a client id is already saved');\n A.eq(fs.readFileSync(spotifyFile, 'utf8'), spotifyBeforeMalformed, 'malformed Spotify auth start leaves durable OAuth configuration byte-identical');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n // needle pins the GUARANTEE (a .saved item 409s before the write), not the local variable name that holds it —\n // the durable pending queue renamed the batch lookup to `live`, and a lock that breaks on a rename teaches\n // nothing about whether the guarantee survived.\n const guard = idxSrc.indexOf('.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // PENDING QUEUE (durable, cross-run): unattended runs reflect now, so a high-stakes deck can be raised with\n // nobody watching. The route is the surface that keeps it answerable — it must exist, be agent-gated, and\n // report an EMPTY deck honestly rather than 404-ing or inventing rows.\n const pend = await j('GET', '/api/memory/pending?agent=agent');\n A.eq(pend.status, 200, 'GET /api/memory/pending -> 200');\n A.eq(pend.body.agentId, 'agent', 'the pending deck is reported per agent');\n A.eq(pend.body.pending, [], 'a station with nothing awaiting a verdict reports an empty deck');\n const pendBad = await j('GET', '/api/memory/pending?agent=..%2Fetc');\n A.eq(pendBad.status, 403, 'a bad agent id is refused, not path-joined');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n /* ⛔ \"no key\" IS ONLY THE TRUTH WHEN THERE IS ALSO NO KEYLESS ENGINE. The STT ladder gained a LOCAL floor\n (tier 4) so a station whose brain is a ChatGPT/Codex or Anthropic subscription is not simply deaf — none\n of those credentials is a transcription key. Where that floor is installed, answering \"no key\" would name\n the wrong cause: the station HAS ears, this clip was just undecodable. So ask the station what it can\n actually do and hold it to the matching truth — asserting one fixed string would force a lie in whichever\n world it was not written for, and dropping to \"some reason exists\" would guard nothing. */\n const localVoice = await j('GET', '/api/local-voice/status');\n const canHearKeyless = !!(localVoice.body && localVoice.body.available);\n if (canHearKeyless) {\n A.ok(/local/i.test(sttNoKey.body.reason || ''), 'with a keyless local engine the degrade names THAT tier failing, not a missing key');\n A.ok(!/no key/i.test(sttNoKey.body.reason || ''), 'and never blames a key on a station that can transcribe without one');\n } else {\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'with no engine and no key the STT degrade names the no-key reason');\n }\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const ssmlSeen = []; // every SSML request the loopback served — carries the <voice name='…'> actually asked for\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { const t = payload.toString('utf8'); if (op === 0x1 && t.includes('Path:ssml')) { ssmlSeen.push(t); respond(); } }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n A.ok(/^edge:/.test(r2.headers.get('x-voice-provider') || ''), 'the serving tier names itself (X-Voice-Provider: edge:…)');\n\n /* ---- THE SHIPPED SHAPE: local:true where the offline engine cannot load (every installed build —\n the bundle ships no node_modules; reproduced here with the STARNET_LOCAL_VOICE=0 kill-switch).\n ⛔ THE BUG THIS LOCKS OUT (2026-07-30, found by ear): this request used to fall through into the\n KEYED provider chain, so live voice silently spoke with a different provider's identity and the\n voice picker adjusted an engine that was not there. Now the picked voice maps onto the nearest\n Edge neural — the SSML the loopback captures is the proof of which voice was actually requested,\n and the mapped name differing from Edge's own default (Christopher) is what discriminates the fix\n from the old fallthrough, with no key and no network. */\n {\n const shipWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-ship-'));\n const sb = await boot(port + 350, shipWs, 20, {\n STARNET_LOCAL_VOICE: '0', SKYNET_LOCAL_VOICE: '0',\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const SB = 'http://' + HOST + ':' + sb.port;\n try {\n const stok = await bootToken(SB, SB);\n const H = { 'Content-Type': 'application/json', 'X-StarNet-Token': stok, Origin: SB };\n const lvs = await (await fetch(SB + '/api/local-voice/status', { headers: H })).json();\n A.eq(lvs.available, false, 'the kill-switch presents the installed-build truth through the route');\n const postLocal = (t, v) => fetch(SB + '/api/tts', { method: 'POST', headers: H, body: JSON.stringify({ text: t, local: true, localVoice: v }) });\n const g = await postLocal('shipped george check', 'bm_george');\n A.eq(g.status, 200, 'local:true with the engine absent still serves (200)');\n A.eq(g.headers.get('x-voice-provider'), 'edge:en-GB-RyanNeural', 'the PICKED voice maps to its Edge neural — never the keyed chain, never Edge\\'s own default');\n A.eq(Buffer.from(await g.arrayBuffer()).equals(audioPayload), true, 'and the audio really came from the Edge floor');\n A.ok(ssmlSeen.some(s => s.indexOf(\"name='en-GB-RyanNeural'\") >= 0), 'the wire asked Edge for the mapped voice by name');\n const b = await postLocal('shipped bella check', 'af_bella');\n A.eq(b.headers.get('x-voice-provider'), 'edge:en-US-AriaNeural', 'a different pick maps differently — the picker is LIVE on an installed build, not decorative');\n const u = await postLocal('shipped unknown check', 'not-a-voice');\n A.eq(u.headers.get('x-voice-provider'), 'edge:en-US-ChristopherNeural', 'an unknown pick maps through the default voice, never to another provider');\n } finally {\n try { sb.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(shipWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "0b82644a600659cf8b9190c60a34113034eb00aa": {"t": 1785754196.3057168, "n": 91154, "h": "7969edeb22bfa106", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // Budget-legibility pass (2026-07-23): the SHIPPED defaults are now perRun $10 with every cross-run pool\n // UNGOVERNED (0). This suite's budget assertions prove ENFORCEMENT mechanics (status pools, one-click\n // resume), so pin the old governed caps via env — deterministic regardless of shipped defaults or a dev\n // machine's ambient SKYNET_BUDGET_* values.\n SKYNET_BUDGET_PER_RUN: '3', SKYNET_BUDGET_PER_AGENT: '5', SKYNET_BUDGET_PER_DAY: '40', SKYNET_BUDGET_GLOBAL: '100',\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n // needle pins the GUARANTEE (a .saved item 409s before the write), not the local variable name that holds it —\n // the durable pending queue renamed the batch lookup to `live`, and a lock that breaks on a rename teaches\n // nothing about whether the guarantee survived.\n const guard = idxSrc.indexOf('.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // PENDING QUEUE (durable, cross-run): unattended runs reflect now, so a high-stakes deck can be raised with\n // nobody watching. The route is the surface that keeps it answerable — it must exist, be agent-gated, and\n // report an EMPTY deck honestly rather than 404-ing or inventing rows.\n const pend = await j('GET', '/api/memory/pending?agent=agent');\n A.eq(pend.status, 200, 'GET /api/memory/pending -> 200');\n A.eq(pend.body.agentId, 'agent', 'the pending deck is reported per agent');\n A.eq(pend.body.pending, [], 'a station with nothing awaiting a verdict reports an empty deck');\n const pendBad = await j('GET', '/api/memory/pending?agent=..%2Fetc');\n A.eq(pendBad.status, 403, 'a bad agent id is refused, not path-joined');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n /* ⛔ \"no key\" IS ONLY THE TRUTH WHEN THERE IS ALSO NO KEYLESS ENGINE. The STT ladder gained a LOCAL floor\n (tier 4) so a station whose brain is a ChatGPT/Codex or Anthropic subscription is not simply deaf — none\n of those credentials is a transcription key. Where that floor is installed, answering \"no key\" would name\n the wrong cause: the station HAS ears, this clip was just undecodable. So ask the station what it can\n actually do and hold it to the matching truth — asserting one fixed string would force a lie in whichever\n world it was not written for, and dropping to \"some reason exists\" would guard nothing. */\n const localVoice = await j('GET', '/api/local-voice/status');\n const canHearKeyless = !!(localVoice.body && localVoice.body.available);\n if (canHearKeyless) {\n A.ok(/local/i.test(sttNoKey.body.reason || ''), 'with a keyless local engine the degrade names THAT tier failing, not a missing key');\n A.ok(!/no key/i.test(sttNoKey.body.reason || ''), 'and never blames a key on a station that can transcribe without one');\n } else {\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'with no engine and no key the STT degrade names the no-key reason');\n }\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const ssmlSeen = []; // every SSML request the loopback served — carries the <voice name='…'> actually asked for\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { const t = payload.toString('utf8'); if (op === 0x1 && t.includes('Path:ssml')) { ssmlSeen.push(t); respond(); } }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n A.ok(/^edge:/.test(r2.headers.get('x-voice-provider') || ''), 'the serving tier names itself (X-Voice-Provider: edge:…)');\n\n /* ---- THE SHIPPED SHAPE: local:true where the offline engine cannot load (every installed build —\n the bundle ships no node_modules; reproduced here with the STARNET_LOCAL_VOICE=0 kill-switch).\n ⛔ THE BUG THIS LOCKS OUT (2026-07-30, found by ear): this request used to fall through into the\n KEYED provider chain, so live voice silently spoke with a different provider's identity and the\n voice picker adjusted an engine that was not there. Now the picked voice maps onto the nearest\n Edge neural — the SSML the loopback captures is the proof of which voice was actually requested,\n and the mapped name differing from Edge's own default (Christopher) is what discriminates the fix\n from the old fallthrough, with no key and no network. */\n {\n const shipWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-ship-'));\n const sb = await boot(port + 350, shipWs, 20, {\n STARNET_LOCAL_VOICE: '0', SKYNET_LOCAL_VOICE: '0',\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const SB = 'http://' + HOST + ':' + sb.port;\n try {\n const stok = await bootToken(SB, SB);\n const H = { 'Content-Type': 'application/json', 'X-StarNet-Token': stok, Origin: SB };\n const lvs = await (await fetch(SB + '/api/local-voice/status', { headers: H })).json();\n A.eq(lvs.available, false, 'the kill-switch presents the installed-build truth through the route');\n const postLocal = (t, v) => fetch(SB + '/api/tts', { method: 'POST', headers: H, body: JSON.stringify({ text: t, local: true, localVoice: v }) });\n const g = await postLocal('shipped george check', 'bm_george');\n A.eq(g.status, 200, 'local:true with the engine absent still serves (200)');\n A.eq(g.headers.get('x-voice-provider'), 'edge:en-GB-RyanNeural', 'the PICKED voice maps to its Edge neural — never the keyed chain, never Edge\\'s own default');\n A.eq(Buffer.from(await g.arrayBuffer()).equals(audioPayload), true, 'and the audio really came from the Edge floor');\n A.ok(ssmlSeen.some(s => s.indexOf(\"name='en-GB-RyanNeural'\") >= 0), 'the wire asked Edge for the mapped voice by name');\n const b = await postLocal('shipped bella check', 'af_bella');\n A.eq(b.headers.get('x-voice-provider'), 'edge:en-US-AriaNeural', 'a different pick maps differently — the picker is LIVE on an installed build, not decorative');\n const u = await postLocal('shipped unknown check', 'not-a-voice');\n A.eq(u.headers.get('x-voice-provider'), 'edge:en-US-ChristopherNeural', 'an unknown pick maps through the default voice, never to another provider');\n } finally {\n try { sb.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(shipWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "299e7449288f3d690fa7e49954ec1fe0844ae179": {"t": 1785754197.7686036, "n": 87762, "h": "08f9b87a69c7d8a9", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // Budget-legibility pass (2026-07-23): the SHIPPED defaults are now perRun $10 with every cross-run pool\n // UNGOVERNED (0). This suite's budget assertions prove ENFORCEMENT mechanics (status pools, one-click\n // resume), so pin the old governed caps via env — deterministic regardless of shipped defaults or a dev\n // machine's ambient SKYNET_BUDGET_* values.\n SKYNET_BUDGET_PER_RUN: '3', SKYNET_BUDGET_PER_AGENT: '5', SKYNET_BUDGET_PER_DAY: '40', SKYNET_BUDGET_GLOBAL: '100',\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n // needle pins the GUARANTEE (a .saved item 409s before the write), not the local variable name that holds it —\n // the durable pending queue renamed the batch lookup to `live`, and a lock that breaks on a rename teaches\n // nothing about whether the guarantee survived.\n const guard = idxSrc.indexOf('.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // PENDING QUEUE (durable, cross-run): unattended runs reflect now, so a high-stakes deck can be raised with\n // nobody watching. The route is the surface that keeps it answerable — it must exist, be agent-gated, and\n // report an EMPTY deck honestly rather than 404-ing or inventing rows.\n const pend = await j('GET', '/api/memory/pending?agent=agent');\n A.eq(pend.status, 200, 'GET /api/memory/pending -> 200');\n A.eq(pend.body.agentId, 'agent', 'the pending deck is reported per agent');\n A.eq(pend.body.pending, [], 'a station with nothing awaiting a verdict reports an empty deck');\n const pendBad = await j('GET', '/api/memory/pending?agent=..%2Fetc');\n A.eq(pendBad.status, 403, 'a bad agent id is refused, not path-joined');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n /* ⛔ \"no key\" IS ONLY THE TRUTH WHEN THERE IS ALSO NO KEYLESS ENGINE. The STT ladder gained a LOCAL floor\n (tier 4) so a station whose brain is a ChatGPT/Codex or Anthropic subscription is not simply deaf — none\n of those credentials is a transcription key. Where that floor is installed, answering \"no key\" would name\n the wrong cause: the station HAS ears, this clip was just undecodable. So ask the station what it can\n actually do and hold it to the matching truth — asserting one fixed string would force a lie in whichever\n world it was not written for, and dropping to \"some reason exists\" would guard nothing. */\n const localVoice = await j('GET', '/api/local-voice/status');\n const canHearKeyless = !!(localVoice.body && localVoice.body.available);\n if (canHearKeyless) {\n A.ok(/local/i.test(sttNoKey.body.reason || ''), 'with a keyless local engine the degrade names THAT tier failing, not a missing key');\n A.ok(!/no key/i.test(sttNoKey.body.reason || ''), 'and never blames a key on a station that can transcribe without one');\n } else {\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'with no engine and no key the STT degrade names the no-key reason');\n }\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x1 && payload.toString('utf8').includes('Path:ssml')) respond(); }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "a4a72bcb5ea59ffcd4008378d22788ee44cfc172": {"t": 1785754199.3959095, "n": 86622, "h": "a90a3da386f69fee", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // Budget-legibility pass (2026-07-23): the SHIPPED defaults are now perRun $10 with every cross-run pool\n // UNGOVERNED (0). This suite's budget assertions prove ENFORCEMENT mechanics (status pools, one-click\n // resume), so pin the old governed caps via env — deterministic regardless of shipped defaults or a dev\n // machine's ambient SKYNET_BUDGET_* values.\n SKYNET_BUDGET_PER_RUN: '3', SKYNET_BUDGET_PER_AGENT: '5', SKYNET_BUDGET_PER_DAY: '40', SKYNET_BUDGET_GLOBAL: '100',\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n // needle pins the GUARANTEE (a .saved item 409s before the write), not the local variable name that holds it —\n // the durable pending queue renamed the batch lookup to `live`, and a lock that breaks on a rename teaches\n // nothing about whether the guarantee survived.\n const guard = idxSrc.indexOf('.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // PENDING QUEUE (durable, cross-run): unattended runs reflect now, so a high-stakes deck can be raised with\n // nobody watching. The route is the surface that keeps it answerable — it must exist, be agent-gated, and\n // report an EMPTY deck honestly rather than 404-ing or inventing rows.\n const pend = await j('GET', '/api/memory/pending?agent=agent');\n A.eq(pend.status, 200, 'GET /api/memory/pending -> 200');\n A.eq(pend.body.agentId, 'agent', 'the pending deck is reported per agent');\n A.eq(pend.body.pending, [], 'a station with nothing awaiting a verdict reports an empty deck');\n const pendBad = await j('GET', '/api/memory/pending?agent=..%2Fetc');\n A.eq(pendBad.status, 403, 'a bad agent id is refused, not path-joined');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x1 && payload.toString('utf8').includes('Path:ssml')) respond(); }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "4aa207798374da512a2b9d2e3114f00eeadac897": {"t": 1785754200.9558961, "n": 85587, "h": "f8313c79696406a1", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // Budget-legibility pass (2026-07-23): the SHIPPED defaults are now perRun $10 with every cross-run pool\n // UNGOVERNED (0). This suite's budget assertions prove ENFORCEMENT mechanics (status pools, one-click\n // resume), so pin the old governed caps via env — deterministic regardless of shipped defaults or a dev\n // machine's ambient SKYNET_BUDGET_* values.\n SKYNET_BUDGET_PER_RUN: '3', SKYNET_BUDGET_PER_AGENT: '5', SKYNET_BUDGET_PER_DAY: '40', SKYNET_BUDGET_GLOBAL: '100',\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x1 && payload.toString('utf8').includes('Path:ssml')) respond(); }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "4e889eafd40b32b2909f3edf5dcc10c90a36e429": {"t": 1785754202.558821, "n": 85072, "h": "e6b7d47cb1e521f8", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'x-ai', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x1 && payload.toString('utf8').includes('Path:ssml')) respond(); }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "b3491aa9ebcb3b0542cfd9c842313e7f3482cd02": {"t": 1785754204.3573232, "n": 85072, "h": "21de6b1eaa13366f", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n // ---- FORGET = NEVER AGAIN (2026-07-16 resurrect audit): Memory Core's Forget must be as durable as a\n // veto/discard — the removed belief's text joins the SAME permanent declined denylist, or reflection\n // re-mints the deleted fact on a later run and the silent-save writes it straight back. ----\n const forgetNoTok = await fetch(B + '/api/memory/forget', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_2' }) });\n A.eq(forgetNoTok.status, 403, 'POST /api/memory/forget WITHOUT a token -> 403');\n const forget = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forget.status, 200, 'forget of a saved record -> 200');\n const recsPostForget = await j('GET', '/api/memory/records?agent=agent');\n A.ok((recsPostForget.body.records || []).every(r => r.id !== 'note_2'), 'the forgotten record was removed from the notebook');\n const declForget = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declForget.body.declined || []).indexOf('prefers terse answers') >= 0, 'the FORGOTTEN belief joined the permanent declined denylist (forget = never re-minted)');\n const forgetMiss = await j('POST', '/api/memory/forget', { agentId: 'agent', id: 'note_2' });\n A.eq(forgetMiss.status, 404, 'a repeat forget of a gone record -> 404 (nothing to remove)');\n const declForget2 = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declForget2.body.declined || []).filter(t => t === 'prefers terse answers').length, 1, 'a repeat forget appends no duplicate denylist entry');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- DELETE AGENT ⇒ ITS AUTOMATION DIES TOO (2026-07-16 resurrect audit): a deleted agent's cron\n // routines (incl. its away-workshop shift) kept firing forever — spend + ghost sessions under the\n // dead agentId. handleAgentDelete must drop every job bound to the agent; the hero's jobs survive. ----\n {\n // roster the doomed specialist so the create paths accept it as a real agent\n const dPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter' },\n { agentId: 'doomed-7', system: 'worker', name: 'Doomed', provider: 'openrouter', role: 'research' }\n ], updatedAt: Date.now() + 300000 });\n A.eq(dPush.status, 200, 'rostered the doomed specialist');\n const mkJob = await j('POST', '/api/cron', { name: 'Doomed digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'doomed-7' });\n A.eq(mkJob.status, 200, 'created a routine bound to the doomed agent');\n const mkHero = await j('POST', '/api/cron', { name: 'Hero digest', prompt: 'summarize', schedule: 'every 60m', agentId: 'agent' });\n A.eq(mkHero.status, 200, 'created a routine bound to the hero');\n const grant = await j('POST', '/api/workshop/grant', { agentId: 'doomed-7', on: true });\n A.eq(grant.status, 200, 'armed the doomed agent\\'s away-workshop shift (a second cron job, meta.workshop)');\n const cronBefore = await j('GET', '/api/cron');\n A.eq((cronBefore.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 2, 'the doomed agent owns 2 jobs (routine + workshop shift)');\n const del = await j('POST', '/api/agent/delete', { agentId: 'doomed-7' });\n A.eq(del.status, 200, 'POST /api/agent/delete -> 200');\n A.eq(del.body.cronRemoved, 2, 'the delete reports BOTH of the agent\\'s cron jobs removed');\n const cronAfter = await j('GET', '/api/cron');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'doomed-7').length, 0, 'no cron job survives for the deleted agent (nothing left to fire/resurrect)');\n A.eq((cronAfter.body.jobs || []).filter(x => x.agentId === 'agent' && x.name === 'Hero digest').length, 1, 'the hero\\'s routine is untouched');\n // cleanup so later sections see the state they expect\n const heroJob = (cronAfter.body.jobs || []).find(x => x.agentId === 'agent' && x.name === 'Hero digest');\n if (heroJob) await j('POST', '/api/cron/remove', { id: heroJob.id });\n await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }], updatedAt: Date.now() + 400000 });\n }\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x1 && payload.toString('utf8').includes('Path:ssml')) respond(); }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "1fcb5547b21f73fdbc389a1bed63e8d0fab9bbe6": {"t": 1785754205.8627696, "n": 80725, "h": "0b3f32c248176850", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n // PU-07: provider probes distinguish a configured endpoint from a reachable/catalog-backed endpoint.\n const offlineProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' });\n A.eq(offlineProbe.status, 200, 'POST /api/providers/probe returns a fact payload even when the endpoint is offline');\n A.eq(offlineProbe.body.reachable, false, 'offline custom endpoint is not reachable');\n A.eq(offlineProbe.body.catalogAvailable, false, 'offline custom endpoint has no proven catalog');\n const probeServer = http.createServer((rq, rs) => {\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ data: [{ id: 'local/proven-model' }] }));\n });\n await new Promise(resolve => probeServer.listen(0, HOST, resolve));\n try {\n const liveBase = 'http://' + HOST + ':' + probeServer.address().port + '/v1';\n const liveProbe = await j('POST', '/api/providers/probe', { provider: 'custom', baseUrl: liveBase });\n A.eq(liveProbe.body.reachable, true, 'reachable custom endpoint is proven by a real model-catalog round-trip');\n A.eq(liveProbe.body.catalogAvailable, true, 'reachable custom endpoint reports its real non-empty catalog');\n A.eq(liveProbe.body.credentialVerified, true, 'keyless custom endpoint needs no fabricated credential proof');\n } finally { await new Promise(resolve => probeServer.close(resolve)); }\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n\n // PU-06: Signal is tokenless. Removing it destroys endpoint/account configuration, not a fictional token.\n const signalConnect = await j('POST', '/api/channels/signal/connect', {\n endpoint: 'http://127.0.0.1:65529', account: '+15551234567', model: 'local/proven-model',\n provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1', agentId: 'agent', agentName: 'Ultron'\n });\n A.eq(signalConnect.status, 200, 'Signal accepts endpoint/account configuration against an offline bridge');\n const signalSaved = await j('GET', '/api/channels/signal/status');\n A.eq(signalSaved.body.configured, true, 'Signal status is configured from saved endpoint/account');\n const signalRemove = await j('POST', '/api/channels/signal/disconnect', { purge: true });\n A.eq(signalRemove.body.removedConfiguration, true, 'Signal removal is read-back-proven');\n A.eq(signalRemove.body.purged, false, 'Signal removal never claims token purging');\n const signalGone = await j('GET', '/api/channels/signal/status');\n A.eq(signalGone.body.configured, false, 'Signal becomes unconfigured immediately after removal');\n A.eq(signalGone.body.endpoint, '', 'removed Signal status exposes no stale endpoint');\n A.eq(signalGone.body.account, '', 'removed Signal status exposes no stale account');\n const signalDisk = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(Object.prototype.hasOwnProperty.call(signalDisk, 'signal'), false, 'Signal record is absent from durable channel configuration');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x1 && payload.toString('utf8').includes('Path:ssml')) respond(); }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "6a14a266a0b3f49a33e8243aa928918d7a460773": {"t": 1785754207.619565, "n": 77724, "h": "916d37a18735afa5", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst http = require('http');\nconst crypto = require('crypto');\nconst edge = require('../sidecar/edgetts.js'); // V-EDGE: exported WS codec + secMsGec for offline unit + loopback tests\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // V-EDGE: disable the free Edge neural floor in the DEFAULT boot so the no-key TTS test degrades to\n // {fallback:true} deterministically without any network attempt. The dedicated edge-loopback boot below\n // re-enables it (SKYNET_EDGE_TTS + host/port overrides) to prove the round-trip offline.\n SKYNET_EDGE_TTS: '0', STARNET_EDGE_TTS: '0',\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n // ---- V-EDGE unit: secMsGec shape + the exported WS codec round-trips (fragmentation + ping) offline ----\n // fixed injected clock (determinism law: secMsGec takes nowMs), aligned to a 300s-window START so the\n // +stable/+roll offsets below are unambiguous. (windows are on (unix+11644473600) seconds boundaries)\n const gecT = (1784000000 - ((1784000000 + 11644473600) % 300)) * 1000;\n const gec = edge.secMsGec(gecT);\n A.ok(/^[0-9A-F]{64}$/.test(gec), 'secMsGec(nowMs) returns 64 uppercase hex chars');\n A.eq(edge.secMsGec(gecT + 299000), gec, 'secMsGec is stable within the same 300s window');\n A.ok(edge.secMsGec(gecT + 300000) !== gec, 'secMsGec rolls to a new token in the next 300s window');\n {\n // simple masked (client-style) round-trip: one text frame encodes and parses back unchanged\n const frames = [];\n const parse = edge.makeWsParser((op, payload) => frames.push({ op, text: payload.toString('utf8') }), () => {});\n parse(edge.wsEncode(0x1, Buffer.from('hello station', 'utf8')));\n A.eq(frames.length, 1, 'one full masked frame parsed');\n A.eq(frames[0].op, 0x1, 'opcode preserved (text)');\n A.eq(frames[0].text, 'hello station', 'masked payload decodes back to the original bytes');\n }\n {\n // a fragmented message (fin=0 first frame + final continuation) with an interleaved ping, fed ONE byte at\n // a time to exercise the parser's incremental buffering. Server-style unmasked frames (mask:false).\n const got = [], pings = [];\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x9) pings.push(payload.toString('utf8')); else got.push({ op, text: payload.toString('utf8') }); }, () => {});\n const first = edge.wsEncode(0x1, Buffer.from('frag-', 'utf8'), { mask: false }); first[0] &= 0x7f; // clear FIN → not final\n const cont = edge.wsEncode(0x0, Buffer.from('part2', 'utf8'), { mask: false }); // final continuation\n const ping = edge.wsEncode(0x9, Buffer.from('png', 'utf8'), { mask: false });\n const stream = Buffer.concat([ping, first, cont]);\n for (const byte of stream) parse(Buffer.from([byte]));\n A.eq(pings.length, 1, 'the ping frame surfaced to the caller (so it can pong)');\n A.eq(got.length, 1, 'the fragmented message reassembles into exactly one delivered frame');\n A.eq(got[0].op, 0x1, 'reassembled opcode is the initiating text opcode, not the continuation');\n A.eq(got[0].text, 'frag-part2', 'fragmented payload halves are concatenated in order');\n }\n\n // ---- V-EDGE loopback: the free Edge TTS floor round-trips OUR chosen bytes through a local WS server that\n // speaks the same hand-rolled protocol (exported codec), proving handleTts serves neural audio no-key ----\n {\n const audioPayload = Buffer.from([0xff, 0xf3, 0x64, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); // fake mp3 bytes we choose\n const wsServer = http.createServer((rq, rs) => { rs.writeHead(426); rs.end(); });\n wsServer.on('upgrade', (rq, socket) => {\n const wskey = rq.headers['sec-websocket-key'] || '';\n const accept = crypto.createHash('sha1').update(wskey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64');\n socket.write('HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ' + accept + '\\r\\n\\r\\n');\n let sent = false;\n const respond = () => {\n if (sent) return; sent = true;\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.start\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n const hdr = Buffer.from('X-RequestId:mock\\r\\nContent-Type:audio/mpeg\\r\\nPath:audio\\r\\n', 'utf8');\n const hl = Buffer.alloc(2); hl.writeUInt16BE(hdr.length, 0);\n socket.write(edge.wsEncode(0x2, Buffer.concat([hl, hdr, audioPayload]), { mask: false }));\n socket.write(edge.wsEncode(0x1, Buffer.from('X-RequestId:mock\\r\\nPath:turn.end\\r\\n\\r\\n{}', 'utf8'), { mask: false }));\n };\n const parse = edge.makeWsParser((op, payload) => { if (op === 0x1 && payload.toString('utf8').includes('Path:ssml')) respond(); }, () => {});\n socket.on('data', parse);\n socket.on('error', () => {});\n });\n await new Promise(r => wsServer.listen(0, '127.0.0.1', r));\n const wsPort = wsServer.address().port;\n const edgeWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-edge-'));\n const eb = await boot(port + 300, edgeWs, 20, {\n // re-enable the Edge floor (the base env disabled it) and aim it at our loopback WS server over plain http\n SKYNET_EDGE_TTS: '1', STARNET_EDGE_TTS: '1',\n SKYNET_EDGE_TTS_HOST: '127.0.0.1', SKYNET_EDGE_TTS_PORT: String(wsPort), SKYNET_EDGE_TTS_INSECURE: '1'\n });\n const EB = 'http://' + HOST + ':' + eb.port;\n try {\n const etok = await bootToken(EB, EB);\n const post = (t) => fetch(EB + '/api/tts', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-StarNet-Token': etok, Origin: EB }, body: JSON.stringify({ text: t }) });\n const r1 = await post('edge floor round trip');\n A.eq(r1.status, 200, 'edge-floor /api/tts (no key) -> 200');\n A.eq(r1.headers.get('x-voice-cache'), 'miss', 'first edge synth is a cache miss');\n A.ok(/audio\\/mpeg/.test(r1.headers.get('content-type') || ''), 'edge audio is served as audio/mpeg');\n const b1 = Buffer.from(await r1.arrayBuffer());\n A.eq(b1.equals(audioPayload), true, 'the sidecar returned exactly the audio bytes our loopback WS server sent');\n const r2 = await post('edge floor round trip');\n A.eq(r2.status, 200, 'the identical second edge /api/tts -> 200');\n A.eq(r2.headers.get('x-voice-cache'), 'hit', 'an identical second edge request is served from the disk cache');\n const b2 = Buffer.from(await r2.arrayBuffer());\n A.eq(b2.equals(audioPayload), true, 'the cached edge clip is byte-identical');\n } finally {\n try { eb.child.kill(); } catch (_) {}\n // bounded teardown: force any lingering (keep-alive/upgrade) sockets shut so close() can't stall the test.\n try { if (wsServer.closeAllConnections) wsServer.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { wsServer.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(edgeWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- V-STT: the dedicated Groq Whisper ASR branch. A local mock stands in for api.groq.com; the sidecar\n // must POST a multipart body carrying OUR audio bytes + model=whisper-large-v3-turbo, return the text ----\n {\n let seenBody = null, seenCt = '';\n const groqMock = http.createServer((rq, rs) => {\n const chunks = [];\n rq.on('data', c => chunks.push(c));\n rq.on('end', () => {\n seenBody = Buffer.concat(chunks); seenCt = String(rq.headers['content-type'] || '');\n rs.writeHead(200, { 'Content-Type': 'application/json' });\n rs.end(JSON.stringify({ text: 'station systems nominal' }));\n });\n });\n await new Promise(r => groqMock.listen(0, '127.0.0.1', r));\n const groqPort = groqMock.address().port;\n const gWs = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-groq-'));\n const gb = await boot(port + 400, gWs, 20, { GROQ_API_KEY: 'test', SKYNET_GROQ_BASE: 'http://127.0.0.1:' + groqPort });\n const GB = 'http://' + HOST + ':' + gb.port;\n try {\n const gtok = await bootToken(GB, GB);\n const audioBytes = Buffer.from('OPUSFAKEAUDIO-abc123');\n const r = await fetch(GB + '/api/stt', { method: 'POST', headers: { 'Content-Type': 'audio/webm', 'X-StarNet-Token': gtok, Origin: GB }, body: audioBytes });\n A.eq(r.status, 200, 'groq-keyed /api/stt -> 200');\n const body = await r.json();\n A.eq(body.text, 'station systems nominal', 'the transcript comes from the dedicated Groq Whisper branch');\n A.ok(/multipart\\/form-data/.test(seenCt), 'the sidecar sent a multipart/form-data body to Groq');\n A.ok(seenBody && seenBody.includes('whisper-large-v3-turbo'), 'the multipart body carries model=whisper-large-v3-turbo');\n A.ok(seenBody && seenBody.includes('name=\"file\"'), 'the multipart body has a file part named \"file\"');\n A.ok(seenBody && seenBody.indexOf(audioBytes) >= 0, 'the multipart body contains our raw audio bytes verbatim');\n } finally {\n try { gb.child.kill(); } catch (_) {}\n // bounded teardown: the sidecar's keep-alive pool may hold the mock socket open — force it shut so\n // close() returns promptly instead of waiting out the keep-alive timeout.\n try { if (groqMock.closeAllConnections) groqMock.closeAllConnections(); } catch (_) {}\n await new Promise(r => { let done = false; const fin = () => { if (!done) { done = true; r(); } }; try { groqMock.close(fin); } catch (_) { fin(); } setTimeout(fin, 1000); });\n await sleep(150);\n try { fs.rmSync(gWs, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "4712618546a33085687e858a8d6ca11c8e07af09": {"t": 1785754209.1143675, "n": 67905, "h": "803fd1fa342d68c4", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the env keys cleared at spawn, we exercise exactly that path — but\n // /api/tts resolves a credential CHAIN (openrouter → gemini → openai), so first clear the fake OpenAI\n // runtime key the /api/key tests pushed above, or the chain would pick it up and hit the network.\n await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: '' })\n });\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "f5bd756082e06dd69db4f3dbcefd40f767ad763d": {"t": 1785754210.6571755, "n": 67485, "h": "2182b76e03e33434", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENROUTER_API_KEY: '', STARNET_OPENROUTER_API_KEY: '', SKYNET_OPENROUTER_API_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n // /api/tts now resolves a CHAIN of credentials (openrouter → gemini → openai) — clear the Gemini\n // keys too, or an ambient dev key would hit the network and flip the no-key degrade assertions.\n GEMINI_API_KEY: '', STARNET_GEMINI_API_KEY: '', SKYNET_GEMINI_API_KEY: '',\n GOOGLE_API_KEY: '', STARNET_GOOGLE_API_KEY: '', SKYNET_GOOGLE_API_KEY: '',\n GOOGLE_AI_API_KEY: '', STARNET_GOOGLE_AI_API_KEY: '', SKYNET_GOOGLE_AI_API_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "d3ef5f19a75f7c3b004d4ba19273af42db021d27": {"t": 1785754212.1607187, "n": 66921, "h": "49881d01ffb26da2", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n const desktopDiagResponse = await fetch(B + '/api/diagnostics', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n const desktopDiag = await desktopDiagResponse.json();\n A.eq(desktopDiag.report.mode, 'desktop', 'http://tauri.localhost is classified as the packaged desktop origin');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "2a8118832ae82e2b6ddca33e0261384cedcf38f7": {"t": 1785754214.267454, "n": 66619, "h": "520db2ab9a4b5d49", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft, extraEnv) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test',\n // Clear any ambient build stamp so the DEFAULT boot deterministically exercises the harness git-describe\n // fallback (harnessSource='git'); a per-boot `extraEnv` (below) sets it to prove the env override wins.\n STARNET_BUILD_DESCRIBE: '', SKYNET_BUILD_DESCRIBE: ''\n }, extraEnv || {}),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1, extraEnv));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n // LANE V: the harness build id must NOT be the intentional '0.0.0' package placeholder — running from a git\n // checkout it resolves via `git describe` (harnessSource='git'), and harnessSource always names WHERE it came from.\n A.ok(ver.body.harness !== '0.0.0', '/api/version harness is NOT the 0.0.0 placeholder (git-describe truth in a checkout)');\n A.ok(['env', 'git', 'pkg'].includes(ver.body.harnessSource), '/api/version reports an honest harnessSource (env|git|pkg)');\n A.eq(ver.body.harnessSource, 'git', \"/api/version resolves harness via git-describe when no build stamp is set\");\n\n // LANE V: env override wins — a boot with STARNET_BUILD_DESCRIBE set reports that exact string + harnessSource='env'\n // (the release pipeline stamps the real build id this way). Dedicated second boot: the value is fixed at spawn time.\n {\n const bd = 'lane-v-build-2f3a9c1';\n const ws2 = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-bd-'));\n const b2 = await boot(port + 200, ws2, 20, { STARNET_BUILD_DESCRIBE: bd });\n const B2 = 'http://' + HOST + ':' + b2.port;\n try {\n const tok2 = await bootToken(B2, B2);\n const r2 = await fetch(B2 + '/api/version', { headers: { 'X-StarNet-Token': tok2, Origin: B2 } });\n const v2 = await r2.json();\n A.eq(r2.status, 200, 'GET /api/version (BUILD_DESCRIBE boot) -> 200');\n A.eq(v2.harness, bd, '/api/version harness = STARNET_BUILD_DESCRIBE (env override wins over git/pkg)');\n A.eq(v2.harnessSource, 'env', '/api/version reports harnessSource=env when the build stamp is set');\n A.eq(v2.app, '9.9.9-http-test', '/api/version app still resolves independently of the harness stamp');\n } finally {\n try { b2.child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws2, { recursive: true, force: true }); } catch (_) {}\n }\n }\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "59a3f31c592190ee81749f61bf33b643f36c2539": {"t": 1785754215.8353472, "n": 64392, "h": "d1eb51e40dd39319", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: '',\n // GROUND_UP_AUDIT P2: the packaged app injects its version via STARNET_APP_VERSION (src-tauri/ isn't a\n // bundled resource, so /api/version's conf lookup returns blank there). Set it so /api/version proves the\n // env-first fallback + honest appSource='env' below.\n STARNET_APP_VERSION: '9.9.9-http-test'\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- GROUND_UP_AUDIT P2: /api/version env-first fallback is HONEST (never a silent blank) ----\n // src-tauri/tauri.conf.json isn't a bundled resource in the packaged app, so the desktop shell exports\n // STARNET_APP_VERSION (set in the boot env above). /api/version must surface it with an honest appSource.\n const ver = await j('GET', '/api/version');\n A.eq(ver.status, 200, 'GET /api/version -> 200');\n A.eq(ver.body.app, '9.9.9-http-test', '/api/version app comes from STARNET_APP_VERSION (env-first fallback)');\n A.eq(ver.body.appSource, 'env', '/api/version reports appSource=env — honest about WHERE the version came from');\n A.ok(typeof ver.body.harness === 'string', '/api/version keeps the byte-compatible {harness} field');\n A.ok(typeof ver.body.node === 'string' && ver.body.node.length > 0, '/api/version keeps the node field');\n\n // ---- GROUND_UP_AUDIT P2: a THROWN store read reports 500 {error}, not a 200-empty ----\n // Happy path already proven above (GET /api/runs -> 200 with {runs:[...]}). Force the runStore.list read to\n // THROW by feeding a runId query far past the internal length guard is NOT possible over HTTP; instead we prove\n // the CONTRACT the frontend relies on: the route JSON always carries {runs} on 200, and the source uses\n // json(500,{error}) on a real failure (verified structurally so we don't have to corrupt the live store here).\n const runsOk = await j('GET', '/api/runs?agent=agent&limit=5');\n A.eq(runsOk.status, 200, 'GET /api/runs happy path stays 200');\n A.ok(Array.isArray(runsOk.body.runs), '/api/runs 200 body carries {runs:[...]} (shape byte-compatible)');\n const ckOk = await j('GET', '/api/checkpoint?agent=agent');\n A.eq(ckOk.status, 200, 'GET /api/checkpoint happy path stays 200');\n A.ok(Array.isArray(ckOk.body.snapshots), '/api/checkpoint 200 body carries {snapshots:[...]}');\n // the truthful-failure branch itself (structural: the catch now reports 500, never a masking 200-empty)\n const idxSrcHonesty = fs.readFileSync(INDEX, 'utf8');\n const serveRunsSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function serveRuns'), idxSrcHonesty.indexOf('function serveInsights'));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveRunsSrc), 'serveRuns reports json(500,{error}) on a thrown read, not a 200-empty');\n const ckStart = idxSrcHonesty.indexOf('function handleCheckpointList');\n const serveCkSrc = idxSrcHonesty.slice(ckStart, idxSrcHonesty.indexOf('function ', ckStart + 20));\n A.ok(/catch\\s*\\(e\\)\\s*\\{[\\s\\S]*?json\\(500,\\s*\\{\\s*error/.test(serveCkSrc), 'handleCheckpointList reports json(500,{error}) on a thrown read, not a 200-empty');\n\n // ---- GROUND_UP_AUDIT P2: three more sidecar honesty/reliability fixes (internal behaviors — asserted\n // structurally against the live source, since none is deterministically triggerable over the HTTP surface) ----\n // #3 on-demand model-catalog re-warm: the boot warm's rejection handler no longer no-ops the session; the\n // channel modelCatalog accessor kicks a throttled re-warm when the snapshot is empty.\n A.ok(/function maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 maybeRewarmModelCatalog exists (on-demand catalog re-warm)');\n A.ok(/modelCatalog:\\s*\\(\\)\\s*=>\\s*\\{\\s*maybeRewarmModelCatalog\\(\\)/.test(idxSrcHonesty), '#3 channel modelCatalog accessor triggers the re-warm');\n A.ok(!/listModels\\(\\)\\.then\\([\\s\\S]{0,120}\\(\\)\\s*=>\\s*\\{\\}\\s*\\)/.test(idxSrcHonesty.slice(idxSrcHonesty.indexOf('server.listen'))), '#3 boot warm no longer has an empty rejection handler');\n // #4 aux-run unmetered parity: skill-review + curator now stamp the ledger with the unmetered flag like\n // reflection/study, so a Codex-only user is not billed phantom metered spend.\n const reviewSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function runBackgroundSkillReview'), idxSrcHonesty.indexOf('async function runSkillCurator'));\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(reviewSrc), '#4 runBackgroundSkillReview ledger.record carries unmetered');\n const curatorSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('async function runSkillCurator'), idxSrcHonesty.indexOf('async function runSkillCurator') + 4000);\n A.ok(/ledger\\.record\\(\\{[^}]*unmetered\\s*\\}\\)/.test(curatorSrc), '#4 runSkillCurator ledger.record carries unmetered');\n A.ok(/runBackgroundSkillReview\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-review invocation threads providerUnmetered');\n A.ok(/runSkillCurator\\(\\{[\\s\\S]*?unmetered:\\s*providerUnmetered/.test(idxSrcHonesty), '#4 skill-curator invocation threads providerUnmetered');\n // #5 steer-drop diagnostics: a non-empty steer buffer discarded at teardown logs a one-line count.\n A.ok(/function dropSteer\\(runId, ctx\\)/.test(idxSrcHonesty), '#5 dropSteer helper exists');\n A.ok(/\\[steer\\] dropped '\\s*\\+\\s*b\\.length/.test(idxSrcHonesty), '#5 dropSteer logs the dropped count');\n A.ok((idxSrcHonesty.match(/dropSteer\\(runId,/g) || []).length >= 3, '#5 all three teardown sites route through dropSteer');\n // #6 _procDiagSeen TTL eviction: the dedupe map now evicts stale entries on every insert (2x the window),\n // not only once it grows past 64 — so a slow trickle of distinct one-shot messages no longer leaks forever.\n const surfaceSrc = idxSrcHonesty.slice(idxSrcHonesty.indexOf('function surfaceProcessError'), idxSrcHonesty.indexOf('function surfaceProcessError') + 1400);\n A.ok(/const ttl = 2 \\* PROC_DIAG_WINDOW_MS/.test(surfaceSrc), '#6 _procDiagSeen uses a 2x-window TTL');\n A.ok(!/_procDiagSeen\\.size > 64/.test(surfaceSrc), '#6 eviction is no longer gated behind size > 64 (runs every insert)');\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "478e9878ae7aa4a3fbbda8df08ce0d1968d3acb9": {"t": 1785754217.577927, "n": 58303, "h": "9c296fff2c45e90c", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- P1.1 ROSTER ENVELOPE + ANTI-CLOBBER (UPDATE_STATE_SAFETY_AUDIT): { version, updatedAt, agents } + refuse a stale push ----\n // Stamps are anchored to a real-clock base ABOVE any prior stamp-less host-clock save this test already made\n // (the earlier P1-2 guard block's unstamped pushes advance the baseline off Date.now()), so these are genuinely\n // fresher / staler relative to the LIVE baseline rather than to fixed small numbers.\n const eBase = Date.now() + 100000;\n // 1) a stamped push round-trips into an envelope on disk (version + updatedAt) and echoes the accepted stamp.\n const envPush = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase });\n A.eq(envPush.status, 200, 'a stamped roster push -> 200');\n A.eq(envPush.body.updatedAt, eBase, 'the accepted push echoes its own updatedAt stamp');\n const env = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n A.eq(env.version, 1, 'the on-disk roster carries version:1');\n A.eq(env.updatedAt, eBase, 'the on-disk envelope records the accepted updatedAt (anti-clobber baseline)');\n A.ok(Array.isArray(env.agents) && env.agents.length === 1, 'the envelope holds the agents array');\n // 2) a STALE push (older updatedAt) is refused with 200 { ok:false, stale:true } and does NOT mutate the store.\n const envBefore = fs.readFileSync(rosterFile, 'utf8');\n const stale = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'STALE', name: 'Ghost', provider: 'openrouter' }], updatedAt: eBase - 1000 });\n A.eq(stale.status, 200, 'a stale roster push -> 200 (behind, not malformed)');\n A.eq(stale.body.ok, false, 'the stale push is refused (ok:false)');\n A.eq(stale.body.stale, true, 'the stale push is flagged stale:true');\n A.eq(stale.body.updatedAt, eBase, 'the stale rejection reports the authoritative on-disk updatedAt');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), envBefore, 'the stale push left the roster BYTE-FOR-BYTE unchanged (no clobber)');\n // 3) a NEWER push wins and advances the baseline.\n const fresh = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'fresh', name: 'Ultron', provider: 'openrouter' }], updatedAt: eBase + 1000 });\n A.eq(fresh.status, 200, 'a newer roster push -> 200');\n A.eq(fresh.body.ok, true, 'the newer push is accepted');\n A.eq(JSON.parse(fs.readFileSync(rosterFile, 'utf8')).updatedAt, eBase + 1000, 'the newer stamp is now the on-disk baseline');\n // 4) BACKWARD COMPAT: a stamp-less push (legacy frontend) skips the gate and still writes as today.\n const noStamp = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'legacy-client', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(noStamp.status, 200, 'a stamp-less (legacy) push -> 200');\n A.eq(noStamp.body.ok, true, 'a stamp-less push is accepted (backward compatible)');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('legacy-client') >= 0, 'the stamp-less push persisted');\n\n // ---- P1.1 UNKNOWN-FIELD PRESERVATION: a field a NEWER frontend adds must survive an older sidecar's re-save ----\n // Push an agent carrying a field this sidecar's schema does NOT model (futureField). On re-save saveAgentRoster\n // rebuilds each row from a fixed known-field list — WITHOUT preservation that field would be silently dropped\n // (the exact reshape-on-save data-loss class P1.1 flags). Prove it round-trips onto disk under the known fields.\n // Stamp above the current baseline (the stamp-less push above advanced it off the host clock).\n const fwPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', provider: 'openrouter', futureField: 'keep-me-42', nested: { a: 1 } }\n ], updatedAt: Date.now() + 200000 });\n A.eq(fwPush.status, 200, 'a push with an unknown per-agent field -> 200');\n const fwDisk = JSON.parse(fs.readFileSync(rosterFile, 'utf8'));\n const fwRec = fwDisk.agents.find(a => a.agentId === 'agent');\n A.ok(fwRec, 'the agent persisted');\n A.eq(fwRec.futureField, 'keep-me-42', 'an UNKNOWN scalar field is preserved through re-save (no reshape drop)');\n A.ok(fwRec.nested && fwRec.nested.a === 1, 'an UNKNOWN nested field is preserved through re-save');\n A.eq(fwRec.name, 'Ultron', 'the KNOWN fields still win / persist alongside the preserved ones');\n A.eq(fwRec.provider, 'openrouter', 'the known provider field is intact next to the preserved unknowns');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "3c58200ac33087e619bc0a095c0ef0315a0846ce": {"t": 1785754219.0575101, "n": 53738, "h": "27353475caae6fa0", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- P1-2 ROSTER GUARD: a malformed /api/roster body must NEVER wipe the persisted crew (data-loss bug). ----\n // Establish a real persisted roster first (valid push -> 200 + on-disk file), then fire every malformed shape\n // and prove each is a 400 that leaves the roster file on disk BYTE-FOR-BYTE unchanged (non-destruction).\n const rosterFile = path.join(ws, 'agent.roster.json');\n const validPush = await j('POST', '/api/roster', { agents: [\n { agentId: 'agent', system: 'hero', name: 'Ultron', model: 'anthropic/claude-sonnet-4.6', provider: 'openrouter', role: 'orchestrator' },\n { agentId: 'researcher-2', system: 'worker', name: 'Scout', model: '', provider: 'openrouter', role: 'research' }\n ] });\n A.eq(validPush.status, 200, 'POST /api/roster with a valid crew -> 200');\n A.eq(validPush.body.count, 2, 'the valid push persisted both agents');\n A.ok(fs.existsSync(rosterFile), 'the roster round-tripped to disk');\n const rosterBefore = fs.readFileSync(rosterFile, 'utf8');\n A.ok(rosterBefore.indexOf('researcher-2') >= 0, 'the persisted roster names the summoned worker');\n\n // every malformed body -> 400 AND the on-disk roster is untouched (prove by reading the store back)\n const malformed = [\n ['missing agents field', {}],\n ['agents is null', { agents: null }],\n ['agents is a string', { agents: 'agent' }],\n ['agents is an object', { agents: { agentId: 'x' } }],\n ['agents is empty (would clear the crew)', { agents: [] }],\n ['an agent entry is null', { agents: [null] }],\n ['an agent entry is a string', { agents: ['agent'] }],\n ['an agent entry is an array', { agents: [[]] }],\n ['an agent has a numeric agentId (coercion class)', { agents: [{ agentId: 123, name: 'x' }] }],\n ['an agent has no agentId', { agents: [{ name: 'x' }] }],\n ['an agent has an out-of-charset agentId', { agents: [{ agentId: '../evil', name: 'x' }] }]\n ];\n for (const [label, badBody] of malformed) {\n const r = await j('POST', '/api/roster', badBody);\n A.eq(r.status, 400, 'malformed /api/roster (' + label + ') -> 400, not a silent wipe');\n A.ok(r.body && typeof r.body.error === 'string', 'the ' + label + ' rejection carries a JSON error envelope');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'the persisted roster is UNCHANGED after the ' + label + ' rejection (no data loss)');\n }\n // a top-level array body (not an object) is also refused without clobbering\n const arrBody = await fetch(B + '/api/roster', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, tok), body: JSON.stringify([{ agentId: 'agent' }]) });\n A.eq(arrBody.status, 400, 'a top-level array roster body -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'a top-level array body leaves the roster unchanged');\n // a malformed-JSON body is a 400 too and never touches the store\n const rosterBadJson = await fetch(B + '/api/roster', { method: 'POST', headers: tok, body: '{not json' });\n A.eq(rosterBadJson.status, 400, 'POST /api/roster with malformed JSON -> 400');\n A.eq(fs.readFileSync(rosterFile, 'utf8'), rosterBefore, 'malformed JSON leaves the roster unchanged');\n // and a fresh VALID push still works after all the rejections (the guard did not wedge the route)\n const revalid = await j('POST', '/api/roster', { agents: [{ agentId: 'agent', system: 'solo', name: 'Ultron', provider: 'openrouter' }] });\n A.eq(revalid.status, 200, 'a valid push after the rejections still succeeds');\n A.eq(revalid.body.count, 1, 'the follow-up valid push replaced the roster as intended');\n A.ok(fs.readFileSync(rosterFile, 'utf8').indexOf('researcher-2') < 0, 'the legitimate replacement dropped the old worker (clear-and-set still works)');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "927a73ac6820c0b78e8eb2f5a6122b6c4ff0f1e0": {"t": 1785754220.6454935, "n": 49900, "h": "ffbd89d6549ea6c6", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n ELEVENLABS_API_KEY: '', // same determinism for the ElevenLabs TTS branch's no-key degrade\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n // ElevenLabs branch: same 200-degrade contract, its own key + validation (no network with the env cleared).\n const elBadVoice = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'not/a/valid/id!' });\n A.eq(elBadVoice.status, 200, 'POST /api/tts elevenlabs with a malformed voiceId -> 200');\n A.eq(elBadVoice.body.fallback, true, 'malformed voiceId degrades to the fallback envelope');\n A.ok(/bad voiceId/i.test(elBadVoice.body.reason || ''), 'the degrade names the bad-voiceId reason');\n const elNoKey = await j('POST', '/api/tts', { provider: 'elevenlabs', text: 'station check', voiceId: 'AbCdEf012345678901234' });\n A.eq(elNoKey.status, 200, 'POST /api/tts elevenlabs with no key -> 200 (never a hard error)');\n A.eq(elNoKey.body.fallback, true, 'no-key ElevenLabs TTS returns the {fallback:true} envelope');\n A.ok(/no elevenlabs key/i.test(elNoKey.body.reason || ''), 'the degrade names the missing ElevenLabs key specifically');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "d1f2f520b230018653290234e6f8b2dbb6476c07": {"t": 1785754222.0774608, "n": 48800, "h": "a0de84d7dfbaa4c5", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n // ---- reconnect reconciliation snapshot: GET /api/state/snapshot returns the documented shape, token-gated ----\n const snapNoTok = await fetch(B + '/api/state/snapshot');\n A.eq(snapNoTok.status, 403, 'GET /api/state/snapshot WITHOUT a token -> 403 (gated like sibling GETs)');\n const snap = await j('GET', '/api/state/snapshot');\n A.eq(snap.status, 200, 'GET /api/state/snapshot -> 200');\n A.ok(snap.body && typeof snap.body.ts === 'number', 'snapshot carries a numeric server ts');\n A.ok(Array.isArray(snap.body.runs), 'snapshot.runs is an array');\n A.ok(Array.isArray(snap.body.prompts), 'snapshot.prompts is an array');\n A.ok(Array.isArray(snap.body.summons), 'snapshot.summons is an array');\n A.ok(Array.isArray(snap.body.queues), 'snapshot.queues is an array');\n // an idle boot has no live runs — the endpoint must report EMPTY, not a fabricated placeholder (truthful telemetry).\n A.eq(snap.body.runs.length, 0, 'an idle boot reports zero live runs (honest, not fabricated)');\n A.eq(JSON.stringify(snap.body).indexOf(apiToken), -1, 'the snapshot never echoes the API token');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "8c985cd2bdbd14c39d1472c1c8a7209ffe01eef2": {"t": 1785754223.582206, "n": 47675, "h": "9063fb99b88149ab", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- T3.9 DIAGNOSTICS: token-gated, assembled server-side from real state, and SECRET-FREE ----\n const diagNoTok = await fetch(B + '/api/diagnostics');\n A.eq(diagNoTok.status, 403, 'GET /api/diagnostics WITHOUT a token -> 403 (gated like other data routes)');\n const diag = await j('GET', '/api/diagnostics');\n A.eq(diag.status, 200, 'GET /api/diagnostics WITH the token -> 200');\n A.ok(diag.body && typeof diag.body.text === 'string' && diag.body.text.length > 0, 'diagnostics returns a paste-ready text block');\n A.ok(diag.body.report && typeof diag.body.report === 'object', 'diagnostics returns a structured report too');\n A.ok(/StarNet diagnostics/.test(diag.body.text), 'the block is clearly fenced');\n A.ok(/no keys, tokens, or message content/.test(diag.body.text), 'the block states it carries no secrets');\n A.eq(typeof diag.body.report.keyPresent, 'boolean', 'keyPresent is a boolean, never the key itself');\n // the pre-seeded discord channel carries a fake OpenRouter key (sk-or-v1-fake-discord) + a bot token — NEITHER\n // may ever appear in the diagnostics payload (the whole point of the sanitization law).\n const diagBlob = JSON.stringify(diag.body);\n A.ok(diagBlob.indexOf('sk-or-v1-fake-discord') < 0, 'diagnostics never leaks the seeded provider key');\n A.ok(diagBlob.indexOf('discord-token') < 0, 'diagnostics never leaks the seeded channel token');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "b914dd5f22e9a0ac2fbb6f0d8c283d991c783e2e": {"t": 1785754226.0267806, "n": 46254, "h": "6ac82b9af16ac302", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n // SOURCE LOCK — auto-saved (saved:true) stash items carry a REAL record id; a keep/edit on one would mint a\n // DUPLICATE note and a discard would denylist while the record survives. The handler must 409 them before the\n // write path (the stash is in-memory + LLM-fed, so this guarantee is locked at the source seam).\n const idxSrc = fs.readFileSync(path.join(__dirname, '..', 'sidecar', 'index.js'), 'utf8');\n const iTurnin = idxSrc.indexOf('async function handleMemoryTurnin');\n const iWrite = idxSrc.indexOf('writeMemoryRecord(agentId, prop', iTurnin);\n const guard = idxSrc.indexOf('if (prop.saved) return json(409', iTurnin);\n A.ok(iTurnin > 0 && iWrite > iTurnin, 'handleMemoryTurnin routes keep/edit through writeMemoryRecord');\n A.ok(guard > iTurnin && guard < iWrite, 'a saved:true stash item is 409-rejected BEFORE the keep/edit write path (no duplicate mint)');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "bb69aba3e36635dd2835cff8fc8ffa5d8f18d23f": {"t": 1785754228.041476, "n": 45351, "h": "e44f86782db026ca", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n // ---- silent-save UX: the one-tap VETO on an auto-saved memory. Seed a real notebook record on disk (the\n // sidecar's durable store reads through to the file on every get), then undo it: the record is removed\n // AND its text lands on the permanent declined denylist, and a repeat veto is an idempotent no-op. ----\n const vetoNoTok = await fetch(B + '/api/memory/turnin', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agentId: 'agent', id: 'note_1', verdict: 'veto' }) });\n A.eq(vetoNoTok.status, 403, 'POST /api/memory/turnin veto WITHOUT a token -> 403');\n const vetoBadVerdict = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'nonsense' });\n A.eq(vetoBadVerdict.status, 400, 'an unknown verdict -> 400');\n const vetoNoId = await j('POST', '/api/memory/turnin', { agentId: 'agent', verdict: 'veto' });\n A.eq(vetoNoId.status, 400, 'veto without an id -> 400');\n // seed one saved record + a distinct one that must survive the veto\n fs.writeFileSync(path.join(ws, 'agent.notebook.json'), JSON.stringify([\n { id: 'note_1', kind: 'fact', title: 'Fact', body: 'deploys the alpha service with npm publish', content: 'deploys the alpha service with npm publish', scope: 'global', createdAt: 1, ts: 1, useCount: 0, trust: 0, pinned: false },\n { id: 'note_2', kind: 'profile', title: 'Preference', body: 'prefers terse answers', content: 'prefers terse answers', scope: 'global', createdAt: 2, ts: 2, useCount: 0, trust: 0, pinned: false }\n ]));\n const veto = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(veto.status, 200, 'veto of a saved record -> 200');\n A.eq(veto.body.ok, true, 'veto reports ok');\n const recsAfter = await j('GET', '/api/memory/records?agent=agent');\n const ids = (recsAfter.body.records || []).map(r => r.id);\n A.ok(ids.indexOf('note_1') < 0, 'the vetoed record was removed from the notebook');\n A.ok(ids.indexOf('note_2') >= 0, 'the distinct record beside it survived');\n const declVeto = await j('GET', '/api/memory/declined?agent=agent');\n A.ok((declVeto.body.declined || []).indexOf('deploys the alpha service with npm publish') >= 0, 'the vetoed belief joined the permanent declined denylist');\n // idempotent: vetoing an already-gone record still succeeds (no 404, no double-append)\n const vetoAgain = await j('POST', '/api/memory/turnin', { agentId: 'agent', id: 'note_1', verdict: 'veto', content: 'deploys the alpha service with npm publish' });\n A.eq(vetoAgain.status, 200, 'a repeat veto of an already-removed record is an idempotent 200 no-op');\n const declAgain = await j('GET', '/api/memory/declined?agent=agent');\n A.eq((declAgain.body.declined || []).filter(t => t === 'deploys the alpha service with npm publish').length, 1, 'the denylist has no duplicate after a repeat veto');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "aef7123f7b7049ed444a4baa4a6ed120465ee6c2": {"t": 1785754229.6693187, "n": 42339, "h": "bc6c4eb982ad44d1", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n // clear the OpenRouter key so the voice routes' no-key fallback path is deterministic (a dev machine\n // with the key in its ambient env would otherwise hit the network and flip the {text:''} assertions).\n OPENROUTER_KEY: '', STARNET_OPENROUTER_KEY: '', SKYNET_OPENROUTER_KEY: '',\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n // ---- voice: /api/tts + /api/stt graceful degradation (no key in this boot → no network) ----\n // Both routes must ALWAYS answer 200 with a degrade envelope rather than a hard error, so the client's\n // voice path never wedges. With the OpenRouter key cleared above, we exercise exactly that path.\n const ttsNoKey = await j('POST', '/api/tts', { text: 'station check', voice: 'Umbriel', style: 'a low, detached transmission' });\n A.eq(ttsNoKey.status, 200, 'POST /api/tts with no key -> 200 (never a hard error)');\n A.eq(ttsNoKey.body.fallback, true, 'no-key TTS returns the {fallback:true} envelope so the client uses speechSynthesis');\n A.ok(/no key/i.test(ttsNoKey.body.reason || ''), 'the TTS fallback names the no-key reason');\n const ttsNoText = await j('POST', '/api/tts', { text: ' ' });\n A.eq(ttsNoText.body.fallback, true, 'empty-text TTS also degrades cleanly');\n\n // STT JSON shape: no audio -> {text:'',reason}; audio present but no key -> {text:'',reason:'no key'}.\n const sttNoAudio = await j('POST', '/api/stt', { format: 'wav' });\n A.eq(sttNoAudio.status, 200, 'POST /api/stt with no audio -> 200 (degrades, never 4xx into the loop)');\n A.eq(sttNoAudio.body.text, '', 'no-audio STT returns an empty transcript');\n A.ok(typeof sttNoAudio.body.reason === 'string' && sttNoAudio.body.reason.length > 0, 'no-audio STT surfaces a reason for the console.warn');\n const sttNoKey = await j('POST', '/api/stt', { audio: Buffer.from('not real audio').toString('base64'), format: 'wav' });\n A.eq(sttNoKey.status, 200, 'POST /api/stt with audio but no key -> 200');\n A.eq(sttNoKey.body.text, '', 'no-key STT returns an empty transcript');\n A.ok(/no key/i.test(sttNoKey.body.reason || ''), 'the STT degrade names the no-key reason');\n // raw-bytes shape (the recorder-provider default): a webm content-type body, still no key -> clean degrade.\n const sttRaw = await fetch(B + '/api/stt', { method: 'POST', headers: Object.assign({ 'Content-Type': 'audio/webm' }, apiToken ? { 'X-StarNet-Token': apiToken } : {}), body: Buffer.from('fake opus bytes') });\n A.eq(sttRaw.status, 200, 'POST /api/stt raw audio bytes -> 200');\n const sttRawBody = await sttRaw.json();\n A.eq(sttRawBody.text, '', 'raw-bytes STT with no key returns an empty transcript');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "ec050e06eae9de96e6f020e22bc0424c3a18fa14": {"t": 1785754232.6906724, "n": 39709, "h": "490863bf8591e404", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n // keep the managed-credits path OFF for the default boot so /api/credits proves the unconfigured 404 path\n // deterministically (a dev machine with these set in the ambient env would otherwise flip the assertion).\n STARNET_CREDITS_URL: '', SKYNET_CREDITS_URL: '',\n STARNET_CREDITS_API_KEY: '', SKYNET_CREDITS_API_KEY: '',\n STARNET_CREDITS_ACCOUNT: '', SKYNET_CREDITS_ACCOUNT: '',\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- managed credits: UNCONFIGURED boot exposes ZERO surface (honesty law — no dead STORE, no fake balance) ----\n // This boot sets no STARNET_CREDITS_URL, so /api/credits must 404 with { configured: false } — the frontend\n // reads that and renders no STORE card at all.\n const creditsNoTok = await fetch(B + '/api/credits');\n A.eq(creditsNoTok.status, 403, 'GET /api/credits WITHOUT a token -> 403 (gated like other data routes)');\n const creditsUnconf = await j('GET', '/api/credits');\n A.eq(creditsUnconf.status, 404, 'GET /api/credits with no credits backend configured -> 404 (no surface)');\n A.eq(creditsUnconf.body.configured, false, 'the 404 body honestly reports configured:false');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "288148d06588fc1ab985e33797a8e4690b19a734": {"t": 1785754234.2416904, "n": 38570, "h": "551f7bf603eedefd", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "d1ccb3c2f4f7aa3b506038350a8e97959b5ca5d8": {"t": 1785754236.6504197, "n": 32885, "h": "3057bf2e6ef31435", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n\n // ================= P1 SETTINGS ROWS =================\n\n // ---- P1-9 ADVANCED runtime knobs: token-gated, persist an override, report env-precedence, reset ----\n const knobsNoTok = await fetch(B + '/api/runtime/knobs');\n A.eq(knobsNoTok.status, 403, 'GET /api/runtime/knobs WITHOUT a token -> 403');\n const knobs = await j('GET', '/api/runtime/knobs');\n A.eq(knobs.status, 200, 'GET /api/runtime/knobs -> 200');\n A.ok(knobs.body.fields && knobs.body.fields.maxIters, 'knobs report the maxIters field');\n A.eq(knobs.body.fields.maxIters.default, 40, 'maxIters default is 40');\n A.eq(knobs.body.fields.maxIters.saved, null, 'no saved override before any POST');\n const knobsSet = await j('POST', '/api/runtime/knobs', { maxIters: 55, cronTickMs: 30000 });\n A.eq(knobsSet.status, 200, 'POST /api/runtime/knobs -> 200');\n A.eq(knobsSet.body.fields.maxIters.saved, 55, 'the saved override is recorded');\n A.eq(knobsSet.body.fields.maxIters.effective, 55, 'the saved override becomes effective');\n const knobsBad = await j('POST', '/api/runtime/knobs', { maxIters: 9999 });\n A.eq(knobsBad.status, 400, 'an out-of-range knob value -> 400');\n A.ok(fs.existsSync(path.join(ws, 'runtime.knobs.json')), 'the knobs override persisted to disk');\n const knobsReset = await j('POST', '/api/runtime/knobs', { maxIters: null, cronTickMs: null });\n A.eq(knobsReset.body.fields.maxIters.saved, null, 'a null clears the override');\n\n // ---- P1-10 memory controls: reflection on/off + cooldown, persisted + validated ----\n const memCfg = await j('GET', '/api/memory/config');\n A.eq(memCfg.status, 200, 'GET /api/memory/config -> 200');\n A.eq(memCfg.body.reflectEnabled, true, 'reflection defaults ON');\n A.ok(typeof memCfg.body.scopeNote === 'string' && memCfg.body.scopeNote.length > 0, 'a scope note is returned');\n const memSet = await j('POST', '/api/memory/config', { reflectEnabled: false, reflectCooldownMs: 300000 });\n A.eq(memSet.status, 200, 'POST /api/memory/config -> 200');\n A.eq(memSet.body.reflectEnabled, false, 'reflection can be turned off');\n A.eq(memSet.body.reflectCooldownMs, 300000, 'the cooldown persists');\n const memBad = await j('POST', '/api/memory/config', { reflectCooldownMs: 99999999 });\n A.eq(memBad.status, 400, 'an over-range cooldown -> 400');\n A.ok(fs.existsSync(path.join(ws, 'memory.config.json')), 'the memory config persisted to disk');\n\n // ---- P1-7 STATION BACKUP: export excludes secrets; import round-trips + reset ----\n // first seed a real budget override so the export has something server-side to carry.\n await j('POST', '/api/budget/caps', { perRun: 7 });\n const exp = await j('POST', '/api/config/export', { sections: { settings: { theme: 'blue' }, autonomy: { initiative: 'wait', reach: 'sandbox' } } });\n A.eq(exp.status, 200, 'POST /api/config/export -> 200');\n A.eq(exp.body.starnetExport, 1, 'the export carries the schema-version pivot');\n A.eq(exp.body.sections.budget.perRun, 7, 'the export carries the saved budget override');\n A.eq(exp.body.sections.settings.theme, 'blue', 'the export carries the browser-owned settings slice');\n // SECURITY: the seeded Discord token / provider keys must NOT appear anywhere in the export.\n A.ok(JSON.stringify(exp.body).indexOf('discord-token') < 0, 'the export never contains the Discord bot token');\n A.ok(JSON.stringify(exp.body).indexOf('sk-') < 0, 'the export never contains a key-shaped secret');\n\n // now reset the budget, then import the envelope back and prove the value is restored.\n const rst = await j('POST', '/api/config/reset', { section: 'budget' });\n A.eq(rst.status, 200, 'POST /api/config/reset{budget} -> 200');\n const afterReset = await j('GET', '/api/budget/status');\n A.ok(!Object.prototype.hasOwnProperty.call(afterReset.body.saved, 'perRun'), 'budget override cleared by reset');\n const imp = await j('POST', '/api/config/import', { envelope: exp.body });\n A.eq(imp.status, 200, 'POST /api/config/import -> 200');\n A.ok(imp.body.applied.indexOf('budget') >= 0, 'import reports budget applied');\n A.ok(imp.body.browser && imp.body.browser.settings && imp.body.browser.settings.theme === 'blue', 'import echoes the browser settings slice for local restore');\n const afterImport = await j('GET', '/api/budget/status');\n A.eq(afterImport.body.saved.perRun, 7, 'the imported budget override is restored (round-trip)');\n const impBad = await j('POST', '/api/config/import', { envelope: { not: 'a backup' } });\n A.eq(impBad.status, 400, 'import of a file with no version marker -> 400');\n const rstBad = await j('POST', '/api/config/reset', { section: 'nonsense' });\n A.eq(rstBad.status, 400, 'reset of an unknown section -> 400');\n const expNoTok = await fetch(B + '/api/config/export', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: '{}' });\n A.eq(expNoTok.status, 403, 'POST /api/config/export WITHOUT a token -> 403');\n\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "c8c6003f47d38b6794de3038fea87319909f00c0": {"t": 1785754238.028583, "n": 33535, "h": "952f227ec0c4a10d", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n // ---- slash command catalog + dispatch seam ----\n const slashCat = await j('GET', '/api/slash/catalog');\n A.eq(slashCat.status, 200, 'GET /api/slash/catalog -> 200');\n A.ok(Array.isArray(slashCat.body.commands), 'slash catalog returns commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'retry'), 'slash catalog includes /retry');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'new'), 'slash catalog includes /new');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'branch' && c.aliases && c.aliases.indexOf('fork') >= 0), 'slash catalog includes /branch with /fork alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'queue' && c.aliases && c.aliases.indexOf('q') >= 0), 'slash catalog includes /queue with /q alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'model'), 'slash catalog includes /model');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'skills'), 'slash catalog includes /skills');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'resume' && c.aliases && c.aliases.indexOf('sessions') >= 0), 'slash catalog includes /resume with /sessions alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'background' && c.aliases && c.aliases.indexOf('bg') >= 0), 'slash catalog includes /background with /bg alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'cron'), 'slash catalog includes /cron');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'memory'), 'slash catalog includes /memory');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-mcp' && c.aliases && c.aliases.indexOf('reload_mcp') >= 0), 'slash catalog includes /reload-mcp alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'version' && c.aliases && c.aliases.indexOf('v') >= 0), 'slash catalog includes /version alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'reload-skills' && c.aliases && c.aliases.indexOf('reload_skills') >= 0), 'slash catalog includes /reload-skills alias');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'morning-brief'), 'slash catalog includes built-in recipe commands');\n A.ok(slashCat.body.commands.some(c => c && c.name === 'plan' && c.source === 'skill'), 'slash catalog includes available compute-only skill commands');\n A.ok(!slashCat.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog omits unavailable workbench skill without placed workbench');\n\n const slashBench = await j('GET', '/api/slash/catalog?placed=workbench');\n A.ok(slashBench.body.commands.some(c => c && c.name === 'test-driven-development'), 'slash catalog includes workbench skill when workbench is placed');\n\n const slashRun = await j('POST', '/api/slash/dispatch', { input: '/retry' });\n A.eq(slashRun.status, 200, 'POST /api/slash/dispatch /retry -> 200');\n A.eq(slashRun.body.directive, { type: 'client', action: 'retry', args: '' }, 'slash dispatch returns a client retry directive');\n\n const slashFork = await j('POST', '/api/slash/dispatch', { input: '/fork copy this' });\n A.eq(slashFork.status, 200, 'POST /api/slash/dispatch /fork -> 200');\n A.eq(slashFork.body.command.name, 'branch', '/fork dispatch canonicalizes to branch');\n A.eq(slashFork.body.directive, { type: 'client', action: 'branch', args: 'copy this' }, 'fork dispatch returns branch client directive');\n\n const slashModel = await j('POST', '/api/slash/dispatch', { input: '/model openai/gpt-5' });\n A.eq(slashModel.status, 200, 'POST /api/slash/dispatch /model -> 200');\n A.eq(slashModel.body.directive, { type: 'client', action: 'model', args: 'openai/gpt-5' }, 'model dispatch returns client directive with model id');\n\n const slashReload = await j('POST', '/api/slash/dispatch', { input: '/reload_skills' });\n A.eq(slashReload.status, 200, 'POST /api/slash/dispatch /reload_skills -> 200');\n A.eq(slashReload.body.command.name, 'reload-skills', 'reload_skills dispatch canonicalizes');\n\n const slashSessions = await j('POST', '/api/slash/dispatch', { input: '/sessions 1' });\n A.eq(slashSessions.status, 200, 'POST /api/slash/dispatch /sessions -> 200');\n A.eq(slashSessions.body.directive, { type: 'client', action: 'resume', args: '1' }, 'sessions dispatch returns resume directive');\n\n const slashMcp = await j('POST', '/api/slash/dispatch', { input: '/reload_mcp github' });\n A.eq(slashMcp.status, 200, 'POST /api/slash/dispatch /reload_mcp -> 200');\n A.eq(slashMcp.body.directive, { type: 'client', action: 'reload-mcp', args: 'github' }, 'reload_mcp dispatch returns reload-mcp directive');\n\n const slashRecipe = await j('POST', '/api/slash/dispatch', { input: '/summarize launch notes' });\n A.eq(slashRecipe.status, 200, 'POST /api/slash/dispatch recipe -> 200');\n A.eq(slashRecipe.body.directive.type, 'insert', 'recipe slash dispatch returns insert directive');\n A.ok(String(slashRecipe.body.directive.text || '').indexOf('launch notes') >= 0, 'recipe slash dispatch carries typed args into the draft');\n\n const slashSkill = await j('POST', '/api/slash/dispatch', { input: '/plan refactor commands' });\n A.eq(slashSkill.status, 200, 'POST /api/slash/dispatch skill -> 200');\n A.eq(slashSkill.body.directive.source, 'skill', 'skill slash dispatch returns a skill directive');\n A.ok(String(slashSkill.body.directive.text || '').indexOf('refactor commands') >= 0, 'skill slash dispatch carries typed args into the task');\n\n const slashUnknown = await j('POST', '/api/slash/dispatch', { input: '/unknown' });\n A.eq(slashUnknown.status, 404, 'POST /api/slash/dispatch unknown -> 404');\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "07f4963cb49b87e0c7bc94eaa694da878eff9f41": {"t": 1785754239.4277282, "n": 27850, "h": "8d72df18d27764cf", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n // `connected` is now the LIVE gateway truth (real WS to Discord), which a sandboxed test can\n // never reach — auto-start itself is proven by the boot log line above. Assert the gateway\n // reported a real state instead of the old adapter-started=connected fiction.\n A.eq(typeof dcStatus.body.connected, 'boolean', 'Discord status reports a truthful gateway connected flag');\n A.ok(typeof dcStatus.body.state === 'string' && dcStatus.body.state.length > 0, 'Discord status reports a gateway state');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "38cfabd6ab7ec4693efe490df29529ec78a83b83": {"t": 1785754240.9370244, "n": 27425, "h": "d6bf675e87c5e7d8", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port, output: () => out }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n fs.mkdirSync(path.join(ws, 'channels'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'channels', 'secrets.json'), JSON.stringify({\n version: 1,\n discord: {\n enabled: true,\n token: 'discord-token',\n key: 'sk-or-v1-fake-discord',\n model: 'anthropic/claude-sonnet-4.6',\n provider: 'openrouter',\n agentId: 'agent',\n ownerId: 'discord-owner-42'\n }\n }));\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- Discord saved-owner restart guard: a boot from persisted config must restore owner lock\n // before the adapter sees any DM, so the first post-reboot stranger cannot claim the bot.\n for (let i = 0; i < 15 && booted.output().indexOf('discord auto-started from saved config') < 0; i++) await sleep(100);\n A.ok(booted.output().indexOf('discord auto-started from saved config') >= 0, 'Discord auto-started from saved config on boot');\n const dcStatus = await j('GET', '/api/channels/discord/status');\n A.eq(dcStatus.status, 200, 'GET /api/channels/discord/status -> 200');\n A.eq(dcStatus.body.configured, true, 'Discord status sees saved config');\n A.eq(dcStatus.body.connected, true, 'Discord status reports the auto-started adapter');\n A.eq(dcStatus.body.ownerLocked, true, 'Discord owner lock survived restart');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-owner-42') < 0, 'Discord status does not expose ownerId');\n A.ok(JSON.stringify(dcStatus.body).indexOf('discord-token') < 0, 'Discord status does not expose token');\n const savedChannels = JSON.parse(fs.readFileSync(path.join(ws, 'channels', 'secrets.json'), 'utf8'));\n A.eq(savedChannels.discord.ownerId, 'discord-owner-42', 'Discord auto-start preserved saved ownerId on disk');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}, "25f4fa1483fb647a14742d5c3c1018f48326323d": {"t": 1785754243.1100514, "n": 25701, "h": "db006622ac4a187a", "c": "/* node test/sidecar.http.test.js — boot-level end-to-end test of the real Node host (sidecar/index.js).\n Spawns the actual server as a child process against an ISOLATED temp workspace (SKYNET_WORKSPACES) on an\n ephemeral port, then drives the cost-spine HTTP surface over real sockets:\n - GET /api/health\n - GET /api/budget/status (reads a PRE-SEEDED ledger -> pools reflect persisted spend)\n - POST /api/budget/resume (valid scope lifts the cap; bad scope 400s)\n - POST /api/run (missing key/model 400s — the guard path, zero spend)\n Zero network dependence (the catalog warm fails closed offline) and ZERO model spend — it never sends a key.\n This is the one test that proves the server actually BOOTS and the new routes are wired, not just the units.\n\n NOT in test:fast (a child-process boot test shouldn't gate ~25 other agents' merges); run via `npm run test:http`. */\n'use strict';\nconst A = require('./_assert.js');\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { bootToken } = require('./_httpToken.js');\n\nconst HOST = '127.0.0.1';\nconst INDEX = path.resolve(__dirname, '..', 'sidecar', 'index.js');\nconst IPC_TOKEN = 'ipc-provider-config-test-token';\n\nfunction sleep(ms) { return new Promise(r => setTimeout(r, ms)); }\n\n// spawn the host on a candidate port; resolve once it logs \"listening\", retry the next port on EADDRINUSE.\nfunction boot(port, workspaces, attemptsLeft) {\n return new Promise((resolve, reject) => {\n const child = spawn(process.execPath, [INDEX], {\n env: Object.assign({}, process.env, {\n SKYNET_PORT: String(port),\n SKYNET_WORKSPACES: workspaces,\n SKYNET_IPC_TOKEN: IPC_TOKEN,\n OPENAI_API_KEY: '',\n STARNET_OPENAI_API_KEY: '',\n SKYNET_OPENAI_API_KEY: '',\n CUSTOM_OPENAI_KEY: '',\n STARNET_CUSTOM_OPENAI_KEY: '',\n SKYNET_CUSTOM_OPENAI_KEY: '',\n XAI_API_KEY: '',\n STARNET_XAI_API_KEY: '',\n SKYNET_XAI_API_KEY: '',\n X_AI_API_KEY: '',\n STARNET_X_AI_API_KEY: '',\n SKYNET_X_AI_API_KEY: '',\n GROQ_API_KEY: '',\n STARNET_GROQ_API_KEY: '',\n SKYNET_GROQ_API_KEY: '',\n MISTRAL_API_KEY: '',\n STARNET_MISTRAL_API_KEY: '',\n SKYNET_MISTRAL_API_KEY: '',\n DEEPSEEK_API_KEY: '',\n STARNET_DEEPSEEK_API_KEY: '',\n SKYNET_DEEPSEEK_API_KEY: '',\n TOGETHER_API_KEY: '',\n STARNET_TOGETHER_API_KEY: '',\n SKYNET_TOGETHER_API_KEY: '',\n FIREWORKS_API_KEY: '',\n STARNET_FIREWORKS_API_KEY: '',\n SKYNET_FIREWORKS_API_KEY: '',\n PERPLEXITY_API_KEY: '',\n STARNET_PERPLEXITY_API_KEY: '',\n SKYNET_PERPLEXITY_API_KEY: '',\n CEREBRAS_API_KEY: '',\n STARNET_CEREBRAS_API_KEY: '',\n SKYNET_CEREBRAS_API_KEY: '',\n CUSTOM_OPENAI_BASE_URL: '',\n STARNET_CUSTOM_OPENAI_BASE_URL: '',\n SKYNET_CUSTOM_OPENAI_BASE_URL: ''\n }),\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let out = '', settled = false;\n const onData = d => {\n out += d.toString();\n if (!settled && out.indexOf('http://' + HOST + ':' + port) >= 0) { settled = true; resolve({ child, port }); }\n if (!settled && /already in use/i.test(out)) {\n settled = true; try { child.kill(); } catch (_) {}\n if (attemptsLeft > 0) resolve(boot(port + 1, workspaces, attemptsLeft - 1));\n else reject(new Error('no free port'));\n }\n };\n child.stdout.on('data', onData);\n child.stderr.on('data', onData);\n child.on('error', e => { if (!settled) { settled = true; reject(e); } });\n setTimeout(() => { if (!settled) { settled = true; try { child.kill(); } catch (_) {} reject(new Error('boot timeout; output:\\n' + out)); } }, 9000);\n });\n}\n\n(async () => {\n // isolated workspace, PRE-SEEDED with two finished runs inside the trailing-day window (ts ~ now)\n const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'sk-http-'));\n const now = Date.now();\n const seed = [\n { runId: 'r1', agentId: 'alice', turns: 3, usd: 1.25, tokens: 1200, ts: now - 1000 },\n { runId: 'r2', agentId: 'bob', turns: 1, usd: 0.75, tokens: 400, ts: now - 500 }\n ];\n fs.writeFileSync(path.join(ws, 'ledger.jsonl'), seed.map(e => JSON.stringify(e)).join('\\n') + '\\n');\n\n const booted = await boot(8820 + (process.pid % 60), ws, 20);\n const { child, port } = booted;\n const B = 'http://' + HOST + ':' + port;\n let apiToken = '';\n const j = async (m, p, body) => {\n const headers = { 'Content-Type': 'application/json' };\n if (apiToken) headers['X-StarNet-Token'] = apiToken; // hardened: GET data routes need the token too now\n const r = await fetch(B + p, { method: m, headers, body: body ? JSON.stringify(body) : undefined });\n const t = await r.text(); let v; try { v = JSON.parse(t); } catch (_) { v = t; }\n return { status: r.status, body: v };\n };\n\n try {\n // ---- health ----\n const health = await j('GET', '/api/health');\n A.eq(health.status, 200, 'GET /api/health -> 200');\n A.eq(health.body, 'ok', 'health body is ok');\n\n // the served page injects the token for browser mode (the primary delivery path)\n const injected = await (await fetch(B + '/')).text();\n A.ok(/__STARNET_API_TOKEN__/.test(injected), 'served index.html bootstraps the API token for browser mode');\n apiToken = await bootToken(B, B);\n A.ok(apiToken.length >= 32, 'served index.html carries a high-entropy API token');\n const tok = { 'X-StarNet-Token': apiToken };\n const tauriOrigin = 'http://tauri.localhost';\n\n const sessNoToken = await fetch(B + '/api/session', { method: 'POST', headers: { Origin: B } });\n A.eq(sessNoToken.status, 403, 'POST /api/session without X-StarNet-Token -> 403');\n const sess = await fetch(B + '/api/session', { method: 'POST', headers: Object.assign({ Origin: B }, tok) });\n A.eq(sess.status, 200, 'POST /api/session with token -> 200');\n const sessBody = await sess.json();\n A.eq(sessBody.token, undefined, 'POST /api/session never echoes the API token');\n\n // ---- localhost API hardening: CORS mirrors only trusted origins; foreign origins rejected ----\n const sameOriginNoToken = await fetch(B + '/api/budget/status', { headers: { Origin: B } });\n A.eq(sameOriginNoToken.status, 403, 'same-origin sensitive GET without token -> 403');\n const sameOrigin = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: B }, tok) });\n A.eq(sameOrigin.status, 200, 'same-origin API request (with token) -> 200');\n A.eq(sameOrigin.headers.get('access-control-allow-origin'), B, 'same-origin CORS mirrors the exact loopback origin');\n\n const tauri = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: tauriOrigin }, tok) });\n A.eq(tauri.status, 200, 'Tauri app origin API request -> 200');\n A.eq(tauri.headers.get('access-control-allow-origin'), tauriOrigin, 'Tauri CORS mirrors the trusted app origin');\n\n const badOrigin = await fetch(B + '/api/budget/status', { headers: { Origin: 'https://evil.example' } });\n A.eq(badOrigin.status, 403, 'foreign web origin API request -> 403');\n A.eq(badOrigin.headers.get('access-control-allow-origin'), null, 'foreign origin gets no CORS read access');\n const badOriginWithToken = await fetch(B + '/api/budget/status', { headers: Object.assign({ Origin: 'https://evil.example' }, tok) });\n A.eq(badOriginWithToken.status, 403, 'foreign web origin with a valid token is still blocked');\n\n const preflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: B, 'Access-Control-Request-Method': 'POST' } });\n A.eq(preflight.status, 204, 'trusted API preflight -> 204');\n A.eq(preflight.headers.get('access-control-allow-origin'), B, 'trusted preflight mirrors loopback origin');\n const badPreflight = await fetch(B + '/api/run', { method: 'OPTIONS', headers: { Origin: 'https://evil.example', 'Access-Control-Request-Method': 'POST' } });\n A.eq(badPreflight.status, 403, 'foreign API preflight -> 403');\n\n // ---- C2 hole closed: GET DATA routes now require the token, not just POSTs ----\n const getNoTok = await fetch(B + '/api/budget/status');\n A.eq(getNoTok.status, 403, 'GET /api/budget/status WITHOUT a token -> 403 (GET data routes are gated now)');\n const getWithTok = await fetch(B + '/api/budget/status', { headers: tok });\n A.eq(getWithTok.status, 200, 'GET /api/budget/status WITH the token -> 200');\n\n // ---- provider registry/catalog routes: dynamic provider surface boots and remains token-gated ----\n const providersNoTok = await fetch(B + '/api/providers');\n A.eq(providersNoTok.status, 403, 'GET /api/providers WITHOUT a token -> 403');\n const providers = await j('GET', '/api/providers');\n A.eq(providers.status, 200, 'GET /api/providers -> 200');\n A.ok(Array.isArray(providers.body.providers), 'providers route returns a list');\n A.ok(providers.body.providers.some(p => p.id === 'openrouter'), 'providers include openrouter');\n A.ok(providers.body.providers.some(p => p.id === 'anthropic'), 'providers include anthropic');\n A.ok(providers.body.providers.some(p => p.id === 'gemini'), 'providers include gemini');\n for (const id of ['xai', 'groq', 'mistral', 'deepseek', 'together', 'fireworks', 'perplexity', 'cerebras']) {\n A.ok(providers.body.providers.some(p => p.id === id), 'providers include ' + id);\n }\n A.ok(providers.body.providers.some(p => p.id === 'custom'), 'providers include custom OpenAI-compatible');\n const models = await j('GET', '/api/models/openrouter');\n A.eq(models.status, 200, 'GET /api/models/openrouter -> 200');\n A.ok(Array.isArray(models.body.models), 'provider model route returns a models array');\n\n const pushOpenAi = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'openai', key: 'sk-provider-http-test-secret' })\n });\n A.eq(pushOpenAi.status, 200, 'POST /api/key can push an OpenAI provider key with the IPC token');\n const openAiAck = await pushOpenAi.json();\n A.eq(openAiAck.provider, 'openai', 'provider key ack names the configured provider');\n A.eq(openAiAck.configured, true, 'provider key ack reports configured');\n A.ok(JSON.stringify(openAiAck).indexOf('sk-provider-http-test-secret') < 0, 'provider key ack never echoes the secret');\n\n const pushAnthropic = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'anthropic', key: 'sk-ant-provider-http-test-secret' })\n });\n A.eq(pushAnthropic.status, 200, 'POST /api/key can push an Anthropic provider key with the IPC token');\n const anthropicAck = await pushAnthropic.json();\n A.eq(anthropicAck.provider, 'anthropic', 'Anthropic provider key ack names the configured provider');\n A.eq(anthropicAck.configured, true, 'Anthropic provider key ack reports configured');\n\n const pushXai = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'grok', key: 'xai-provider-http-test-secret' })\n });\n A.eq(pushXai.status, 200, 'POST /api/key can push an xAI provider key by alias with the IPC token');\n const xaiAck = await pushXai.json();\n A.eq(xaiAck.provider, 'xai', 'xAI provider key ack names the canonical provider');\n A.eq(xaiAck.configured, true, 'xAI provider key ack reports configured');\n\n const pushCustomBase = await fetch(B + '/api/key', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'X-Skynet-Token': IPC_TOKEN },\n body: JSON.stringify({ provider: 'custom', baseUrl: 'http://127.0.0.1:65530/v1' })\n });\n A.eq(pushCustomBase.status, 200, 'POST /api/key can push custom provider base URL with the IPC token');\n const configuredProviders = await j('GET', '/api/providers');\n const openaiProvider = configuredProviders.body.providers.find(p => p.id === 'openai');\n const xaiProvider = configuredProviders.body.providers.find(p => p.id === 'xai');\n const customProvider = configuredProviders.body.providers.find(p => p.id === 'custom');\n A.eq(!!(openaiProvider && openaiProvider.configured), true, 'OpenAI provider is configured after scoped key push');\n A.eq(!!(xaiProvider && xaiProvider.configured), true, 'xAI provider is configured after scoped key push');\n A.eq(!!(customProvider && customProvider.configured), true, 'Custom provider is configured after scoped base URL push');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('sk-provider-http-test-secret') < 0, 'provider list never leaks the pushed secret');\n A.ok(JSON.stringify(configuredProviders.body).indexOf('xai-provider-http-test-secret') < 0, 'provider list never leaks the pushed xAI secret');\n const sensitiveGets = [\n ['/api/connectors', 'connectors'],\n ['/api/cron', 'cron'],\n ['/api/checkpoint?agent=agent', 'checkpoint'],\n ['/api/notebook?agent=agent', 'notebook'],\n ['/api/save?agent=agent', 'save'],\n ['/api/runs?agent=agent&limit=5', 'runs'],\n ['/api/transcript?stream=global&agent=agent&limit=5', 'transcript'],\n ['/api/memory/proposals?agent=agent', 'memory proposals'],\n ['/api/memory/records?agent=agent', 'memory records'],\n ['/api/insights?agent=agent', 'insights']\n ];\n for (const [p, label] of sensitiveGets) {\n const noTok = await fetch(B + p);\n A.eq(noTok.status, 403, 'GET ' + label + ' WITHOUT a token -> 403');\n const withTok = await fetch(B + p, { headers: Object.assign({ Origin: B }, tok) });\n A.eq(withTok.status, 200, 'GET ' + label + ' WITH trusted browser token -> 200');\n }\n\n // ---- SSE telemetry requires the ?token= query (EventSource cannot send a header) ----\n const sseNoTok = await fetch(B + '/api/channels/events');\n A.eq(sseNoTok.status, 403, 'GET /api/channels/events WITHOUT ?token -> 403');\n const sseBadTok = await fetch(B + '/api/channels/events?token=nope');\n A.eq(sseBadTok.status, 403, 'GET /api/channels/events with a WRONG ?token -> 403');\n const sseWithTok = await fetch(B + '/api/channels/events?token=' + encodeURIComponent(apiToken), { headers: { Origin: B } });\n A.eq(sseWithTok.status, 200, 'GET /api/channels/events with the token query -> 200');\n try { if (sseWithTok.body && sseWithTok.body.cancel) await sseWithTok.body.cancel(); } catch (_) {}\n\n const noApiToken = await fetch(B + '/api/budget/resume', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ scope: 'day' }) });\n A.eq(noApiToken.status, 403, 'privileged POST without X-StarNet-Token -> 403');\n\n // ---- budget status reflects the PRE-SEEDED ledger (persisted spend survived a fresh boot) ----\n const st = await j('GET', '/api/budget/status');\n A.eq(st.status, 200, 'GET /api/budget/status -> 200');\n A.eq(st.body.runs, 2, 'status counts the two seeded runs');\n A.ok(Math.abs(st.body.totalUsd - 2.0) < 1e-9, 'totalUsd = seeded $1.25 + $0.75');\n A.eq(st.body.perRun, 3, 'perRun cap exposed (Balanced $3)');\n A.ok(st.body.day && Math.abs(st.body.day.cap - 40) < 1e-9, 'day pool base cap is $40');\n A.ok(st.body.global && Math.abs(st.body.global.cap - 100) < 1e-9, 'global pool base cap is $100');\n A.ok(Math.abs(st.body.day.usd - 2.0) < 1e-9, 'day pool reflects seeded spend inside the 24h window');\n A.ok(Math.abs(st.body.global.usd - 2.0) < 1e-9, 'global pool reflects seeded spend');\n A.ok(JSON.stringify(st.body).indexOf('sk-') < 0, 'status leaks no key-shaped secret');\n\n // ---- resume lifts a pool for the session ----\n const rs = await j('POST', '/api/budget/resume', { scope: 'day' });\n A.eq(rs.status, 200, 'POST /api/budget/resume{day} -> 200');\n A.eq(rs.body.resumed, 'day', 'resume names the scope');\n A.ok(Math.abs(rs.body.cap - 80) < 1e-9, 'resume lifts the day cap to $80 (base + base)');\n\n const st2 = await j('GET', '/api/budget/status');\n A.ok(Math.abs(st2.body.day.cap - 80) < 1e-9, 'status reflects the lifted day cap after resume');\n A.ok(Math.abs(st2.body.overrides.day - 40) < 1e-9, 'override headroom exposed');\n\n // ---- resume rejects an ungoverned / bogus scope ----\n const bad = await j('POST', '/api/budget/resume', { scope: 'bogus' });\n A.eq(bad.status, 400, 'resume of a bogus scope -> 400');\n const runScope = await j('POST', '/api/budget/resume', { scope: 'run' });\n A.eq(runScope.status, 400, 'resume of the per-run scope -> 400 (run is the loop hard cap, not a pool)');\n\n // ---- /api/run guard path (no key -> 400, zero spend) ----\n const noKey = await j('POST', '/api/run', { model: 'anthropic/claude-sonnet-4.6' });\n A.eq(noKey.status, 400, 'POST /api/run without a key -> 400');\n const noModel = await j('POST', '/api/run', { key: 'sk-or-v1-fake' });\n A.eq(noModel.status, 400, 'POST /api/run without a model -> 400');\n const badJson = await fetch(B + '/api/run', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(badJson.status, 400, 'POST /api/run with malformed JSON -> 400');\n\n // ---- /api/file media serving: typed content-type + HTTP Range (so COMMS <video>/<audio> can seek) ----\n // write a known-size clip into the agent's jailed workspace (<ws>/agent/clips/clip.webm)\n const N = 1000;\n fs.mkdirSync(path.join(ws, 'agent', 'clips'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'clips', 'clip.webm'), Buffer.alloc(N, 7));\n const fileUrl = B + '/api/file?agent=agent&path=' + encodeURIComponent('clips/clip.webm');\n\n // the deliverable-read route is token-gated too (audit C2: GET data routes were readable without a token)\n const fileNoTok = await fetch(fileUrl);\n A.eq(fileNoTok.status, 403, 'GET /api/file WITHOUT a token -> 403');\n\n const full = await fetch(fileUrl, { headers: tok });\n A.eq(full.status, 200, 'GET /api/file (with token) -> 200');\n A.eq(full.headers.get('content-type'), 'video/webm', 'webm served with a video content-type');\n A.ok(/^inline\\b/.test(full.headers.get('content-disposition') || ''), 'video deliverable is still served inline');\n A.eq(full.headers.get('accept-ranges'), 'bytes', 'full response advertises byte-range support');\n A.eq(full.headers.get('content-length'), String(N), 'full Content-Length is the file size');\n A.eq((await full.arrayBuffer()).byteLength, N, 'full body is the whole file');\n\n const ranged = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=100-199' }, tok) });\n A.eq(ranged.status, 206, 'ranged GET -> 206 Partial Content');\n A.eq(ranged.headers.get('content-range'), 'bytes 100-199/' + N, 'Content-Range names the served slice + total');\n A.eq(ranged.headers.get('content-length'), '100', 'partial Content-Length is the slice size');\n A.eq((await ranged.arrayBuffer()).byteLength, 100, 'partial body is exactly the requested 100 bytes');\n\n const suffix = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=-50' }, tok) });\n A.eq(suffix.status, 206, 'suffix range -> 206');\n A.eq(suffix.headers.get('content-range'), 'bytes 950-999/' + N, 'suffix range resolves to the last N bytes');\n\n const unsat = await fetch(fileUrl, { headers: Object.assign({ Range: 'bytes=99999-' }, tok) });\n A.eq(unsat.status, 416, 'unsatisfiable range -> 416');\n A.eq(unsat.headers.get('content-range'), 'bytes */' + N, '416 reports the full size');\n\n const head = await fetch(fileUrl, { method: 'HEAD', headers: tok });\n A.eq(head.status, 200, 'HEAD /api/file -> 200');\n A.eq(head.headers.get('content-length'), String(N), 'HEAD reports the size with no body');\n A.eq((await head.arrayBuffer()).byteLength, 0, 'HEAD carries no body');\n\n const nativeUrl = fileUrl + '&token=' + encodeURIComponent(apiToken);\n const nativeMedia = await fetch(nativeUrl, { headers: { Range: 'bytes=0-9' } });\n A.eq(nativeMedia.status, 206, 'GET /api/file with ?token supports native media loads');\n A.eq(nativeMedia.headers.get('content-range'), 'bytes 0-9/' + N, 'query-token media load still supports Range');\n\n const escape = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('../../etc/passwd'), { headers: tok });\n A.ok(escape.status === 403 || escape.status === 404, 'a jail-escape path is refused (403/404), never served');\n\n // ---- /api/file active deliverables: script-capable files download with a sandbox CSP instead of executing\n // on the app's origin with API-token authority. Media UX above stays inline/range-capable.\n fs.mkdirSync(path.join(ws, 'agent', 'active'), { recursive: true });\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'page.html'), '<script>fetch(\"/api/budget/status\")</script>');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'app.js'), 'fetch(\"/api/budget/status\")');\n fs.writeFileSync(path.join(ws, 'agent', 'active', 'vector.svg'), '<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>');\n for (const f of ['page.html', 'app.js', 'vector.svg']) {\n const activeNoTok = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f));\n A.eq(activeNoTok.status, 403, f + ' active deliverable without token is blocked');\n const active = await fetch(B + '/api/file?agent=agent&path=' + encodeURIComponent('active/' + f) + '&token=' + encodeURIComponent(apiToken));\n A.eq(active.status, 200, f + ' served for download');\n A.eq(active.headers.get('content-type'), 'application/octet-stream', f + ' loses executable content-type');\n A.ok(/^attachment\\b/.test(active.headers.get('content-disposition') || ''), f + ' is an attachment, not inline');\n A.ok(/sandbox/.test(active.headers.get('content-security-policy') || ''), f + ' carries a sandbox CSP');\n A.ok(/script-src 'none'/.test(active.headers.get('content-security-policy') || ''), f + ' explicitly denies script');\n }\n\n // ---- /api/summon/ack: the team.summon round-trip's reply leg. A stale runId/requestId is a harmless 200\n // no-op (the run already ended or auto-settled), exactly like /api/consent; malformed JSON 400s. ----\n const ackStale = await j('POST', '/api/summon/ack', { runId: 'nope', requestId: 'nope', agentId: 'researcher-2' });\n A.eq(ackStale.status, 200, 'POST /api/summon/ack with an unknown run -> 200 no-op');\n const ackBadJson = await fetch(B + '/api/summon/ack', { method: 'POST', headers: { 'X-StarNet-Token': apiToken }, body: '{not json' });\n A.eq(ackBadJson.status, 400, 'POST /api/summon/ack with malformed JSON -> 400');\n\n // ---- memory observability + new-hero reset: the declined reject-list GET, the undo-a-discard restore, and the\n // server-side memory wipe are all token-gated, agent-validated, and wired through the real router. ----\n const declNoTok = await fetch(B + '/api/memory/declined?agent=agent');\n A.eq(declNoTok.status, 403, 'GET /api/memory/declined WITHOUT a token -> 403 (gated data route)');\n const decl = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(decl.status, 200, 'GET /api/memory/declined (with token) -> 200');\n A.ok(Array.isArray(decl.body.declined), 'declined is an array (empty for a fresh agent)');\n const declBadAgent = await j('GET', '/api/memory/declined?agent=' + encodeURIComponent('../evil'));\n A.eq(declBadAgent.status, 403, 'a path-traversal agent id on the declined route -> 403');\n\n const restoreNoTok = await fetch(B + '/api/memory/declined/restore', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent', text: 'x' }) });\n A.eq(restoreNoTok.status, 403, 'POST /api/memory/declined/restore WITHOUT a token -> 403');\n const restoreNoText = await j('POST', '/api/memory/declined/restore', { agent: 'agent' });\n A.eq(restoreNoText.status, 400, 'restore without text -> 400');\n const restoreMiss = await j('POST', '/api/memory/declined/restore', { agent: 'agent', text: 'never declined' });\n A.eq(restoreMiss.status, 200, 'restore of an absent belief -> 200 (no-op)');\n A.eq(restoreMiss.body.removed, false, 'restore reports removed=false when nothing matched');\n\n const resetNoTok = await fetch(B + '/api/memory/reset', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: B }, body: JSON.stringify({ agent: 'agent' }) });\n A.eq(resetNoTok.status, 403, 'POST /api/memory/reset WITHOUT a token -> 403');\n const resetBad = await j('POST', '/api/memory/reset', { agent: '../evil' });\n A.eq(resetBad.status, 403, 'reset with a path-traversal agent id -> 403');\n const reset = await j('POST', '/api/memory/reset', { agent: 'agent' });\n A.eq(reset.status, 200, 'POST /api/memory/reset (with token) -> 200');\n A.eq(reset.body.ok, true, 'reset reports ok');\n A.eq(reset.body.agent, 'agent', 'reset echoes the agent it cleared');\n const declAfter = await j('GET', '/api/memory/declined?agent=agent');\n A.eq(JSON.stringify(declAfter.body.declined), '[]', 'the declined list reads empty after a reset');\n } finally {\n try { child.kill(); } catch (_) {}\n await sleep(150);\n try { fs.rmSync(ws, { recursive: true, force: true }); } catch (_) {}\n }\n\n A.report('sidecar.http.test');\n})().catch(e => { console.log('FAIL: sidecar.http.test threw — ' + (e && e.stack || e)); process.exit(1); });\n"}}, "sidecar/providers/registry.js": {"606027f69a39f104774733d291224243a9fff9ce": {"t": 1785746674.8911102, "n": 21820, "h": "1c728e6837fd7624", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n // Wire-hint fields on openai-compatible profiles (all sourced from the provider's official API docs,\n // verified 2026-07; the adapter also self-heals by dropping any optional param a provider 400s on):\n // wireReasoningEffort — endpoint documents the `reasoning_effort` chat-completions param.\n // wireStreamOptions: false — endpoint does not accept `stream_options` (usage still arrives:\n // these providers report usage in the stream by default).\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n credentialProbePath: '/auth/key',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n // A ChatGPT subscription reaches the SAME realtime endpoint as an OpenAI API key: its access token is\n // minted with `aud: https://api.openai.com/v1`, and a call there returns `invalid_offer` — past auth and\n // past session validation — rather than 401/403. Verified 2026-07-30 against the live endpoint.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'ash' },\n order: 10\n },\n {\n // GROK OAUTH — Grok on a SuperGrok / X Premium+ subscription via the RFC 8628 device-code flow (no API\n // key). Inference is OpenAI-compatible at api.x.ai/v1 with the OAuth access token riding in AS the Bearer\n // key. Separate id from the API-key 'xai' profile above (same wire, different auth), like codex vs openai.\n id: 'grok',\n aliases: ['grok-oauth', 'xai-oauth'],\n name: 'Grok (xAI)',\n label: 'GROK OAUTH',\n endpoint: 'OAuth, SuperGrok / X Premium+',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: null, // mirror the api-key xai profile — asserted by the live catalog, never guessed\n wireReasoningEffort: true,\n // Fallback roster for when the live /models catalog is unreachable (the xAI OAuth surface can 403 an\n // allowlisted-out account). Conservative, tools-only capability; the live catalog always wins when present.\n staticModels: [\n { id: 'grok-4', name: 'Grok 4', context_length: 256000, supportsTools: true, supportsReasoning: true },\n { id: 'grok-3', name: 'Grok 3', context_length: 131072, supportsTools: true, supportsReasoning: false },\n { id: 'grok-code-fast-1', name: 'Grok Code Fast 1', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 11\n },\n {\n // KIMI OAUTH — Moonshot's Kimi for Coding on a Kimi subscription via the RFC 8628 device-code flow. Auth\n // requests carry the kimi-cli X-Msh-* headers (extraHeaders below + the runtime X-Msh-Os-Version /\n // X-Msh-Device-Id the host injects). Inference is OpenAI-compatible at api.kimi.com/coding/v1.\n id: 'kimi',\n aliases: ['moonshot', 'kimi-code', 'kimi-oauth'],\n name: 'Kimi for Coding',\n label: 'KIMI OAUTH',\n endpoint: 'OAuth, Kimi subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.kimi.com/coding/v1',\n chatPath: '/chat/completions',\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: false,\n // The STATIC part of the kimi-cli device signature; the host adds X-Msh-Os-Version + a stable per-install\n // X-Msh-Device-Id (minted once, persisted with the tokens) at request time. Sent on inference too.\n extraHeaders: {\n 'X-Msh-Platform': 'kimi_cli',\n 'X-Msh-Version': '1.0.0',\n 'X-Msh-Device-Name': 'starnet',\n 'X-Msh-Device-Model': 'starnet'\n },\n staticModels: [\n { id: 'kimi-for-coding', name: 'Kimi for Coding', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'kimi-for-coding-highspeed', name: 'Kimi for Coding (Highspeed)', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'k3', name: 'K3', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 12\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Native speech-to-speech: the provider itself listens and speaks, so live voice needs NO second\n // credential and no transcription layer of ours. Present here only because it is PROVEN against the\n // real endpoint; a provider without a verified descriptor gets composed voice instead of a guess.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'ash' },\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n // NOTE: 'grok' is intentionally NO LONGER an alias here — it is now the id of the OAuth (subscription)\n // Grok profile below. This api.x.ai/v1 profile is the API-KEY Grok ('XAI'), a separate id, exactly as\n // 'openai' (API key) and 'codex' (OAuth) coexist. Users choosing \"GROK OAUTH\" vs \"XAI\" pick their auth.\n id: 'xai',\n aliases: ['x-ai'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Mistral strictly validates request bodies (422 \"Extra inputs are not permitted\") and its spec\n // has no stream_options; its stream reports usage in the final chunk without it.\n wireStreamOptions: false,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n // Perplexity's /chat/completions schema has no tools/tool_choice — function calling lives on its\n // separate Agent API. It DOES document its own reasoning_effort, and streams usage by default.\n supportsTools: false,\n supportsReasoning: null,\n wireReasoningEffort: true,\n wireStreamOptions: false,\n // Perplexity has NO usable /models for chat completions (its /v1/models lists Agent-API ids),\n // so the connect screen would be empty and contextLimit 0 (compaction off). Static Sonar roster\n // from docs.perplexity.ai/docs/sonar/models, verified 2026-07 (sonar-reasoning was removed\n // 2025-12-15). Deliberately NO pricing: Perplexity bills per-request search fees on top of\n // tokens, so token-only catalog pricing would under-report real cost — 'unpriced' is the\n // honest label until the provider reports cost on the wire.\n staticModels: [\n { id: 'sonar', name: 'Sonar', context_length: 128000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-pro', name: 'Sonar Pro', context_length: 200000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-reasoning-pro', name: 'Sonar Reasoning Pro', context_length: 128000, supportsTools: false, supportsReasoning: true },\n { id: 'sonar-deep-research', name: 'Sonar Deep Research', context_length: 128000, supportsTools: false, supportsReasoning: true }\n ],\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n wireReasoningEffort: true,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n // True for any profile on the STANDARD RFC 8628 device-code wire (grok / kimi). Codex is DELIBERATELY excluded\n // (it speaks OpenAI's proprietary usercode/exchange wire and keeps flowing through the providerUsesCodex paths\n // untouched) — so a caller can branch: codex -> ensureCodexAccessToken, device-oauth -> ensureOAuthAccessToken.\n function providerUsesDeviceOAuth(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.authType === 'oauth_device_code' && profile.id !== 'codex');\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n providerUsesDeviceOAuth,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "ac2017206a0a632d95e3564f91bee9c042a77036": {"t": 1785754192.3280056, "n": 21820, "h": "1c728e6837fd7624", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n // Wire-hint fields on openai-compatible profiles (all sourced from the provider's official API docs,\n // verified 2026-07; the adapter also self-heals by dropping any optional param a provider 400s on):\n // wireReasoningEffort — endpoint documents the `reasoning_effort` chat-completions param.\n // wireStreamOptions: false — endpoint does not accept `stream_options` (usage still arrives:\n // these providers report usage in the stream by default).\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n credentialProbePath: '/auth/key',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n // A ChatGPT subscription reaches the SAME realtime endpoint as an OpenAI API key: its access token is\n // minted with `aud: https://api.openai.com/v1`, and a call there returns `invalid_offer` — past auth and\n // past session validation — rather than 401/403. Verified 2026-07-30 against the live endpoint.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'ash' },\n order: 10\n },\n {\n // GROK OAUTH — Grok on a SuperGrok / X Premium+ subscription via the RFC 8628 device-code flow (no API\n // key). Inference is OpenAI-compatible at api.x.ai/v1 with the OAuth access token riding in AS the Bearer\n // key. Separate id from the API-key 'xai' profile above (same wire, different auth), like codex vs openai.\n id: 'grok',\n aliases: ['grok-oauth', 'xai-oauth'],\n name: 'Grok (xAI)',\n label: 'GROK OAUTH',\n endpoint: 'OAuth, SuperGrok / X Premium+',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: null, // mirror the api-key xai profile — asserted by the live catalog, never guessed\n wireReasoningEffort: true,\n // Fallback roster for when the live /models catalog is unreachable (the xAI OAuth surface can 403 an\n // allowlisted-out account). Conservative, tools-only capability; the live catalog always wins when present.\n staticModels: [\n { id: 'grok-4', name: 'Grok 4', context_length: 256000, supportsTools: true, supportsReasoning: true },\n { id: 'grok-3', name: 'Grok 3', context_length: 131072, supportsTools: true, supportsReasoning: false },\n { id: 'grok-code-fast-1', name: 'Grok Code Fast 1', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 11\n },\n {\n // KIMI OAUTH — Moonshot's Kimi for Coding on a Kimi subscription via the RFC 8628 device-code flow. Auth\n // requests carry the kimi-cli X-Msh-* headers (extraHeaders below + the runtime X-Msh-Os-Version /\n // X-Msh-Device-Id the host injects). Inference is OpenAI-compatible at api.kimi.com/coding/v1.\n id: 'kimi',\n aliases: ['moonshot', 'kimi-code', 'kimi-oauth'],\n name: 'Kimi for Coding',\n label: 'KIMI OAUTH',\n endpoint: 'OAuth, Kimi subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.kimi.com/coding/v1',\n chatPath: '/chat/completions',\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: false,\n // The STATIC part of the kimi-cli device signature; the host adds X-Msh-Os-Version + a stable per-install\n // X-Msh-Device-Id (minted once, persisted with the tokens) at request time. Sent on inference too.\n extraHeaders: {\n 'X-Msh-Platform': 'kimi_cli',\n 'X-Msh-Version': '1.0.0',\n 'X-Msh-Device-Name': 'starnet',\n 'X-Msh-Device-Model': 'starnet'\n },\n staticModels: [\n { id: 'kimi-for-coding', name: 'Kimi for Coding', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'kimi-for-coding-highspeed', name: 'Kimi for Coding (Highspeed)', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'k3', name: 'K3', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 12\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Native speech-to-speech: the provider itself listens and speaks, so live voice needs NO second\n // credential and no transcription layer of ours. Present here only because it is PROVEN against the\n // real endpoint; a provider without a verified descriptor gets composed voice instead of a guess.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'ash' },\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n // NOTE: 'grok' is intentionally NO LONGER an alias here — it is now the id of the OAuth (subscription)\n // Grok profile below. This api.x.ai/v1 profile is the API-KEY Grok ('XAI'), a separate id, exactly as\n // 'openai' (API key) and 'codex' (OAuth) coexist. Users choosing \"GROK OAUTH\" vs \"XAI\" pick their auth.\n id: 'xai',\n aliases: ['x-ai'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Mistral strictly validates request bodies (422 \"Extra inputs are not permitted\") and its spec\n // has no stream_options; its stream reports usage in the final chunk without it.\n wireStreamOptions: false,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n // Perplexity's /chat/completions schema has no tools/tool_choice — function calling lives on its\n // separate Agent API. It DOES document its own reasoning_effort, and streams usage by default.\n supportsTools: false,\n supportsReasoning: null,\n wireReasoningEffort: true,\n wireStreamOptions: false,\n // Perplexity has NO usable /models for chat completions (its /v1/models lists Agent-API ids),\n // so the connect screen would be empty and contextLimit 0 (compaction off). Static Sonar roster\n // from docs.perplexity.ai/docs/sonar/models, verified 2026-07 (sonar-reasoning was removed\n // 2025-12-15). Deliberately NO pricing: Perplexity bills per-request search fees on top of\n // tokens, so token-only catalog pricing would under-report real cost — 'unpriced' is the\n // honest label until the provider reports cost on the wire.\n staticModels: [\n { id: 'sonar', name: 'Sonar', context_length: 128000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-pro', name: 'Sonar Pro', context_length: 200000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-reasoning-pro', name: 'Sonar Reasoning Pro', context_length: 128000, supportsTools: false, supportsReasoning: true },\n { id: 'sonar-deep-research', name: 'Sonar Deep Research', context_length: 128000, supportsTools: false, supportsReasoning: true }\n ],\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n wireReasoningEffort: true,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n // True for any profile on the STANDARD RFC 8628 device-code wire (grok / kimi). Codex is DELIBERATELY excluded\n // (it speaks OpenAI's proprietary usercode/exchange wire and keeps flowing through the providerUsesCodex paths\n // untouched) — so a caller can branch: codex -> ensureCodexAccessToken, device-oauth -> ensureOAuthAccessToken.\n function providerUsesDeviceOAuth(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.authType === 'oauth_device_code' && profile.id !== 'codex');\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n providerUsesDeviceOAuth,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "8c1f4a47fb4c4505897f40c6f048b92c76fa8db5": {"t": 1785754193.713744, "n": 21780, "h": "0fa64e331507eedd", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n // Wire-hint fields on openai-compatible profiles (all sourced from the provider's official API docs,\n // verified 2026-07; the adapter also self-heals by dropping any optional param a provider 400s on):\n // wireReasoningEffort — endpoint documents the `reasoning_effort` chat-completions param.\n // wireStreamOptions: false — endpoint does not accept `stream_options` (usage still arrives:\n // these providers report usage in the stream by default).\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n // A ChatGPT subscription reaches the SAME realtime endpoint as an OpenAI API key: its access token is\n // minted with `aud: https://api.openai.com/v1`, and a call there returns `invalid_offer` — past auth and\n // past session validation — rather than 401/403. Verified 2026-07-30 against the live endpoint.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'ash' },\n order: 10\n },\n {\n // GROK OAUTH — Grok on a SuperGrok / X Premium+ subscription via the RFC 8628 device-code flow (no API\n // key). Inference is OpenAI-compatible at api.x.ai/v1 with the OAuth access token riding in AS the Bearer\n // key. Separate id from the API-key 'xai' profile above (same wire, different auth), like codex vs openai.\n id: 'grok',\n aliases: ['grok-oauth', 'xai-oauth'],\n name: 'Grok (xAI)',\n label: 'GROK OAUTH',\n endpoint: 'OAuth, SuperGrok / X Premium+',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: null, // mirror the api-key xai profile — asserted by the live catalog, never guessed\n wireReasoningEffort: true,\n // Fallback roster for when the live /models catalog is unreachable (the xAI OAuth surface can 403 an\n // allowlisted-out account). Conservative, tools-only capability; the live catalog always wins when present.\n staticModels: [\n { id: 'grok-4', name: 'Grok 4', context_length: 256000, supportsTools: true, supportsReasoning: true },\n { id: 'grok-3', name: 'Grok 3', context_length: 131072, supportsTools: true, supportsReasoning: false },\n { id: 'grok-code-fast-1', name: 'Grok Code Fast 1', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 11\n },\n {\n // KIMI OAUTH — Moonshot's Kimi for Coding on a Kimi subscription via the RFC 8628 device-code flow. Auth\n // requests carry the kimi-cli X-Msh-* headers (extraHeaders below + the runtime X-Msh-Os-Version /\n // X-Msh-Device-Id the host injects). Inference is OpenAI-compatible at api.kimi.com/coding/v1.\n id: 'kimi',\n aliases: ['moonshot', 'kimi-code', 'kimi-oauth'],\n name: 'Kimi for Coding',\n label: 'KIMI OAUTH',\n endpoint: 'OAuth, Kimi subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.kimi.com/coding/v1',\n chatPath: '/chat/completions',\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: false,\n // The STATIC part of the kimi-cli device signature; the host adds X-Msh-Os-Version + a stable per-install\n // X-Msh-Device-Id (minted once, persisted with the tokens) at request time. Sent on inference too.\n extraHeaders: {\n 'X-Msh-Platform': 'kimi_cli',\n 'X-Msh-Version': '1.0.0',\n 'X-Msh-Device-Name': 'starnet',\n 'X-Msh-Device-Model': 'starnet'\n },\n staticModels: [\n { id: 'kimi-for-coding', name: 'Kimi for Coding', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'kimi-for-coding-highspeed', name: 'Kimi for Coding (Highspeed)', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'k3', name: 'K3', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 12\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Native speech-to-speech: the provider itself listens and speaks, so live voice needs NO second\n // credential and no transcription layer of ours. Present here only because it is PROVEN against the\n // real endpoint; a provider without a verified descriptor gets composed voice instead of a guess.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'ash' },\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n // NOTE: 'grok' is intentionally NO LONGER an alias here — it is now the id of the OAuth (subscription)\n // Grok profile below. This api.x.ai/v1 profile is the API-KEY Grok ('XAI'), a separate id, exactly as\n // 'openai' (API key) and 'codex' (OAuth) coexist. Users choosing \"GROK OAUTH\" vs \"XAI\" pick their auth.\n id: 'xai',\n aliases: ['x-ai'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Mistral strictly validates request bodies (422 \"Extra inputs are not permitted\") and its spec\n // has no stream_options; its stream reports usage in the final chunk without it.\n wireStreamOptions: false,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n // Perplexity's /chat/completions schema has no tools/tool_choice — function calling lives on its\n // separate Agent API. It DOES document its own reasoning_effort, and streams usage by default.\n supportsTools: false,\n supportsReasoning: null,\n wireReasoningEffort: true,\n wireStreamOptions: false,\n // Perplexity has NO usable /models for chat completions (its /v1/models lists Agent-API ids),\n // so the connect screen would be empty and contextLimit 0 (compaction off). Static Sonar roster\n // from docs.perplexity.ai/docs/sonar/models, verified 2026-07 (sonar-reasoning was removed\n // 2025-12-15). Deliberately NO pricing: Perplexity bills per-request search fees on top of\n // tokens, so token-only catalog pricing would under-report real cost — 'unpriced' is the\n // honest label until the provider reports cost on the wire.\n staticModels: [\n { id: 'sonar', name: 'Sonar', context_length: 128000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-pro', name: 'Sonar Pro', context_length: 200000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-reasoning-pro', name: 'Sonar Reasoning Pro', context_length: 128000, supportsTools: false, supportsReasoning: true },\n { id: 'sonar-deep-research', name: 'Sonar Deep Research', context_length: 128000, supportsTools: false, supportsReasoning: true }\n ],\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n wireReasoningEffort: true,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n // True for any profile on the STANDARD RFC 8628 device-code wire (grok / kimi). Codex is DELIBERATELY excluded\n // (it speaks OpenAI's proprietary usercode/exchange wire and keeps flowing through the providerUsesCodex paths\n // untouched) — so a caller can branch: codex -> ensureCodexAccessToken, device-oauth -> ensureOAuthAccessToken.\n function providerUsesDeviceOAuth(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.authType === 'oauth_device_code' && profile.id !== 'codex');\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n providerUsesDeviceOAuth,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "9768d8729ded4e5de098b700687c9f047724e89e": {"t": 1785754195.1799898, "n": 21784, "h": "341b8620f2fe138b", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n // Wire-hint fields on openai-compatible profiles (all sourced from the provider's official API docs,\n // verified 2026-07; the adapter also self-heals by dropping any optional param a provider 400s on):\n // wireReasoningEffort — endpoint documents the `reasoning_effort` chat-completions param.\n // wireStreamOptions: false — endpoint does not accept `stream_options` (usage still arrives:\n // these providers report usage in the stream by default).\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n // A ChatGPT subscription reaches the SAME realtime endpoint as an OpenAI API key: its access token is\n // minted with `aud: https://api.openai.com/v1`, and a call there returns `invalid_offer` — past auth and\n // past session validation — rather than 401/403. Verified 2026-07-30 against the live endpoint.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'marin' },\n order: 10\n },\n {\n // GROK OAUTH — Grok on a SuperGrok / X Premium+ subscription via the RFC 8628 device-code flow (no API\n // key). Inference is OpenAI-compatible at api.x.ai/v1 with the OAuth access token riding in AS the Bearer\n // key. Separate id from the API-key 'xai' profile above (same wire, different auth), like codex vs openai.\n id: 'grok',\n aliases: ['grok-oauth', 'xai-oauth'],\n name: 'Grok (xAI)',\n label: 'GROK OAUTH',\n endpoint: 'OAuth, SuperGrok / X Premium+',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: null, // mirror the api-key xai profile — asserted by the live catalog, never guessed\n wireReasoningEffort: true,\n // Fallback roster for when the live /models catalog is unreachable (the xAI OAuth surface can 403 an\n // allowlisted-out account). Conservative, tools-only capability; the live catalog always wins when present.\n staticModels: [\n { id: 'grok-4', name: 'Grok 4', context_length: 256000, supportsTools: true, supportsReasoning: true },\n { id: 'grok-3', name: 'Grok 3', context_length: 131072, supportsTools: true, supportsReasoning: false },\n { id: 'grok-code-fast-1', name: 'Grok Code Fast 1', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 11\n },\n {\n // KIMI OAUTH — Moonshot's Kimi for Coding on a Kimi subscription via the RFC 8628 device-code flow. Auth\n // requests carry the kimi-cli X-Msh-* headers (extraHeaders below + the runtime X-Msh-Os-Version /\n // X-Msh-Device-Id the host injects). Inference is OpenAI-compatible at api.kimi.com/coding/v1.\n id: 'kimi',\n aliases: ['moonshot', 'kimi-code', 'kimi-oauth'],\n name: 'Kimi for Coding',\n label: 'KIMI OAUTH',\n endpoint: 'OAuth, Kimi subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.kimi.com/coding/v1',\n chatPath: '/chat/completions',\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: false,\n // The STATIC part of the kimi-cli device signature; the host adds X-Msh-Os-Version + a stable per-install\n // X-Msh-Device-Id (minted once, persisted with the tokens) at request time. Sent on inference too.\n extraHeaders: {\n 'X-Msh-Platform': 'kimi_cli',\n 'X-Msh-Version': '1.0.0',\n 'X-Msh-Device-Name': 'starnet',\n 'X-Msh-Device-Model': 'starnet'\n },\n staticModels: [\n { id: 'kimi-for-coding', name: 'Kimi for Coding', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'kimi-for-coding-highspeed', name: 'Kimi for Coding (Highspeed)', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'k3', name: 'K3', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 12\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Native speech-to-speech: the provider itself listens and speaks, so live voice needs NO second\n // credential and no transcription layer of ours. Present here only because it is PROVEN against the\n // real endpoint; a provider without a verified descriptor gets composed voice instead of a guess.\n liveVoice: { transport: 'webrtc', url: 'https://api.openai.com/v1/realtime/calls', model: 'gpt-realtime-2.1', voice: 'marin' },\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n // NOTE: 'grok' is intentionally NO LONGER an alias here — it is now the id of the OAuth (subscription)\n // Grok profile below. This api.x.ai/v1 profile is the API-KEY Grok ('XAI'), a separate id, exactly as\n // 'openai' (API key) and 'codex' (OAuth) coexist. Users choosing \"GROK OAUTH\" vs \"XAI\" pick their auth.\n id: 'xai',\n aliases: ['x-ai'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Mistral strictly validates request bodies (422 \"Extra inputs are not permitted\") and its spec\n // has no stream_options; its stream reports usage in the final chunk without it.\n wireStreamOptions: false,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n // Perplexity's /chat/completions schema has no tools/tool_choice — function calling lives on its\n // separate Agent API. It DOES document its own reasoning_effort, and streams usage by default.\n supportsTools: false,\n supportsReasoning: null,\n wireReasoningEffort: true,\n wireStreamOptions: false,\n // Perplexity has NO usable /models for chat completions (its /v1/models lists Agent-API ids),\n // so the connect screen would be empty and contextLimit 0 (compaction off). Static Sonar roster\n // from docs.perplexity.ai/docs/sonar/models, verified 2026-07 (sonar-reasoning was removed\n // 2025-12-15). Deliberately NO pricing: Perplexity bills per-request search fees on top of\n // tokens, so token-only catalog pricing would under-report real cost — 'unpriced' is the\n // honest label until the provider reports cost on the wire.\n staticModels: [\n { id: 'sonar', name: 'Sonar', context_length: 128000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-pro', name: 'Sonar Pro', context_length: 200000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-reasoning-pro', name: 'Sonar Reasoning Pro', context_length: 128000, supportsTools: false, supportsReasoning: true },\n { id: 'sonar-deep-research', name: 'Sonar Deep Research', context_length: 128000, supportsTools: false, supportsReasoning: true }\n ],\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n wireReasoningEffort: true,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n // True for any profile on the STANDARD RFC 8628 device-code wire (grok / kimi). Codex is DELIBERATELY excluded\n // (it speaks OpenAI's proprietary usercode/exchange wire and keeps flowing through the providerUsesCodex paths\n // untouched) — so a caller can branch: codex -> ensureCodexAccessToken, device-oauth -> ensureOAuthAccessToken.\n function providerUsesDeviceOAuth(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.authType === 'oauth_device_code' && profile.id !== 'codex');\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n providerUsesDeviceOAuth,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "aa7f5d8d9f320972dddc90cde3488ea501139163": {"t": 1785754196.4406977, "n": 20876, "h": "4526aba9a8e47391", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n // Wire-hint fields on openai-compatible profiles (all sourced from the provider's official API docs,\n // verified 2026-07; the adapter also self-heals by dropping any optional param a provider 400s on):\n // wireReasoningEffort — endpoint documents the `reasoning_effort` chat-completions param.\n // wireStreamOptions: false — endpoint does not accept `stream_options` (usage still arrives:\n // these providers report usage in the stream by default).\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n order: 10\n },\n {\n // GROK OAUTH — Grok on a SuperGrok / X Premium+ subscription via the RFC 8628 device-code flow (no API\n // key). Inference is OpenAI-compatible at api.x.ai/v1 with the OAuth access token riding in AS the Bearer\n // key. Separate id from the API-key 'xai' profile above (same wire, different auth), like codex vs openai.\n id: 'grok',\n aliases: ['grok-oauth', 'xai-oauth'],\n name: 'Grok (xAI)',\n label: 'GROK OAUTH',\n endpoint: 'OAuth, SuperGrok / X Premium+',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: null, // mirror the api-key xai profile — asserted by the live catalog, never guessed\n wireReasoningEffort: true,\n // Fallback roster for when the live /models catalog is unreachable (the xAI OAuth surface can 403 an\n // allowlisted-out account). Conservative, tools-only capability; the live catalog always wins when present.\n staticModels: [\n { id: 'grok-4', name: 'Grok 4', context_length: 256000, supportsTools: true, supportsReasoning: true },\n { id: 'grok-3', name: 'Grok 3', context_length: 131072, supportsTools: true, supportsReasoning: false },\n { id: 'grok-code-fast-1', name: 'Grok Code Fast 1', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 11\n },\n {\n // KIMI OAUTH — Moonshot's Kimi for Coding on a Kimi subscription via the RFC 8628 device-code flow. Auth\n // requests carry the kimi-cli X-Msh-* headers (extraHeaders below + the runtime X-Msh-Os-Version /\n // X-Msh-Device-Id the host injects). Inference is OpenAI-compatible at api.kimi.com/coding/v1.\n id: 'kimi',\n aliases: ['moonshot', 'kimi-code', 'kimi-oauth'],\n name: 'Kimi for Coding',\n label: 'KIMI OAUTH',\n endpoint: 'OAuth, Kimi subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'oauth_device_code',\n keyRequired: false,\n modelsRequireAuth: true,\n baseUrl: 'https://api.kimi.com/coding/v1',\n chatPath: '/chat/completions',\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: false,\n // The STATIC part of the kimi-cli device signature; the host adds X-Msh-Os-Version + a stable per-install\n // X-Msh-Device-Id (minted once, persisted with the tokens) at request time. Sent on inference too.\n extraHeaders: {\n 'X-Msh-Platform': 'kimi_cli',\n 'X-Msh-Version': '1.0.0',\n 'X-Msh-Device-Name': 'starnet',\n 'X-Msh-Device-Model': 'starnet'\n },\n staticModels: [\n { id: 'kimi-for-coding', name: 'Kimi for Coding', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'kimi-for-coding-highspeed', name: 'Kimi for Coding (Highspeed)', context_length: 256000, supportsTools: true, supportsReasoning: false },\n { id: 'k3', name: 'K3', context_length: 256000, supportsTools: true, supportsReasoning: false }\n ],\n order: 12\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n // NOTE: 'grok' is intentionally NO LONGER an alias here — it is now the id of the OAuth (subscription)\n // Grok profile below. This api.x.ai/v1 profile is the API-KEY Grok ('XAI'), a separate id, exactly as\n // 'openai' (API key) and 'codex' (OAuth) coexist. Users choosing \"GROK OAUTH\" vs \"XAI\" pick their auth.\n id: 'xai',\n aliases: ['x-ai'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Mistral strictly validates request bodies (422 \"Extra inputs are not permitted\") and its spec\n // has no stream_options; its stream reports usage in the final chunk without it.\n wireStreamOptions: false,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n // Perplexity's /chat/completions schema has no tools/tool_choice — function calling lives on its\n // separate Agent API. It DOES document its own reasoning_effort, and streams usage by default.\n supportsTools: false,\n supportsReasoning: null,\n wireReasoningEffort: true,\n wireStreamOptions: false,\n // Perplexity has NO usable /models for chat completions (its /v1/models lists Agent-API ids),\n // so the connect screen would be empty and contextLimit 0 (compaction off). Static Sonar roster\n // from docs.perplexity.ai/docs/sonar/models, verified 2026-07 (sonar-reasoning was removed\n // 2025-12-15). Deliberately NO pricing: Perplexity bills per-request search fees on top of\n // tokens, so token-only catalog pricing would under-report real cost — 'unpriced' is the\n // honest label until the provider reports cost on the wire.\n staticModels: [\n { id: 'sonar', name: 'Sonar', context_length: 128000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-pro', name: 'Sonar Pro', context_length: 200000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-reasoning-pro', name: 'Sonar Reasoning Pro', context_length: 128000, supportsTools: false, supportsReasoning: true },\n { id: 'sonar-deep-research', name: 'Sonar Deep Research', context_length: 128000, supportsTools: false, supportsReasoning: true }\n ],\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n wireReasoningEffort: true,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n // True for any profile on the STANDARD RFC 8628 device-code wire (grok / kimi). Codex is DELIBERATELY excluded\n // (it speaks OpenAI's proprietary usercode/exchange wire and keeps flowing through the providerUsesCodex paths\n // untouched) — so a caller can branch: codex -> ensureCodexAccessToken, device-oauth -> ensureOAuthAccessToken.\n function providerUsesDeviceOAuth(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.authType === 'oauth_device_code' && profile.id !== 'codex');\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n providerUsesDeviceOAuth,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "c9b7f4900909a3df555f415c50dd44a798b6048f": {"t": 1785754197.6750538, "n": 16472, "h": "2c064fbb1c1906da", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n // Wire-hint fields on openai-compatible profiles (all sourced from the provider's official API docs,\n // verified 2026-07; the adapter also self-heals by dropping any optional param a provider 400s on):\n // wireReasoningEffort — endpoint documents the `reasoning_effort` chat-completions param.\n // wireStreamOptions: false — endpoint does not accept `stream_options` (usage still arrives:\n // these providers report usage in the stream by default).\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n order: 10\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n id: 'xai',\n aliases: ['x-ai', 'grok'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Mistral strictly validates request bodies (422 \"Extra inputs are not permitted\") and its spec\n // has no stream_options; its stream reports usage in the final chunk without it.\n wireStreamOptions: false,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n // Perplexity's /chat/completions schema has no tools/tool_choice — function calling lives on its\n // separate Agent API. It DOES document its own reasoning_effort, and streams usage by default.\n supportsTools: false,\n supportsReasoning: null,\n wireReasoningEffort: true,\n wireStreamOptions: false,\n // Perplexity has NO usable /models for chat completions (its /v1/models lists Agent-API ids),\n // so the connect screen would be empty and contextLimit 0 (compaction off). Static Sonar roster\n // from docs.perplexity.ai/docs/sonar/models, verified 2026-07 (sonar-reasoning was removed\n // 2025-12-15). Deliberately NO pricing: Perplexity bills per-request search fees on top of\n // tokens, so token-only catalog pricing would under-report real cost — 'unpriced' is the\n // honest label until the provider reports cost on the wire.\n staticModels: [\n { id: 'sonar', name: 'Sonar', context_length: 128000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-pro', name: 'Sonar Pro', context_length: 200000, supportsTools: false, supportsReasoning: false },\n { id: 'sonar-reasoning-pro', name: 'Sonar Reasoning Pro', context_length: 128000, supportsTools: false, supportsReasoning: true },\n { id: 'sonar-deep-research', name: 'Sonar Deep Research', context_length: 128000, supportsTools: false, supportsReasoning: true }\n ],\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n wireReasoningEffort: true,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "9a933142efc05e944e6b309588bc956ef11d6cd9": {"t": 1785754198.9220862, "n": 15369, "h": "0943602280522348", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n // Wire-hint fields on openai-compatible profiles (all sourced from the provider's official API docs,\n // verified 2026-07; the adapter also self-heals by dropping any optional param a provider 400s on):\n // wireReasoningEffort — endpoint documents the `reasoning_effort` chat-completions param.\n // wireStreamOptions: false — endpoint does not accept `stream_options` (usage still arrives:\n // these providers report usage in the stream by default).\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n order: 10\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n id: 'xai',\n aliases: ['x-ai', 'grok'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n // Mistral strictly validates request bodies (422 \"Extra inputs are not permitted\") and its spec\n // has no stream_options; its stream reports usage in the final chunk without it.\n wireStreamOptions: false,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n // Perplexity's /chat/completions schema has no tools/tool_choice — function calling lives on its\n // separate Agent API. It DOES document its own reasoning_effort, and streams usage by default.\n supportsTools: false,\n supportsReasoning: null,\n wireReasoningEffort: true,\n wireStreamOptions: false,\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n wireReasoningEffort: true,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n wireReasoningEffort: true,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "6ddd649e56586844873c814b0410d891d28eea54": {"t": 1785754200.1115296, "n": 14112, "h": "e6faf5377e462be4", "c": "/* sidecar/providers/registry.js - provider profiles, following the reference harness.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n order: 10\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n id: 'xai',\n aliases: ['x-ai', 'grok'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "5736698f0eae4cc71efb3408060a68824b694eb7": {"t": 1785754201.2892005, "n": 14092, "h": "7ab20f3c73592e59", "c": "/* sidecar/providers/registry.js - Hermes-style provider profiles.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n order: 10\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n id: 'xai',\n aliases: ['x-ai', 'grok'],\n name: 'xAI',\n label: 'XAI',\n endpoint: 'api.x.ai/v1',\n blurb: 'Grok OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['XAI_API_KEY', 'X_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.x.ai/v1',\n baseUrlEnv: ['XAI_BASE_URL', 'X_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 37\n },\n {\n id: 'groq',\n aliases: [],\n name: 'Groq',\n label: 'GROQ',\n endpoint: 'api.groq.com/openai/v1',\n blurb: 'fast OpenAI-compatible inference',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GROQ_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.groq.com/openai/v1',\n baseUrlEnv: ['GROQ_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 38\n },\n {\n id: 'mistral',\n aliases: ['mistralai'],\n name: 'Mistral AI',\n label: 'MISTRAL',\n endpoint: 'api.mistral.ai/v1',\n blurb: 'Mistral OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['MISTRAL_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.mistral.ai/v1',\n baseUrlEnv: ['MISTRAL_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 39\n },\n {\n id: 'deepseek',\n aliases: [],\n name: 'DeepSeek',\n label: 'DEEPSEEK',\n endpoint: 'api.deepseek.com',\n blurb: 'DeepSeek OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['DEEPSEEK_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.deepseek.com',\n baseUrlEnv: ['DEEPSEEK_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 40\n },\n {\n id: 'together',\n aliases: ['together-ai'],\n name: 'Together AI',\n label: 'TOGETHER',\n endpoint: 'api.together.ai/v1',\n blurb: 'Together OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['TOGETHER_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.together.ai/v1',\n baseUrlEnv: ['TOGETHER_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 41\n },\n {\n id: 'fireworks',\n aliases: ['fireworks-ai'],\n name: 'Fireworks AI',\n label: 'FIREWORKS',\n endpoint: 'api.fireworks.ai/inference/v1',\n blurb: 'Fireworks OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['FIREWORKS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.fireworks.ai/inference/v1',\n baseUrlEnv: ['FIREWORKS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 42\n },\n {\n id: 'perplexity',\n aliases: ['pplx', 'sonar'],\n name: 'Perplexity',\n label: 'PERPLEXITY',\n endpoint: 'api.perplexity.ai',\n blurb: 'Sonar chat completions API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['PERPLEXITY_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.perplexity.ai',\n baseUrlEnv: ['PERPLEXITY_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 43\n },\n {\n id: 'cerebras',\n aliases: [],\n name: 'Cerebras',\n label: 'CEREBRAS',\n endpoint: 'api.cerebras.ai/v1',\n blurb: 'Cerebras OpenAI-compatible API',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['CEREBRAS_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.cerebras.ai/v1',\n baseUrlEnv: ['CEREBRAS_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 44\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n order: 60\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 70\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "996330e80c6d598340abf12c2269ea83d52c336b": {"t": 1785754202.4691517, "n": 8600, "h": "f425b66b2efab44f", "c": "/* sidecar/providers/registry.js - Hermes-style provider profiles.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n order: 10\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 30\n },\n {\n id: 'anthropic',\n aliases: ['claude'],\n name: 'Anthropic',\n label: 'ANTHROPIC',\n endpoint: 'api.anthropic.com/v1',\n blurb: 'Claude native Messages API',\n live: true,\n adapter: 'anthropic',\n apiMode: 'anthropic_messages',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['ANTHROPIC_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.anthropic.com/v1',\n baseUrlEnv: ['ANTHROPIC_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 35\n },\n {\n id: 'gemini',\n aliases: ['google', 'google-ai', 'google-gemini'],\n name: 'Google Gemini',\n label: 'GEMINI',\n endpoint: 'generativelanguage.googleapis.com/v1beta',\n blurb: 'Gemini native GenerateContent API',\n live: true,\n adapter: 'gemini',\n apiMode: 'gemini_generate_content',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_AI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta',\n baseUrlEnv: ['GEMINI_BASE_URL', 'GOOGLE_AI_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: true,\n supportsReasoning: null,\n order: 36\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n order: 40\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 50\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}, "c1a11ddf89985d8d6534680c940dcb2b7b818ebe": {"t": 1785754203.7471592, "n": 7100, "h": "0b7add49b8028763", "c": "/* sidecar/providers/registry.js - Hermes-style provider profiles.\n Profiles are metadata only: adapters still implement the LLMProvider stream seam.\n This keeps provider discovery, auth shape, defaults, aliases, model endpoints, and UI\n labels in one place instead of spreading hardcoded provider ids through the host. */\n'use strict';\n(function (root, factory) {\n const api = factory();\n if (typeof module !== 'undefined' && module.exports) module.exports = api;\n else { root.SK = root.SK || {}; root.SK.providers = root.SK.providers || {}; root.SK.providers.registry = api; }\n})(typeof globalThis !== 'undefined' ? globalThis : this, function () {\n 'use strict';\n\n const DEFAULT_PROVIDER_ID = 'openrouter';\n\n const PROFILES = [\n {\n id: 'openrouter',\n aliases: ['or'],\n name: 'OpenRouter',\n label: 'OPENROUTER',\n endpoint: 'openrouter.ai/api/v1',\n blurb: 'one key, broad model catalog',\n live: true,\n adapter: 'openrouter',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENROUTER_KEY', 'OPENROUTER_API_KEY'],\n modelsRequireAuth: false,\n baseUrl: 'https://openrouter.ai/api/v1',\n baseUrlEnv: ['OPENROUTER_BASE'],\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: 'catalog',\n supportsReasoning: 'catalog',\n order: 20\n },\n {\n id: 'codex',\n aliases: ['openai-codex'],\n name: 'ChatGPT Codex',\n label: 'CHATGPT (CODEX)',\n endpoint: 'OAuth, ChatGPT subscription',\n blurb: 'sign in, no API key',\n live: true,\n adapter: 'codex',\n apiMode: 'codex_responses',\n authType: 'oauth_device_code',\n keyRequired: false,\n baseUrl: 'https://chatgpt.com/backend-api/codex',\n defaultReasoningEffort: 'low',\n unmetered: true,\n credentialPool: false,\n supportsTools: true,\n supportsReasoning: true,\n order: 10\n },\n {\n id: 'openai',\n aliases: ['openai-api'],\n name: 'OpenAI API',\n label: 'OPENAI API',\n endpoint: 'api.openai.com/v1',\n blurb: 'OpenAI-compatible chat completions',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key',\n keyRequired: true,\n keyEnv: ['OPENAI_API_KEY'],\n modelsRequireAuth: true,\n baseUrl: 'https://api.openai.com/v1',\n modelsPath: '/models',\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 30\n },\n {\n id: 'ollama',\n aliases: ['ollama-local'],\n name: 'Ollama',\n label: 'OLLAMA',\n endpoint: '127.0.0.1:11434/v1',\n blurb: 'local OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'none',\n keyRequired: false,\n modelsRequireAuth: false,\n baseUrl: 'http://127.0.0.1:11434/v1',\n baseUrlEnv: ['OLLAMA_BASE_URL'],\n modelsPath: '/models',\n defaultReasoningEffort: 'none',\n unmetered: true,\n credentialPool: false,\n supportsTools: null,\n supportsReasoning: false,\n order: 40\n },\n {\n id: 'custom',\n aliases: ['openai-compatible', 'local', 'vllm', 'lmstudio'],\n name: 'Custom OpenAI-Compatible',\n label: 'CUSTOM',\n endpoint: 'your /v1 endpoint',\n blurb: 'bring any OpenAI-compatible endpoint',\n live: true,\n adapter: 'openai-compatible',\n apiMode: 'chat_completions',\n authType: 'api_key_optional',\n keyRequired: false,\n keyEnv: ['CUSTOM_OPENAI_KEY', 'OPENAI_COMPATIBLE_KEY'],\n modelsRequireAuth: false,\n baseUrl: '',\n baseUrlEnv: ['CUSTOM_OPENAI_BASE_URL', 'OPENAI_COMPATIBLE_BASE_URL'],\n modelsPath: '/models',\n requiresBaseUrl: true,\n defaultReasoningEffort: 'medium',\n unmetered: false,\n credentialPool: true,\n supportsTools: null,\n supportsReasoning: null,\n order: 50\n }\n ];\n\n const BY_ID = new Map();\n const ALIASES = new Map();\n for (const profile of PROFILES) {\n BY_ID.set(profile.id, profile);\n ALIASES.set(profile.id, profile.id);\n for (const a of (profile.aliases || [])) ALIASES.set(String(a).toLowerCase(), profile.id);\n }\n\n function canonicalKey(value) {\n return String(value || '').trim().toLowerCase();\n }\n function getProviderProfile(value) {\n const key = canonicalKey(value);\n if (!key) return null;\n const id = ALIASES.get(key);\n return id ? BY_ID.get(id) || null : null;\n }\n function normalizeProviderId(value, fallback) {\n const profile = getProviderProfile(value);\n if (profile) return profile.id;\n return fallback == null ? '' : String(fallback);\n }\n function providerUsesCodex(value) {\n return normalizeProviderId(value, '') === 'codex';\n }\n function defaultReasoningEffortForProvider(value) {\n const profile = getProviderProfile(value) || BY_ID.get(DEFAULT_PROVIDER_ID);\n return (profile && profile.defaultReasoningEffort) || 'medium';\n }\n function providerRequiresKey(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.keyRequired);\n }\n function providerRequiresBaseUrl(value) {\n const profile = getProviderProfile(value);\n return !!(profile && profile.requiresBaseUrl);\n }\n function toPublicProfile(profile) {\n return {\n id: profile.id,\n aliases: (profile.aliases || []).slice(),\n name: profile.name,\n label: profile.label,\n endpoint: profile.endpoint,\n blurb: profile.blurb,\n live: profile.live !== false,\n apiMode: profile.apiMode,\n authType: profile.authType,\n keyRequired: !!profile.keyRequired,\n keyEnv: (profile.keyEnv || []).slice(),\n modelsRequireAuth: profile.modelsRequireAuth !== false,\n baseUrl: profile.baseUrl || '',\n baseUrlEnv: (profile.baseUrlEnv || []).slice(),\n requiresBaseUrl: !!profile.requiresBaseUrl,\n defaultReasoningEffort: profile.defaultReasoningEffort || 'medium',\n unmetered: !!profile.unmetered,\n supportsTools: profile.supportsTools,\n supportsReasoning: profile.supportsReasoning,\n credentialPool: !!profile.credentialPool\n };\n }\n function listProviderProfiles(opts) {\n opts = opts || {};\n return PROFILES\n .filter(p => opts.includeInactive || p.live !== false)\n .slice()\n .sort((a, b) => (a.order || 1000) - (b.order || 1000) || a.id.localeCompare(b.id))\n .map(p => opts.public === false ? Object.assign({}, p) : toPublicProfile(p));\n }\n function providerIds(opts) {\n return listProviderProfiles(Object.assign({}, opts, { public: false })).map(p => p.id);\n }\n\n return {\n DEFAULT_PROVIDER_ID,\n getProviderProfile,\n listProviderProfiles,\n normalizeProviderId,\n providerUsesCodex,\n defaultReasoningEffortForProvider,\n providerRequiresKey,\n providerRequiresBaseUrl,\n providerIds,\n _profiles: PROFILES\n };\n});\n"}}}} |