Files
stack/packages/cli/live/request-lifecycle.mjs
jason.woltje a8d9eecf65 organization: move packages/business to packages/organization (rename r2 stage B1)
git mv packages/business -> packages/organization (@mosaic/organization). The
old package stays as one-line re-export shims of each moved module, with
src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs
forwarding to the moved executable. BusinessError is OrganizationConfigError
(one constructor, name kept BusinessError); OrganizationError stays distinct.
The bus adapter gains organization-context.mjs (busOrganization), with
business.mjs (busBusiness) as the same function.

Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh
COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure
walker (organization-imports.mjs, both roots required) move to the new path.
v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage
A contract tests stay where they were and pass as before.
2026-11-09 11:47:48 -06:00

112 lines
8.8 KiB
JavaScript

// Opt-in live acceptance observer/submission. Never starts a host, selects a mode,
// approves a PRD, creates a service resource, or substitutes a scripted role.
import { readFileSync, writeFileSync, mkdirSync, lstatSync } from 'node:fs';
import { dirname, isAbsolute, join, resolve, relative } from 'node:path';
import { pathToFileURL } from 'node:url';
import { createHash, randomUUID } from 'node:crypto';
import { loadSystem, bootConfig, rolesDir } from '../src/config.mjs';
import { loadBusiness } from '../../organization/src/index.mjs';
import { readHostState } from '../src/host.mjs';
import { humanTransport, refuseInsideAgent } from '../src/transport.mjs';
import { staffFor } from '../../bus/src/request-host.mjs';
const fail = message => { throw new Error(message); };
export function parseOptions(argv) {
const result = {};
const flags = { '--authorize-isolated-run': 'authorized', '--submit': 'submit' };
const values = { '--instance':'instance', '--evidence':'evidence', '--config':'config', '--business':'business', '--owner':'owner',
'--request-file':'requestFile', '--request':'request', '--thread':'thread', '--artifact':'artifact', '--timeout-ms':'timeout' };
for (let n = 0; n < argv.length; n++) {
const key = flags[argv[n]] ?? values[argv[n]];
if (!key || key in result) fail('unknown or duplicate option');
result[key] = argv[n] in flags ? true : argv[++n];
if (result[key] === undefined) fail('missing option value');
}
if (!/^[a-z][a-z0-9]{1,15}$/.test(result.instance ?? '') || ['dev','prod','production'].includes(result.instance)) fail('explicit named isolated --instance required');
if (!result.authorized) fail('explicit owner authorization --authorize-isolated-run required');
for (const key of ['config','evidence','requestFile']) if (!isAbsolute(result[key] ?? '')) fail('explicit absolute --' + key + ' required');
if (!result.business || !result.owner) fail('explicit business and expected owner required');
if (result.submit && (!result.thread || result.request)) fail('--submit requires an existing owner --thread and no --request');
result.timeout = result.timeout === undefined ? 300000 : Number(result.timeout);
if (!Number.isSafeInteger(result.timeout) || result.timeout < 1000 || result.timeout > 3600000) fail('timeout must be 1000..3600000 ms');
return result;
}
export function checkResources({ options, system, business, marker, host, tracker }) {
if (system.environment !== 'development') fail('isolated development instance required');
if (marker?.instanceVersion !== 1 || marker.name !== options.instance) fail('instance marker mismatch');
if (business.id !== options.business || business.human !== options.owner) fail('owner/business mismatch');
if (!host?.live || (host.business && host.business !== business.id)) fail('configured live host required; runner does not start one');
if (!Number.isSafeInteger(tracker?.project) || tracker.project < 1) fail('tracker-install: declared installed tracker root required');
const pms = Object.keys(business.roles).filter(r => business.roles[r].definition === 'pm');
if (pms.length !== 1 || !staffFor(business, pms[0])) fail('configured PM, independent technical arbiter, chief, coder and reviewer staff required');
if (!Object.values(business.projects).some(p => p.kind !== 'library')) fail('declared factory source project required');
}
function json(file) {
const stat = lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) fail('bounded regular input required');
return JSON.parse(readFileSync(file, 'utf8'));
}
export async function main(argv, env = process.env) {
const options = parseOptions(argv);
refuseInsideAgent(env); // --owner is an expectation, never authentication or a capability.
const selected = { ...env, MOSAIC_CONFIG: options.config };
const system = loadSystem({ env: selected }), config = bootConfig({ system, businessId: options.business, env: selected });
const business = loadBusiness(options.business, { dir: dirname(options.config), rolesDir: rolesDir(selected) });
const host = readHostState(system.dataRoot);
checkResources({ options, system, business, marker: json(join(system.dataRoot, 'instance.json')), host, tracker: config.trackers?.[options.business] });
for (const [name, role] of Object.entries(business.roles)) {
if (!role.vars?.harness || !role.vars?.model || !role.vars?.thinking) fail('model route missing for ' + name);
}
const source = json(options.requestFile);
if (typeof source.text !== 'string' || !source.text || source.provenance?.raw_runtime_event_exported !== false ||
createHash('sha256').update(source.text).digest('hex') !== source.source_sha256) fail('verified owner-supplied original request quotation required');
const call = humanTransport({ socket: host.socket ?? join(system.dataRoot, 'bus/broker.sock'), business: business.id, env: selected });
await call('instances'); // Existing owner process proof succeeds before any submission.
const credentials = await call('credentials');
if (Array.isArray(credentials) && credentials.some(c => c.service === 'vikunja' && !['valid','ready'].includes(c.state))) fail('tracker credential setup wait; inspect Console credentials');
const rel = relative(system.dataRoot, resolve(options.evidence));
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) fail('evidence output must be outside the instance data root');
mkdirSync(options.evidence, { recursive: true, mode: 0o700 });
if (!lstatSync(options.evidence).isDirectory() || lstatSync(options.evidence).isSymbolicLink()) fail('plain evidence output directory required');
const record = (kind, value) => {
const file = join(options.evidence, `${kind}-${randomUUID()}.json`);
writeFileSync(file, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 }); return file;
};
record('preflight', { instance: options.instance, business: business.id, owner: business.human, source,
qualification: 'Owner-supplied incident quote; new runtime submission IDs are recorded separately. Fixture success is not live model acceptance.' });
let request = options.request;
if (options.submit) {
const threads = await call('thread.list');
if (!threads.some(t => t.id === options.thread)) fail('the explicitly selected thread is not owned by this user');
const sent = await call('thread.send', { thread: options.thread, body: source.text });
request = sent.request;
record('submission', { request, message: sent.id, original: { source_event: source.source_event, source_message: source.source_message, source_sha256: source.source_sha256 } });
process.stdout.write(`Request ${request}. Select and confirm the exact plan in the signed-in Console.\n`);
}
if (!request) fail('--submit or an existing --request is required');
const end = Date.now() + options.timeout;
let state, revision = null;
while (Date.now() < end) {
try { state = await call('request.get', { request }); }
catch (e) { if (e.code !== 'request-not-found') throw e; }
if (state && state.revision !== revision) { record('request', state); revision = state.revision; }
if (state?.phase === 'cancelled') fail('request cancelled; receipts retained');
if (state?.phase === 'complete' && state.delivery_evidence) break;
await new Promise(resolve => setTimeout(resolve, 500));
}
if (!state?.delivery_evidence || state.phase !== 'complete') fail('live acceptance incomplete: exact current wait is retained in request evidence');
const delivery = state.delivery_evidence;
const trails = [];
for (const subject of [request, state.source_message, ...state.tasks, delivery.review_request, delivery.verdict]) trails.push({ subject, rows: await call('trail', { subject }) });
record('delivery', { state, trails, verified_local_delivery_only: true });
if (!options.artifact) fail(`download required from signed-in Console /api/requests/${request}/artifact/${delivery.id}; rerun observation with --artifact PATH`);
const bytes = readFileSync(options.artifact);
if (bytes.length !== delivery.artifact.bytes || createHash('sha256').update(bytes).digest('hex') !== delivery.artifact.sha256) fail('download differs from reviewed artifact');
writeFileSync(join(options.evidence, 'reviewed-' + delivery.id + '.html'), bytes, { flag: 'wx', mode: 0o600 });
record('download', { request, delivery: delivery.id, artifact: delivery.artifact, qualification: 'Hash-verified live delivery; lead must run offline behavior checks and inspect real independent role/test receipts before acceptance.' });
process.stdout.write('Live delivery and downloaded bytes retained. Offline browser verification and lead acceptance remain explicit.\n');
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
main(process.argv.slice(2)).catch(e => { process.stderr.write(e.message + '\n'); process.exitCode = 2; });
}