One Proxy setup panel on the Model proxy settings page takes an owner from
nothing to a running proxy: status with one next-step button per state, the
pinned fetch with hash checks, progress and a receipt, init with a
bus-generated key (a pasted key only over loopback or HTTPS, with a
confirmation), unit render/install/start/stop/restart with health, a fence
clear that the bus re-proves against a confirmed restart, and a stale init
lock clear that names its blockers. Own proxy per organization shows as not
built.
New Console routes under /api/proxy/admin/ use the existing owner proxy
checks; agents, testers and grant holders get human-required. New bus verbs
proxy.init {generate:true} and proxy.fence.clear. Receipts go to
<dataRoot>/state/proxy-admin.jsonl with labels and key ids only. Every new
refusal code has a refusal-table entry.
CLI: mosaic proxy status, unit render|status|install|start|stop|restart,
init --generate-key, fence clear --restart, init-lock status|clear; proxy
fetch keeps its stdout and adds progress and the receipt.
Page fix found in the end-to-end run: a stopped proxy hides the account
panels until the service answers. (The catalogue switch label fix found in
the same run landed upstream as D3 P0-4.)
127 lines
9.2 KiB
JavaScript
127 lines
9.2 KiB
JavaScript
// #1873 CLI parity for the Console's Proxy page: status, unit, init-lock, init --generate-key and
|
|
// fence clear --restart. Every verb runs only from a human shell; nothing prints a key or a path.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { main } from '../src/cli.mjs';
|
|
import { proxyCommand } from '../src/model-proxy.mjs';
|
|
import { io } from './helpers.mjs';
|
|
|
|
const code = c => e => e.code === c;
|
|
const fakeSystem = () => ({ dataRoot: join(mkdtempSync(join(tmpdir(), 'ps-cli-')), 'data') });
|
|
const STATUS = {
|
|
release: { fetched: true, version: '8.0.42', panelVersion: '1.26.7', verified: true },
|
|
init: { initialized: true, partial: false, settings: { port: 18432 }, files: [] },
|
|
unit: { unit: 'mosaic-proxy.service', installed: true, active: 'active', pid: 77, port: 18432, listening: true },
|
|
fence: { set: false }, initLock: { present: false, clearable: false, blockers: [] },
|
|
ownProxy: { available: false, state: 'not built' }, next: ['accounts'],
|
|
};
|
|
function fakeAdmin({ restart = { confirmed: true } } = {}) {
|
|
const calls = [];
|
|
return {
|
|
calls,
|
|
status: async () => { calls.push(['status']); return STATUS; },
|
|
render: async () => { calls.push(['render']); return { text: '[Unit]\nDescription=fixture\n' }; },
|
|
unitAction: async op => { calls.push(['unit', op]); return op === 'restart' ? { receipt: { op: 'unit.restart' }, evidence: restart } : { op, ok: true }; },
|
|
initLockClear: async () => { calls.push(['init-lock.clear']); return { cleared: true, receipt: { op: 'init-lock.clear' } }; },
|
|
fenceClear: async call => { calls.push(['fence.clear']); return call('proxy.fence.clear', { evidence: restart }); },
|
|
};
|
|
}
|
|
const human = () => io();
|
|
|
|
test('every new setup verb refuses inside an agent shell and with --tester, before the controller runs', async () => {
|
|
const admin = fakeAdmin();
|
|
for (const args of [['proxy', 'status'], ['proxy', 'status', '--json'], ['proxy', 'unit', 'render'], ['proxy', 'unit', 'start'], ['proxy', 'unit', 'restart'],
|
|
['proxy', 'init-lock', 'status'], ['proxy', 'init-lock', 'clear'], ['proxy', 'fence', 'clear', '--restart', '--business', 'demo'], ['proxy', 'init', '--generate-key', '--business', 'demo']]) {
|
|
await assert.rejects(main(args, io({ CODEX_THREAD_ID: 'fixture' }), { system: fakeSystem(), proxyAdmin: admin, transport: () => () => { throw Error('must not call'); } }), code('human-required'), args.join(' '));
|
|
await assert.rejects(main([...args, '--tester'], human(), { system: fakeSystem(), proxyAdmin: admin }), code('human-required'), `${args.join(' ')} --tester`);
|
|
}
|
|
assert.deepEqual(admin.calls, []);
|
|
});
|
|
|
|
test('proxy status prints the same facts the page shows, and --json the status object', async () => {
|
|
const admin = fakeAdmin();
|
|
const x = human();
|
|
await main(['proxy', 'status'], x, { system: fakeSystem(), proxyAdmin: admin });
|
|
assert.match(x.out.text, /^release: 8\.0\.42 \(panel 1\.26\.7\) hashes verified$/m);
|
|
assert.match(x.out.text, /^unit: mosaic-proxy\.service installed, active pid 77, port 18432 answering$/m);
|
|
assert.match(x.out.text, /^own proxy: not built$/m);
|
|
assert.match(x.out.text, /^next: accounts$/m);
|
|
const y = human();
|
|
await main(['proxy', 'status', '--json'], y, { system: fakeSystem(), proxyAdmin: admin });
|
|
assert.deepEqual(JSON.parse(y.out.text), STATUS);
|
|
await assert.rejects(main(['proxy', 'status', '--verbose'], human(), { system: fakeSystem(), proxyAdmin: admin }), code('proxy-usage'));
|
|
});
|
|
|
|
test('proxy unit and init-lock verbs map one to one onto the controller; bad usage refuses', async () => {
|
|
const admin = fakeAdmin();
|
|
const x = human();
|
|
await main(['proxy', 'unit', 'render'], x, { system: fakeSystem(), proxyAdmin: admin });
|
|
assert.equal(x.out.text, '[Unit]\nDescription=fixture\n');
|
|
for (const op of ['install', 'start', 'stop']) await main(['proxy', 'unit', op], human(), { system: fakeSystem(), proxyAdmin: admin });
|
|
await main(['proxy', 'unit', 'status'], human(), { system: fakeSystem(), proxyAdmin: admin });
|
|
await main(['proxy', 'init-lock', 'status'], human(), { system: fakeSystem(), proxyAdmin: admin });
|
|
const y = human();
|
|
await main(['proxy', 'init-lock', 'clear'], y, { system: fakeSystem(), proxyAdmin: admin });
|
|
assert.equal(JSON.parse(y.out.text).cleared, true);
|
|
assert.deepEqual(admin.calls, [['render'], ['unit', 'install'], ['unit', 'start'], ['unit', 'stop'], ['status'], ['status'], ['init-lock.clear']]);
|
|
for (const args of [['proxy', 'unit'], ['proxy', 'unit', 'reload'], ['proxy', 'unit', 'start', 'now'], ['proxy', 'init-lock'], ['proxy', 'init-lock', 'clear', '--force']])
|
|
await assert.rejects(main(args, human(), { system: fakeSystem(), proxyAdmin: admin }), code('proxy-usage'), args.join(' '));
|
|
});
|
|
|
|
test('a controller refusal reaches the shell as its proxy code only, with no path or detail', async () => {
|
|
const admin = fakeAdmin();
|
|
admin.unitAction = async () => { throw Object.assign(Error('/home/owner/.config/mosaic-dev/proxy.json pa-cli-sentinel'), { code: 'proxy-unit-missing' }); };
|
|
admin.status = async () => { throw Error('pa-cli-sentinel'); };
|
|
await assert.rejects(main(['proxy', 'unit', 'start'], human(), { system: fakeSystem(), proxyAdmin: admin }), e => e.code === 'proxy-unit-missing' && !e.message.includes('pa-cli-sentinel') && !e.message.includes('/home/'));
|
|
await assert.rejects(main(['proxy', 'status'], human(), { system: fakeSystem(), proxyAdmin: admin }), e => e.code === 'proxy-unit-failed' && !e.message.includes('pa-cli-sentinel'));
|
|
});
|
|
|
|
test('proxy init --generate-key needs no terminal, never prompts, and sends generate to the bus', async () => {
|
|
const x = human();
|
|
assert.notEqual(x.stdin.isTTY, true);
|
|
const calls = [];
|
|
await main(['proxy', 'init', '--generate-key', '--business', 'demo', '--port', '18440', '--rotate-grace-minutes', '5'], x, {
|
|
system: fakeSystem(), promptProxyKey: () => { throw Error('must not prompt'); },
|
|
transport: b => async (verb, args) => { calls.push({ b, verb, args }); return { port: 18440, rotateGraceMinutes: 5, binary: '/fixture' }; } });
|
|
assert.deepEqual(calls, [{ b: 'demo', verb: 'proxy.init', args: { generate: true, port: 18440, rotateGraceMinutes: 5 } }]);
|
|
assert.deepEqual(JSON.parse(x.out.text), { port: 18440, rotateGraceMinutes: 5, firstOutputSeconds: null, generated: true });
|
|
await assert.rejects(main(['proxy', 'init', '--generate-key', '--generate-key', '--business', 'demo'], human(), { system: fakeSystem() }), code('proxy-usage'));
|
|
await assert.rejects(main(['proxy', 'init', '--generate-key', '--port', '8317', '--business', 'demo'], human(), { system: fakeSystem() }), code('proxy-config-invalid'));
|
|
});
|
|
|
|
test('proxy fence clear --restart restarts first and clears only on a confirmed restart', async () => {
|
|
const admin = fakeAdmin();
|
|
const calls = [];
|
|
const x = human();
|
|
await main(['proxy', 'fence', 'clear', '--restart', '--business', 'demo'], x, { system: fakeSystem(), proxyAdmin: admin,
|
|
transport: b => async (verb, args) => { calls.push({ b, verb, args }); return { cleared: true, evidence: { after: { pid: 2 } } }; } });
|
|
assert.deepEqual(admin.calls, [['unit', 'restart'], ['fence.clear']]);
|
|
assert.deepEqual(calls, [{ b: 'demo', verb: 'proxy.fence.clear', args: { evidence: { confirmed: true } } }]);
|
|
assert.deepEqual(JSON.parse(x.out.text), { restart: { op: 'unit.restart' }, cleared: true, evidence: { after: { pid: 2 } } });
|
|
const unconfirmed = fakeAdmin({ restart: { confirmed: false } });
|
|
await assert.rejects(main(['proxy', 'fence', 'clear', '--restart', '--business', 'demo'], human(), { system: fakeSystem(), proxyAdmin: unconfirmed,
|
|
transport: () => () => { throw Error('must not call'); } }), code('proxy-restart-unconfirmed'));
|
|
assert.deepEqual(unconfirmed.calls, [['unit', 'restart']]);
|
|
await assert.rejects(main(['proxy', 'fence', 'clear', '--restart', '--restart'], human(), { system: fakeSystem(), proxyAdmin: admin }), code('proxy-usage'));
|
|
});
|
|
|
|
test('proxy fetch shows the page\'s progress steps and writes the same fetch receipt; stdout keeps its line', async () => {
|
|
const system = fakeSystem();
|
|
const x = human();
|
|
await proxyCommand(['fetch'], x, { system, fetch: async ({ progress }) => { for (const step of ['download-archive', 'verify-archive', 'install']) progress(step); return { binary: 'fixture-binary', panel: 'fixture-panel' }; } });
|
|
assert.deepEqual(JSON.parse(x.out.text), { binary: 'fixture-binary', panel: 'fixture-panel' });
|
|
assert.match(x.err.text, /^fetch: download-archive\nfetch: verify-archive\nfetch: install\nreceipt: /);
|
|
const receipts = readFileSync(join(system.dataRoot, 'state', 'proxy-admin.jsonl'), 'utf8').trim().split('\n').map(JSON.parse);
|
|
assert.equal(receipts.length, 1);
|
|
assert.equal(receipts[0].op, 'fetch');
|
|
assert.equal(receipts[0].verified, true);
|
|
assert.match(receipts[0].archiveSha256, /^[0-9a-f]{64}$/);
|
|
// A failed hash check writes no receipt and keeps its code.
|
|
const failed = fakeSystem();
|
|
await assert.rejects(proxyCommand(['fetch'], human(), { system: failed, fetch: async () => { throw Object.assign(Error('mismatch'), { code: 'proxy-fetch-hash' }); } }), code('proxy-fetch-hash'));
|
|
assert.throws(() => readFileSync(join(failed.dataRoot, 'state', 'proxy-admin.jsonl')), { code: 'ENOENT' });
|
|
});
|