Files
stack/packages/webui/src/action-trail.mjs
jason.woltje 1b6cac2bdf feat(webui): restart the Console, the org bus and agents from the WebUI (F3)
Self-service operation from the WebUI, for a dev instance first.

- Settings > Console > Restart Console. A foreground `mosaic console
  start` now supervises the webui: the child exits 75 after answering a
  restart and the supervisor relaunches it with the same argv. Org buses
  keep running. /api/config carries a per-process boot id, so the open
  page polls until a new process answers and then reloads. A detached
  Console has no supervisor and answers 409.
- Restart bus on every "Restart needed" receipt and on the bus-down
  banner: POST /api/orgs/<id>/restart/bus restarts a running bus or
  starts a stopped one, then shows a done receipt with the time.
- Settings > Routing: after a save, the page lists how the change
  reaches each agent (next start, session end, thread idle) and offers
  "Restart <agent> now" for a running one. A session is terminated with
  the reason "route change" and launched again; a thread is terminated.
- Organization settings: the signed-in owner's settings, routing,
  agent, plug-in and Model proxy writes now reach the selected org's
  bus instead of answering 403. Org mode exists only under
  --dev-local-owner. Proxy setup stays a terminal step; a dev-only note
  names the two commands.

Every control needs the signed-in owner session, the page's exact
Origin and Sec-Fetch-Site: same-origin. The tester listener, granted
users, API tokens, agents and bus roles are refused before any work.
Each use is written to <dataRoot>/console/actions.jsonl (0600, no
symlinks) with who, when, what and the outcome. The Console and bus
restart trails fail closed. No change to credential handling, the
broker, the bus tenant boundary or v2 schemas.
2026-11-09 13:37:28 -06:00

25 lines
1.5 KiB
JavaScript

// F3: the Console's trail of human operations (Console restart, org bus restart, route and
// instance changes made from the page). One JSON line per use: who, when, what, outcome.
// The file is <dataRoot>/console/actions.jsonl, owner-only. A write that cannot be made safely
// throws, and the caller refuses the operation (fail closed).
import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, writeSync } from 'node:fs';
import { join } from 'node:path';
// The person, never a token: provider, issuer, subject and display name of the session.
export const actor = session => ({ provider: session.provider, iss: session.iss, sub: session.sub, ...(typeof session.name === 'string' ? { name: session.name } : {}) });
export function actionTrail(dir) {
const file = join(dir, 'actions.jsonl');
return entry => {
mkdirSync(dir, { recursive: true, mode: 0o700 });
const st = lstatSync(dir);
if (!st.isDirectory() || st.isSymbolicLink() || st.uid !== process.getuid() || (st.mode & 0o077) !== 0) throw new Error('unsafe trail directory');
const fd = openSync(file, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
try {
const f = fstatSync(fd);
if (!f.isFile() || f.uid !== process.getuid() || (f.mode & 0o777) !== 0o600 || f.nlink !== 1) throw new Error('unsafe trail file');
writeSync(fd, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`);
} finally { closeSync(fd); }
};
}