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.)
50 lines
2.2 KiB
JavaScript
50 lines
2.2 KiB
JavaScript
// #1873 proxy setup: host facts that `mosaic proxy` and the Console page share. Never reads a
|
|
// key, proxy setting or bus record; labels only, never an absolute path.
|
|
import net from 'node:net';
|
|
import { hostStatus } from './host.mjs';
|
|
import { socketPath, orgPaths } from './config.mjs';
|
|
import { devOwnerInstance } from './dev-local-owner.mjs';
|
|
import { listOrgs } from '../../organization/src/org-registry.mjs';
|
|
|
|
// A socket that accepts a connection, or fails other than "absent"/"refused", counts as running.
|
|
function socketLive(file, timeout = 1000) {
|
|
return new Promise(resolve => {
|
|
const socket = net.connect(file);
|
|
const done = value => { socket.destroy(); resolve(value); };
|
|
socket.setTimeout(timeout, () => done(true));
|
|
socket.once('connect', () => done(true));
|
|
socket.once('error', error => done(!['ENOENT', 'ECONNREFUSED'].includes(error.code)));
|
|
});
|
|
}
|
|
|
|
async function rootRunning(root, socket) {
|
|
try {
|
|
const status = hostStatus(root);
|
|
if (status.host?.live || status.writerLock?.live) return true;
|
|
} catch { return true; }
|
|
return socketLive(socket);
|
|
}
|
|
|
|
// The init-lock README condition "the bus is stopped", proved for the instance's own bus and
|
|
// every registered organization's bus. Anything that cannot be examined counts as running.
|
|
// `listOrgsFn` and `running` are for tests.
|
|
export async function busesStopped({ dataRoot, configDir, listOrgsFn = listOrgs, running = rootRunning }) {
|
|
const out = [];
|
|
if (await running(dataRoot, socketPath(dataRoot))) out.push('the instance bus');
|
|
let orgs;
|
|
try { orgs = await listOrgsFn({ configDir }); }
|
|
catch { return { stopped: false, running: [...out, 'organization registry unreadable'] }; }
|
|
for (const org of orgs) {
|
|
const label = `organization ${typeof org?.id === 'string' ? org.id : '?'}`;
|
|
let paths;
|
|
try { paths = orgPaths(org.root); } catch { out.push(label); continue; }
|
|
if (await running(org.root, paths.socket)) out.push(label);
|
|
}
|
|
return { stopped: out.length === 0, running: out };
|
|
}
|
|
|
|
// The named instance MOSAIC_CONFIG selects, or null for the default (dev) instance.
|
|
export function instanceFromConfig(env = process.env) {
|
|
try { return devOwnerInstance(env); } catch { return null; }
|
|
}
|