Adds the harness plug-in kind (interface 1). packages/plugins/harness/pi and claude-code hold what core branched on: the adapter, thinking levels, preflight, version probe, sandbox mounts, builtins line, typed tool names, gate timeout, bundle files, adapter env, turn end and session-files path. Core reaches a module only through the loader. A session's route is the instance's harness, model and thinking vars, or the three fallback. vars; a missing one refuses no-route, and the system file's execution keys are never read for a session. The launcher checks the primary route, then the fallback: harness-off, harness-refused, thinking-unsupported, then preflight. A failed preflight falls back when a fallback is set, and any refusal of the fallback names the primary's failure too. Account checks run only when the route's module counts an account. The manifest records route, routeReason and harnessVersion, replacing piVersion and claudeVersion. pi gets --thinking, Claude Code --effort, both from MOSAIC_THINKING. The business file refuses a partial fallback. routes and routes.set are human-only bus verbs outside ORGANIZATION_KEYS; checkRoutes is shared by routes.set, instances.set and limits.set and checks only changed routes. The chief of staff changes an instance's model, and the assistant's whole vars (Q169), through limits.set, so its route write runs the same checks. A model outside launch.models refuses unknown-model, primary or fallback, and routes.set passes that code through. A new run binds only on a switched-on harness module name and records its transcript path, which the usage reader uses. Settings gains a Routing table with six selects per instance and loses the Role models fieldset; it posts changed rows to /api/routes. webui/src/bus.mjs is unchanged. Until Jason's `mosaic plugins set` adds "harness": ["claude-code"], a new launch refuses harness-off.
67 lines
4.1 KiB
JavaScript
67 lines
4.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { startBroker } from '../src/runtime.mjs';
|
|
import { Client } from '../src/client.mjs';
|
|
import { startTimeOf } from '../../seat/src/proc.mjs';
|
|
import { initProject } from '../../project/src/index.mjs';
|
|
import { fixture } from './spec-fixtures.mjs';
|
|
|
|
test('Broker keeps the sandbox client import closure: only the listed imports, no lazy project import', () => {
|
|
const source = readFileSync(new URL('../src/broker.mjs', import.meta.url), 'utf8');
|
|
assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), [
|
|
"import { randomUUID, randomBytes } from 'node:crypto';",
|
|
// Row 65 (#1558): bindLaunch checks a run's transcript path is absolute.
|
|
"import { isAbsolute } from 'node:path';",
|
|
// Row 65: the plug-in name pattern a harness takes. kinds.mjs is in RUNNER_FILES and imports nothing.
|
|
"import { NAME as HARNESS_NAME } from '../../plugins/src/kinds.mjs';",
|
|
"import { loadRequirements } from '../../tasks/src/requirements.mjs';",
|
|
// Row 137 (#1631): land-work's checks, in RUNNER_FILES beside the broker. They import nothing.
|
|
"import { MERGE, LandingRefusal, readLanding } from './landing.mjs';",
|
|
// Row 141 (#1635): the business ID_PATTERN for instance names. The vocabulary is already in
|
|
// RUNNER_FILES and imports nothing. Row 155 (#1649) reads THREAD_DEFINITION and END_PROTECTED
|
|
// from it too.
|
|
"import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED } from '../../business/src/vocabulary.mjs';",
|
|
]);
|
|
assert.doesNotMatch(source, /\bimport\s*\(/);
|
|
assert.doesNotMatch(readFileSync(new URL('../src/landing.mjs', import.meta.url), 'utf8'), /\bimport\b/);
|
|
assert.doesNotMatch(readFileSync(new URL('../../business/src/vocabulary.mjs', import.meta.url), 'utf8'), /\bimport\b/);
|
|
assert.doesNotMatch(readFileSync(new URL('../../plugins/src/kinds.mjs', import.meta.url), 'utf8'), /\bimport\b/);
|
|
});
|
|
|
|
test('without a trusted project adapter all three async paths refuse project-not-configured', async t => {
|
|
const f = await fixture(t, { adapter: false });
|
|
for (const [verb, args] of [
|
|
['spec.read', { project: 'stack' }],
|
|
['record.write', { project: 'stack', path: 'docs/a.md', record: '# A' }],
|
|
['decision.raise', { project: 'stack', spec_path: 'spec/north-star.md', spec_commit: f.commit }],
|
|
]) await assert.rejects(f.call(f.coder, verb, args), { code: 'project-not-configured' });
|
|
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, 0);
|
|
});
|
|
|
|
test('real startBroker injects the project adapter for socket reads and bound-author record writes', async t => {
|
|
const root = mkdtempSync(join(tmpdir(), 'project-runtime-'));
|
|
let runtime;
|
|
t.after(async () => { await runtime?.close(); rmSync(root, { recursive: true, force: true }); });
|
|
// Row 65: the bound run's harness is switched on.
|
|
const business = { id: 'demo', human: 'jason', plugins: { harness: ['pi'] }, projects: { stack: { root: join(root, 'code') } },
|
|
arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: {
|
|
definition: 'coder', authority: { withinRole: ['record.write'], crossRole: [] },
|
|
} } };
|
|
const initialized = await initProject({ business, project: 'stack', dataRoot: root });
|
|
runtime = await startBroker({ dataRoot: root, businesses: { demo: business }, launches: [{
|
|
business: 'demo', role: 'coder', run: 'runtime-run', harness: 'pi',
|
|
pid: process.pid, startTime: startTimeOf(process.pid),
|
|
}] });
|
|
const client = new Client({ path: runtime.path, cap: runtime.launches[0].cap });
|
|
await client.call('role.claim');
|
|
const spec = await client.call('spec.read', { project: 'stack' });
|
|
assert.equal(spec.commit, initialized.commit);
|
|
assert.equal(spec.confirmed, true);
|
|
const record = await client.call('record.write', { project: 'stack', path: 'docs/runtime.md', record: '# Runtime\n' });
|
|
assert.match(record.commit, /^[0-9a-f]{40}$/);
|
|
assert.equal(readFileSync(join(initialized.repo, record.path), 'utf8'), '# Runtime\n');
|
|
});
|