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.
96 lines
6.3 KiB
JavaScript
96 lines
6.3 KiB
JavaScript
// Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): the runtime wires the limit checks. The limits verb
|
|
// carries each token limit's count, a reset moves since and the count follows, and the host's
|
|
// mayLaunch asks the shipped token-limit through the real loader.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, rmSync, mkdirSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
|
|
const SINCE = '2026-10-18T07:00:00.000Z';
|
|
const CODER = ['tokens', 'instances', 'coder'];
|
|
|
|
async function setup(t, plugins, limit = 0) {
|
|
const { startBroker } = await import('../src/runtime.mjs');
|
|
const { businessDoc, rolesCopy, systemFor, writeJson } = await import('../../business/tests/helpers.mjs');
|
|
const root = mkdtempSync(join(tmpdir(), 'mosaic-limits-tokens-')), rolesDir = rolesCopy(root), doc = businessDoc(root, 'demo');
|
|
doc.plugins = { 'limit-check': plugins, harness: ['pi'] };
|
|
doc.launch.limits = { ...doc.launch.limits, tokens: { instances: { coder: { limit, since: SINCE } } } };
|
|
writeJson(join(root, 'businesses/demo.json'), doc);
|
|
mkdirSync(join(root, 'data'), { mode: 0o700 });
|
|
const bus = await startBroker({ dataRoot: join(root, 'data'),
|
|
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
|
|
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } }, definitions: { coder: {} }, projects: { stack: { root: '/nonexistent' } } } },
|
|
organization: { dir: root, rolesDir, system: systemFor(root) } });
|
|
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
|
|
return { bus, human: bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
|
|
}
|
|
|
|
test('REQ-LIMIT-3 REQ-LIMIT-4 runtime: limits carries the token count, mayLaunch refuses at the limit, a reset moves since', async t => {
|
|
const { bus, human } = await setup(t, ['token-limit']);
|
|
const view = bus.broker.request(human, { verb: 'limits' });
|
|
assert.equal(view.tokens.length, 1);
|
|
assert.deepEqual({ ...view.tokens[0], count: Number(view.tokens[0].count) },
|
|
{ path: CODER, limit: 0, since: SINCE, status: 'counted', check: 'token-limit', count: 0,
|
|
kinds: view.tokens[0].kinds, unknownTurns: 0 });
|
|
assert.deepEqual(await bus.mayLaunch({ business: 'demo', instance: 'coder', provider: 'zai', model: 'glm-5.3-flash' }),
|
|
{ allow: false, check: 'token-limit', hit: { path: CODER, limit: 0, count: view.tokens[0].count, unknownTurns: 0 } });
|
|
// An instance with no limit launches.
|
|
assert.deepEqual(await bus.mayLaunch({ business: 'demo', instance: 'reviewer', provider: 'zai', model: 'glm-5.3-flash' }), { allow: true });
|
|
const done = bus.broker.request(human, { verb: 'limits.set', args: { revision: view.revision, reset: [CODER] } });
|
|
assert.ok(done.tokens[0].since > SINCE, done.tokens[0].since);
|
|
assert.equal(done.tokens[0].status, 'counted');
|
|
});
|
|
|
|
test('REQ-LIMIT-3 runtime: with no limit check switched on, the limit shows not enforced and every launch is allowed', async t => {
|
|
const { bus, human } = await setup(t, []);
|
|
assert.deepEqual(bus.broker.request(human, { verb: 'limits' }).tokens,
|
|
[{ path: CODER, limit: 0, since: SINCE, status: 'not-enforced', reason: null }]);
|
|
assert.deepEqual(await bus.mayLaunch({ business: 'demo', instance: 'coder', provider: 'zai', model: 'glm-5.3-flash' }), { allow: true });
|
|
});
|
|
|
|
test('REQ-LIMIT-4 runtime: the broker asks the shipped token-limit after a turn, and the run at its limit stops on its next receive', async t => {
|
|
const { bus, human } = await setup(t, ['token-limit']);
|
|
const { broker } = bus;
|
|
const coder = broker.bindLaunch({ business: 'demo', role: 'coder', run: 'run1', harness: 'pi', provider: 'zai', model: 'glm-5.3-flash' });
|
|
broker.request(coder, { verb: 'role.claim' });
|
|
const { id } = broker.request(human, { verb: 'message.send', args: { to: 'coder', body: 'work' } });
|
|
broker.request(coder, { verb: 'message.receive' });
|
|
await broker.request(coder, { verb: 'usage.begin', args: { turn: id } });
|
|
await broker.request(coder, { verb: 'usage.end', args: { turn: id } });
|
|
assert.throws(() => broker.request(coder, { verb: 'message.receive' }), { code: 'not-holder' });
|
|
// endLaunch reads the reason from the claim row the stop wrote with by='limit-check'.
|
|
assert.deepEqual(bus.endLaunch({ business: 'demo', run: 'run1', reason: 'exited', exitCode: 22 }),
|
|
{ released: false, limit: 'limit: token-limit tokens.instances.coder limit 0 count 0' });
|
|
});
|
|
|
|
test('REQ-LIMIT-4 runtime: with no usage source a turn is unknown, counts zero and stops nothing under a limit of 1', async t => {
|
|
const { bus, human } = await setup(t, ['token-limit'], 1);
|
|
const { broker } = bus;
|
|
const coder = broker.bindLaunch({ business: 'demo', role: 'coder', run: 'run1', harness: 'pi', provider: 'zai', model: 'glm-5.3-flash' });
|
|
broker.request(coder, { verb: 'role.claim' });
|
|
const { id } = broker.request(human, { verb: 'message.send', args: { to: 'coder', body: 'work' } });
|
|
broker.request(coder, { verb: 'message.receive' });
|
|
await broker.request(coder, { verb: 'usage.begin', args: { turn: id } });
|
|
await broker.request(coder, { verb: 'usage.end', args: { turn: id } });
|
|
broker.request(coder, { verb: 'message.receive' });
|
|
const [row] = broker.request(human, { verb: 'limits' }).tokens;
|
|
assert.deepEqual({ count: row.count, unknownTurns: row.unknownTurns, status: row.status }, { count: 0, unknownTurns: 1, status: 'counted' });
|
|
assert.deepEqual(await bus.mayLaunch({ business: 'demo', instance: 'coder', provider: 'zai', model: 'glm-5.3-flash' }), { allow: true });
|
|
});
|
|
|
|
test('REQ-LIMIT-3 runtime: switching a limit check on with plugins.set loads it without a launch or a turn', async t => {
|
|
const { bus, human } = await setup(t, []);
|
|
const { revision } = bus.broker.request(human, { verb: 'plugins' });
|
|
bus.broker.request(human, { verb: 'plugins.set', args: { revision, plugins: { 'limit-check': ['token-limit'] } } });
|
|
let row;
|
|
for (let i = 0; i < 100; i++) {
|
|
[row] = bus.broker.request(human, { verb: 'limits' }).tokens;
|
|
if (row.status === 'counted') break;
|
|
await delay(20);
|
|
}
|
|
assert.equal(row.status, 'counted', JSON.stringify(row));
|
|
assert.equal(row.check, 'token-limit');
|
|
});
|