Files
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

131 lines
8.0 KiB
JavaScript

// C2-14 fixture: the real host/launcher with C2-06's scripted citation adapter. No model binary runs.
import { chmodSync, mkdirSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { BusError, writeGrantEvent } from '../../packages/bus/src/broker.mjs';
import { Store } from '../../packages/bus/src/store.mjs';
import { startBroker } from '../../packages/bus/src/runtime.mjs';
import { grantDecision } from '../../packages/bus/tests/host-fixtures.mjs';
import { launchDoc } from '../../packages/organization/tests/helpers.mjs';
import { fakeHarnesses, fixture } from '../../packages/cli/tests/helpers.mjs';
import { bootConfig, loadSystem } from '../../packages/cli/src/config.mjs';
import { createLauncher } from '../../packages/cli/src/launcher.mjs';
import { SESSION_CREDENTIALS } from '../../packages/seat/src/session.mjs';
import * as library from '../../packages/library/src/index.mjs';
import { FAKE_ZAI_KEY } from './fixture-credentials.mjs';
const ADAPTER = fileURLToPath(new URL('./citation-adapter.mjs', import.meta.url));
const REPLY_ADAPTER = fileURLToPath(new URL('../../packages/harness/tests/fixtures/citation-adapter.mjs', import.meta.url));
const ROTATE = '2099-01-01';
const AT = '2026-10-24T00:00:00.000Z';
const shell = value => "'" + String(value).replaceAll("'", "'\\''") + "'";
// A fixture grant is part of the brief. It uses the normal store shape; no executor/network mint or
// role-policy modification occurs. This is deliberately distinct from the row287 grant acceptance.
function seedGrant(dataRoot, storeRoot, token) {
const dir = join(storeRoot, 'acme', 'bean');
mkdirSync(dir, { recursive: true, mode: 0o700 });
writeFileSync(join(dir, 'gitea'), token + '\n', { mode: 0o600 });
const store = new Store(dataRoot);
try {
const decision = grantDecision(store, { business: 'acme', grant: {
method: 'gitea', agent: 'bean', target: { projects: ['books'] }, rotateBy: ROTATE,
} });
store.run('INSERT INTO gitea_users(business,agent,user,id,nonce,decision) VALUES(?,?,?,?,?,?)',
'acme', 'bean', 'acme-bean', 1, '2270227022702270', decision);
store.run('INSERT INTO gitea_grants(business,agent,user,token_id,token_name,projects,rotate_by,decision) VALUES(?,?,?,?,?,?,?,?)',
'acme', 'bean', 'acme-bean', 1, 'fixture-227', '["books"]', ROTATE, decision);
writeGrantEvent(store, AT, 'acme', { operation: 'grant.gitea', decision }, decision);
writeGrantEvent(store, AT, 'acme', { action: 'grant.applied', method: 'gitea', agent: 'bean',
grantDecision: decision, user: 'acme-bean', userId: 1, tokenId: 1, forks: [],
repositories: [{ project: 'books', repository: 'fixture', level: 'write' }] }, 'bean');
} finally { store.close(); }
}
export async function launchFixture(root, entry, { fetchToken, seatToken, humanToken, ca }) {
const configDir = join(root, 'config'), storeRoot = join(configDir, 'secrets', 'agents');
const slotDir = join(configDir, 'secrets', 'acme');
mkdirSync(slotDir, { recursive: true, mode: 0o700 });
for (const [slot, token] of [[library.SLOT, fetchToken], [library.HUMAN_SLOT, humanToken]])
writeFileSync(join(slotDir, slot), token + '\n', { mode: 0o600 });
const plan = join(root, 'citation-plan.json');
writeFileSync(plan, '[]\n', { mode: 0o600 });
const f = fixture(root, 'acme', doc => {
// The unused factory roles remain valid, but no tracker is configured or polled.
delete doc.tracker;
doc.projects = { books: entry };
doc.roles.chief.credentials = { gitea: { file: join(slotDir, library.SLOT), rotateBy: ROTATE } };
doc.roles.bean = { definition: 'companion',
credentials: { gitea: { store: true, rotateBy: ROTATE } },
identity: { name: 'Bean fixture', email: '[email protected]', gitea: { url: new URL(entry.remote).origin, user: 'acme-bean' } },
publish: ['books'], vars: { harness: 'pi', model: 'claude-sonnet-5-5', thinking: 'high' } };
doc.launch = { ...launchDoc('chief', ['bean'], { opus: 1, sonnet: 1 }), threadIdleSeconds: 3600 };
doc.plugins = { ...doc.plugins, channel: ['webui'] };
doc.replyLimits = { requireCitation: true, citationsPerReply: 20 };
return doc;
});
chmodSync(configDir, 0o700);
for (const key of SESSION_CREDENTIALS.flat()) delete f.env[key];
// Joined P3 keeps its real preflight; this fake route owns only its made-up ZAI key.
for (const key of ['OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'ZAI_API_KEY']) delete f.env[key];
f.env.ZAI_API_KEY = FAKE_ZAI_KEY;
seedGrant(f.dataRoot, storeRoot, seatToken);
const system = loadSystem({ env: f.env });
const { trackers, ...boot } = bootConfig({ system, businessId: 'acme', env: f.env });
if (trackers !== undefined) throw new Error('acceptance fixture must not configure tracker polling');
const command = (kind, url, opts) => {
const c = library.syncCommand(kind, url, opts);
return { argv: c.argv, env: { ...c.env, GIT_SSL_CAINFO: ca } };
};
const runtime = await startBroker({ ...boot, librarySync: { command, intervalMs: 3_600_000, configDir } });
const hooks = [], cleanups = [], caps = new Map(), records = [], logs = [];
let closing = null;
const close = () => (closing ??= (async () => {
try { for (const hook of hooks) await hook(0); }
finally {
try { await runtime.close(); }
finally {
for (const cleanup of cleanups.reverse()) await cleanup();
const directory = join(f.dataRoot, 'control', 'runtimes');
const remaining = existsSync(directory) ? readdirSync(directory) : [];
if (remaining.length) throw new Error('owned-runtime-cleanup-incomplete');
}
}
})());
try {
const ops = {
identity: m => runtime.identity(m.cap), authorizeLaunch: m => runtime.authorizeLaunch(m.cap, m.instance),
refuse: m => runtime.refuse(m.cap, m.code, m.subject), endLaunch: m => runtime.endLaunch(m.record),
credentialStatus: m => runtime.credentialStatus(m.business, m.role), threadsState: m => runtime.threadsState(m.business),
threadIdle: m => runtime.threadIdle(m.record), mayLaunch: m => runtime.mayLaunch(m),
landHeld: m => runtime.landHeld(m.record), landHolders: m => runtime.landHolders(m.business),
landRelease: m => runtime.landRelease(m.record), sessionClears: m => runtime.sessionClears(m.business),
lifecycleRequests: m => runtime.lifecycleRequests(m.business),
};
const wrap = fn => async value => {
try { return await fn(value); }
catch (e) { throw Object.assign(new Error('fixture host refused'), { code: e instanceof BusError || /^[a-z-]+$/.test(e?.code ?? '') ? e.code : 'fixture-host-refused' }); }
};
const host = { business: 'acme', path: runtime.path, dataRoot: f.dataRoot,
op: wrap(m => { if (!Object.hasOwn(ops, m.op)) throw new BusError('invalid-host-request'); return ops[m.op](m); }),
bindLaunch: wrap(async record => {
const bound = runtime.bindLaunch(record);
records.push(record); caps.set(record.thread, bound.cap);
return bound;
}),
beforeClose: fn => void hooks.push(fn),
};
const adapter = join(root, 'citation-adapter.sh');
writeFileSync(adapter, `cp ${shell(plan)} "$(dirname "$MOSAIC_TURN_MARKER")/fixture-steps.json"\nexec ${shell(process.execPath)} ${shell(ADAPTER)}\n`, { mode: 0o644 });
const launcher = createLauncher({ host, system, env: f.env,
makeLoader: await fakeHarnesses({ after: fn => cleanups.push(fn) }, { pi: adapter }, write =>
write('channel', 'webui', `export * from ${JSON.stringify(new URL('../../packages/plugins/channel/webui/index.mjs', import.meta.url).href)};`)),
readOnly: [ADAPTER, REPLY_ADAPTER, plan], harnessVersion: '0.85.1',
sessionDefaults: { pollInterval: 50 }, threadTickMs: 50, log: line => logs.push(line),
});
await launcher.listen();
return { ...f, runtime, launcher, logs, records, plan, cap: thread => caps.get(thread), close,
business: boot.businesses.acme, setPlan: steps => writeFileSync(plan, JSON.stringify(steps) + '\n') };
} catch (e) { await close(); throw e; }
}