#!/usr/bin/env node // Real Settings UI + persisted config + live CSP. Only loopback test services. import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { once } from 'node:events'; import { mkdtemp, mkdir, copyFile, writeFile, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { spawn } from 'node:child_process'; import { chromium } from 'playwright'; const root = resolve(import.meta.dirname, '../..'); const temp = await mkdtemp(join(tmpdir(), 'maestro-egress-settings-')); const delay = ms => new Promise(resolveDelay => setTimeout(resolveDelay, ms)); let worker, browser; let output = ''; const servers = []; async function listen(server) { server.listen(0, '127.0.0.1'); await once(server, 'listening'); servers.push(server); return `http://127.0.0.1:${server.address().port}`; } async function stop() { if (!worker || worker.exitCode !== null || worker.signalCode !== null) return; const done = once(worker, 'exit'); worker.kill('SIGTERM'); const timeout = setTimeout(() => worker?.kill('SIGKILL'), 5000); try { await done; } finally { clearTimeout(timeout); worker = undefined; } } try { await mkdir(join(temp, 'dist/db'), { recursive: true }); await copyFile(join(root, 'dist/db/schema.sql'), join(temp, 'dist/db/schema.sql')); let hits = 0; const canary = await listen(createServer((_req, res) => { hits++; res.setHeader('Access-Control-Allow-Origin', '*'); res.end('ok'); })); const llm = await listen(createServer((_req, res) => { res.setHeader('Content-Type', 'application/json'); res.end('{"data":[{"id":"test-model"}],"models":[]}'); })); const reserve = createServer(); reserve.listen(0, '127.0.0.1'); await once(reserve, 'listening'); const port = reserve.address().port; await new Promise(resolveClose => reserve.close(resolveClose)); const base = `http://127.0.0.1:${port}`; const config = join(temp, 'config.yaml'); await writeFile(config, 'safety:\n bash_allow_network: true\n'); const env = { PATH: process.env.PATH, HOME: temp, NODE_ENV: 'test', AAO_MODE: 'worker', AAO_CONFIG: config, PORT: String(port), DB_PATH: join(temp, 'test.db'), WORKTREE_DIR: join(temp, 'workspaces'), UI_DIST_PATH: join(root, 'ui/dist'), OLLAMA_BASE_URL: `${llm}/v1`, OLLAMA_MODEL: 'test-model', LOG_LEVEL: 'info' }; async function start(extra = {}) { output = ''; worker = spawn(process.execPath, [join(root, 'dist/main.js')], { cwd: temp, env: { ...env, ...extra }, stdio: ['ignore', 'pipe', 'pipe'] }); const capture = value => { output += value.toString(); if (output.length > 2_000_000) worker?.kill('SIGTERM'); }; worker.stdout.on('data', capture); worker.stderr.on('data', capture); worker.on('error', error => { output += error.message; }); for (let i = 0; i < 300; i++) { if (worker.exitCode !== null) throw new Error(`worker startup failed: ${output.slice(-5000)}`); try { if ((await fetch(`${base}/ui/`, { signal: AbortSignal.timeout(1000) })).ok) return; } catch {} await delay(100); } throw new Error('worker did not start'); } await start(); browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] }); const context = await browser.newContext({ locale: 'ja-JP', serviceWorkers: 'block' }); context.setDefaultTimeout(15000); const external = []; await context.route('**/*', route => { const origin = new URL(route.request().url()).origin; if ([base, canary].includes(origin)) return route.continue(); external.push(origin); return route.abort(); }); await context.routeWebSocket('**/*', route => { if (new URL(route.url()).origin.replace(/^ws/, 'http') === base) route.connectToServer(); else { external.push(new URL(route.url()).origin); route.close(); } }); const page = await context.newPage(); await page.goto(`${base}/ui/?page=settings§ion=egress-policy`); const mode = page.getByRole('combobox', { name: 'ブラウザの通信', exact: true }); const observe = page.getByRole('checkbox', { name: 'サーバー通信の接続先を記録', exact: true }); const connectOrigins = page.getByRole('textbox', { name: '接続の許可先', exact: true }); await mode.waitFor(); assert.equal(await mode.inputValue(), 'off'); const pid = worker.pid; async function save() { const response = page.waitForResponse(r => r.url() === `${base}/api/config` && r.request().method() === 'PUT'); await page.getByRole('button', { name: '保存して反映', exact: true }).click(); assert.equal((await response).status(), 200, 'settings save succeeds'); assert.equal(worker.pid, pid, 'settings apply without restarting worker'); } async function attempt() { return page.evaluate(url => fetch(url).then(r => r.ok, () => false), canary); } await mode.selectOption('report-only'); await observe.check(); await save(); assert((await fetch(`${base}/ui/`)).headers.get('content-security-policy-report-only')?.includes('connect-src')); await page.reload(); assert.equal(await attempt(), true, 'report-only does not block'); assert(output.includes('Node destination observation enabled'), 'Node monitoring enabled live'); await mode.selectOption('enforce'); await save(); await page.reload(); const beforeBlocked = hits; assert.equal(await attempt(), false, 'enforce blocks unlisted destination'); assert.equal(hits, beforeBlocked, 'blocked request never reaches destination'); await connectOrigins.fill(canary); await save(); await page.reload(); assert.equal(await attempt(), true, 'adding an allowed origin works'); const onDisk = await readFile(config, 'utf8'); assert(onDisk.includes('observe_node: true') && onDisk.includes('mode: enforce'), 'settings persisted to YAML'); await stop(); await start(); await page.reload(); assert.equal(await mode.inputValue(), 'enforce'); assert.equal(await connectOrigins.inputValue(), canary); assert.equal(await observe.isChecked(), true); assert.equal(await attempt(), true, 'saved policy survives restart'); const locked = join(temp, 'external.json'); await writeFile(locked, JSON.stringify({ version: 1, observeNode: false, browser: { mode: 'report-only', connectOrigins: [], imageOrigins: [], frameOrigins: [] } })); await stop(); await start({ AAO_EGRESS_POLICY: locked }); await page.reload(); assert.equal(await mode.inputValue(), 'report-only'); assert.equal(await mode.isDisabled(), true); assert.equal(await observe.isDisabled(), true); assert.equal(await connectOrigins.isDisabled(), true); const beforeWrite = await readFile(config, 'utf8'); const rejected = await fetch(`${base}/api/config`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ egress: { browser: { mode: 'off' } } }) }); assert.equal(rejected.status, 400, 'generic API cannot bypass external lock'); assert.equal(await readFile(config, 'utf8'), beforeWrite); assert.deepEqual(external, []); await context.close(); console.log('Settings UI: live report/enforce/allowlist, Node monitoring, persistence, external lock passed'); } finally { await stop(); await browser?.close(); for (const server of servers) { server.closeAllConnections(); server.close(); } await rm(temp, { recursive: true, force: true }); }