feat: integrate reviewed proxy page and managed model keys

Source-candidate: poe d9a564e6d20f5f0fd75ce45b12cbbf8bb4c9d568\nSource-candidate: magica38ba5762b06716db1dee00d0afec162656488f5c\nValidated321whole-file phase plus whole bus1349/model-proxy100/webui490; final gate tooling checks pending, not landed.
This commit is contained in:
jason.woltje committed 2026-10-26 12:04:21 -05:00
1 parent acd2f4abfa
commit 362893d043
87 files changed
+4766 -99

No files matched your search

+4 -1
View File
@@ -216,7 +216,10 @@ delists the agent, and no session starts on a key the proxy refuses.
how the code relies on that. how the code relies on that.
6. **Preflight (D17).** Before each launch in proxy mode, the bus sends one `GET /v1/models` with the agent's 6. **Preflight (D17).** Before each launch in proxy mode, the bus sends one `GET /v1/models` with the agent's
key. A 401 refuses `proxy-key-refused`, and a connection failure refuses `proxy-down`. A session with no key. A 401 refuses `proxy-key-refused`, and a connection failure refuses `proxy-down`. A session with no
output for `proxy.firstOutputSeconds` (default 120) is stopped and logged as `proxy-stall`. output during the one initial `proxy.firstOutputSeconds` budget (default 120), armed by successful
first actual adapter-child spawn, is stopped and logged as `proxy-stall`. Idle before then is
unarmed; later turns do not reset or extend it; first output cancels. Invalid first-turn metadata
uses the same existing stop and value-free receipt once, unless output already cancelled it.
7. If P1 found that a client key can be limited to some models, narrow each agent's key to its role's 7. If P1 found that a client key can be limited to some models, narrow each agent's key to its role's
models. If it can't, say so in BUILD.md. The bundle's model choice stays the guard. models. If it can't, say so in BUILD.md. The bundle's model choice stays the guard.
+7 -2
View File
@@ -249,8 +249,13 @@ D17. **Key and proxy preflight.** Before a launch in proxy mode, the bus makes o
`GET /v1/models`. The build row confirms on the stand-in that this is answered locally with no upstream call. A `GET /v1/models`. The build row confirms on the stand-in that this is answered locally with no upstream call. A
401 refuses the launch with `proxy-key-refused`. A connection failure refuses with `proxy-down`. Neither starts a 401 refuses the launch with `proxy-key-refused`. A connection failure refuses with `proxy-down`. Neither starts a
session, so a revoked key never reaches Claude Code's silent retry. A key that is revoked mid-session is session, so a revoked key never reaches Claude Code's silent retry. A key that is revoked mid-session is
revoked by the stack, which stops that session first (D14). A session with no output for revoked by the stack, which stops that session first (D14). Idle before the first actual adapter turn
`proxy.firstOutputSeconds` (default 120) is stopped and logged as `proxy-stall`. is unarmed. The successful adapter-child spawn arms one initial `proxy.firstOutputSeconds` budget
(default 120) per proxy run; later turns never reset or extend it and first output cancels it. A turn
still silent when that budget expires is stopped through the existing trusted path and logged as
`proxy-stall`. Invalid first-turn metadata also uses that same stop and value-free receipt once,
unless first output already cancelled the watcher. The internal timestamp protocol uses the current
Linux host/runner shared monotonic clock; a confined timestamp-bound check is required verification.
## 7. Usage and limits ## 7. Usage and limits
+8
View File
@@ -820,3 +820,11 @@ refresh. The proxy kept serving with its fallback version. Evidence: origin/feth
cutover, find a config switch that turns these off and set it in P1's initial config with a test, or cutover, find a config switch that turns these off and set it in P1's initial config with a test, or
decide on purpose to allow or block that egress and record it in P7's receipt. Tracked as Vikunja 633 decide on purpose to allow or block that egress and record it in P7's receipt. Tracked as Vikunja 633
(umbrella 237). (umbrella 237).
## Row 331: human-only per-agent model rotation override — P7
Sage ruling sage-4388 (2026-10-24) limits P2 to the 30-day model-key rotation default,
with no setter and no `proxy.rotate --agent --rotate-by` verb. A human-only
per-agent rotation override is bound to P7, with Jason's cutover/settings review.
It must preserve D20/Q222: no agent or tester can change the period. Row 331 ships
only the default and the approved proxy.json grace/first-output settings.
+66 -27
View File
@@ -1,4 +1,6 @@
import { PROXY_PAGE_VERBS, PROXY_PAGE_CODES } from './proxy-page-vocabulary.mjs';
import { randomUUID, randomBytes, createHash } from 'node:crypto'; import { randomUUID, randomBytes, createHash } from 'node:crypto';
import { validManagementKey, proxyTiming } from '../../model-proxy/src/validation.mjs';
import { parseAddress } from './address.mjs'; import { parseAddress } from './address.mjs';
import { isAbsolute, dirname } from 'node:path'; import { isAbsolute, dirname } from 'node:path';
import { initializeControl, processingStatus } from '../../processing-control/src/state.mjs'; import { initializeControl, processingStatus } from '../../processing-control/src/state.mjs';
@@ -419,6 +421,7 @@ export class Broker {
#organization; #organization;
#credentialVerbs; #credentialVerbs;
#proxyInit; #proxyInit;
#proxyPage;
#project; #project;
#library; #library;
// C2-06 (design 5.2, B7): cap -> the session's kept web.fetch pages, in a library business only. // C2-06 (design 5.2, B7): cap -> the session's kept web.fetch pages, in a library business only.
@@ -477,7 +480,7 @@ export class Broker {
#clock = 0; #clock = 0;
#processing; #processing;
// credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use(). // credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use().
constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, library = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, proxyInit = null, hostGrant = null, hostKeys = null, processing = null, testerOn = null, grantScope = null, roleFile = null, giteaGrant = null, giteaKeys = null, access = null, accessStamp = null, compactions = null }) { constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, library = null, usage = null, limits = null, harnessOn = null, credentialVerbs = null, proxyInit = null, proxyPage = null, hostGrant = null, hostKeys = null, processing = null, testerOn = null, grantScope = null, roleFile = null, giteaGrant = null, giteaKeys = null, access = null, accessStamp = null, compactions = null }) {
if (!object(businesses) || !Object.keys(businesses).length) fail('invalid-business'); if (!object(businesses) || !Object.keys(businesses).length) fail('invalid-business');
// Row 194 b15 (sage-2501): the function-argument checks come before anything reads the store. // Row 194 b15 (sage-2501): the function-argument checks come before anything reads the store.
if (harnessOn !== null && typeof harnessOn !== 'function') throw new TypeError('harnessOn must be a function'); if (harnessOn !== null && typeof harnessOn !== 'function') throw new TypeError('harnessOn must be a function');
@@ -502,6 +505,7 @@ export class Broker {
// Row 199: the runtime's credential lifecycle (credential-verbs.mjs); it holds the store, not the broker. // Row 199: the runtime's credential lifecycle (credential-verbs.mjs); it holds the store, not the broker.
this.#credentialVerbs = credentialVerbs; this.#credentialVerbs = credentialVerbs;
this.#proxyInit = proxyInit; this.#proxyInit = proxyInit;
this.#proxyPage = proxyPage;
this.#hostGrant = hostGrant; this.#hostGrant = hostGrant;
this.#hostKeys = hostKeys; this.#hostKeys = hostKeys;
this.#giteaGrant = giteaGrant; this.#giteaGrant = giteaGrant;
@@ -1365,7 +1369,7 @@ export class Broker {
})); }));
} }
// Trusted launcher API, deliberately NOT a socket verb. S6 supplies immutable launch records. // Trusted launcher API, deliberately NOT a socket verb. S6 supplies immutable launch records.
bindLaunch(record) { #launchProof(record) {
keys( keys(
record, record,
['business', 'role', 'run', 'harness', 'address', 'pid', 'startTime', 'thread', 'session', 'provider', 'model', 'account', 'transcript'], ['business', 'role', 'run', 'harness', 'address', 'pid', 'startTime', 'thread', 'session', 'provider', 'model', 'account', 'transcript'],
@@ -1414,22 +1418,28 @@ export class Broker {
// A record without a session keeps the body it had before row 264a. // A record without a session keeps the body it had before row 264a.
...(record.session === undefined ? {} : { session: record.session }), ...(record.session === undefined ? {} : { session: record.session }),
}; };
// A run an earlier broker bound is bound again unchanged (the host's recover()), even after its
// harness was switched off, so it can be ended.
if (!old) this.#processingWork();
if (!old && !this.#harnessOn(record.business, record.harness)) fail('invalid-harness');
// FED R4 (design 3.7, D1): a new run of a revoked thread refuses, which covers a start racing the
// revocation. recover()'s rebind of an earlier run isn't refused, so it can still end that run.
if (!old && record.thread !== undefined && this.#threadLost(record.business, record.thread)) fail('thread-revoked');
if (old) { if (old) {
if (old.actor_role !== record.role || old.body !== JSON.stringify(body)) fail('launch-record-mismatch'); if (old.actor_role !== record.role || old.body !== JSON.stringify(body)) fail('launch-record-mismatch');
// An ended run stays ended across broker restarts. if (this.#store.get("SELECT 1 FROM events WHERE business=? AND kind='session.ended' AND subject=?", record.business, record.run)) fail('run-ended');
if (this.#store.get("SELECT 1 FROM events WHERE business=? AND kind='session.ended' AND subject=?", record.business, record.run)) }
fail('run-ended'); return { key, session, old, body };
} else this.#store.transaction(() => this.#event(session, 'session.launched', body, record.run)); }
bindLaunch(record) {
const { key, session, old, body } = this.#launchProof(record);
if (!old) this.#processingWork();
if (!old && !this.#harnessOn(record.business, record.harness)) fail('invalid-harness');
if (!old && record.thread !== undefined && this.#threadLost(record.business, record.thread)) fail('thread-revoked');
if (!old) this.#store.transaction(() => this.#event(session, 'session.launched', body, record.run));
this.#runs.set(key, session); this.#runs.set(key, session);
return this.#cap(session); return this.#cap(session);
} }
// Trusted historical cleanup ONLY: exact persisted launch proof, no new run,
// capability or launch/rotation binding; existing end/release semantics.
recoverLaunch(record) {
const { session, old } = this.#launchProof(record);
if (!old) fail('unknown-run');
return this.#endSession(session, { business: record.business, run: record.run, reason: 'host-lost', exitCode: null });
}
// Trusted launcher API, the counterpart of bindLaunch and likewise NOT a socket verb. The S6 host // Trusted launcher API, the counterpart of bindLaunch and likewise NOT a socket verb. The S6 host
// calls it once when a launched run's process exits: session.ended is written, the run's claim is // calls it once when a launched run's process exits: session.ended is written, the run's claim is
// released if it still holds one, and every capability of the run stops working. // released if it still holds one, and every capability of the run stops working.
@@ -1438,6 +1448,10 @@ export class Broker {
this.#business(identifier(record.business)); this.#business(identifier(record.business));
const key = record.business + ':' + identifier(record.run); const key = record.business + ':' + identifier(record.run);
const session = this.#runs.get(key) ?? fail('unknown-run'); const session = this.#runs.get(key) ?? fail('unknown-run');
return this.#endSession(session, record);
}
#endSession(session, record) {
const key = session.business + ':' + session.run;
if (this.#ended.has(key)) fail('run-ended'); if (this.#ended.has(key)) fail('run-ended');
identifier(record.reason); identifier(record.reason);
const exitCode = record.exitCode ?? null; const exitCode = record.exitCode ?? null;
@@ -2587,7 +2601,7 @@ export class Broker {
} }
let step = 'B'; let step = 'B';
try { try {
const result = write('instance.fire'); const result = await write('instance.fire');
step = 'C'; step = 'C';
this.#store.transaction(() => { this.#store.transaction(() => {
this.#recordAuthority(sc, 'role.fire', authority, { reason: args.reason, transaction: result.transaction ?? null }); this.#recordAuthority(sc, 'role.fire', authority, { reason: args.reason, transaction: result.transaction ?? null });
@@ -4593,7 +4607,8 @@ export class Broker {
this.#organizationFail(e, 'instances.set'); this.#organizationFail(e, 'instances.set');
} }
// sage-1610 (b): a deleted agent's Vikunja bot is named, so the human removes it in Vikunja. // sage-1610 (b): a deleted agent's Vikunja bot is named, so the human removes it in Vikunja.
return { transaction: out.transaction, retry: false, ...(out.tracker ? { bot: out.tracker.bot } : {}) }; const finish = out => ({ transaction: out.transaction, retry: false, ...(out.tracker ? { bot: out.tracker.bot } : {}) });
return out?.then ? out.then(finish, error => this.#organizationFail(error, 'instances.set')) : finish(out);
} }
// The close of a taken request, with the run it started, the delete's transaction or the code // The close of a taken request, with the run it started, the delete's transaction or the code
// that stopped it. A delete also names the agent's Vikunja bot (sage-1610 b). A fire's end // that stopped it. A delete also names the agent's Vikunja bot (sage-1610 b). A fire's end
@@ -4803,6 +4818,17 @@ export class Broker {
try { try {
keys(request, ['verb', 'args'], ['verb']); keys(request, ['verb', 'args'], ['verb']);
string(request.verb, 64); string(request.verb, 64);
// P4: this boundary precedes the generic user/tester gates so every
// nonhuman caller gets human-required. Never secret-check, clone or trail
// a write-only API key or a callback URL as request content.
if (PROXY_PAGE_VERBS.includes(request.verb)) {
this.#human(s);
if (!this.#proxyPage) fail('not-configured');
const args = request.args ?? {};
if (!object(args)) fail('invalid-request');
return Promise.resolve().then(() => this.#proxyPage(request.verb, args, { business: s.business, human: s.human }))
.catch(error => { throw this.#refused(s, new BusError(PROXY_PAGE_CODES.includes(error.code) ? error.code : 'proxy-down'), { verb: request.verb }); });
}
// FED R3 (design 3.5, 3.7 trigger 1): a user reaches the collaborator list only, and each // FED R3 (design 3.5, 3.7 trigger 1): a user reaches the collaborator list only, and each
// request reads the access file again, so a removed or expired grant refuses the next one. // request reads the access file again, so a removed or expired grant refuses the next one.
let grants = null; let grants = null;
@@ -4823,14 +4849,16 @@ export class Broker {
if (request.verb === 'proxy.init') { if (request.verb === 'proxy.init') {
this.#human(s); this.#human(s);
if (!this.#proxyInit) fail('not-configured'); if (!this.#proxyInit) fail('not-configured');
keys(args, ['key', 'port'], ['key']); keys(args, ['key', 'port', 'rotateGraceMinutes', 'firstOutputSeconds'], ['key']);
if (typeof args.key !== 'string' || !args.key.trim() || args.key.length > 4096 || /[\x00-\x1f\x7f]/.test(args.key)) fail('proxy-key-invalid'); if (!validManagementKey(args.key)) fail('proxy-key-invalid');
if (Object.hasOwn(args, 'port') && (!Number.isInteger(args.port) || args.port < 1024 || args.port > 65535 || args.port === 8317)) fail('proxy-config-invalid'); if (Object.hasOwn(args, 'port') && (!Number.isInteger(args.port) || args.port < 1024 || args.port > 65535 || args.port === 8317)) fail('proxy-config-invalid');
try { proxyTiming(args); } catch (error) { fail(error.code); }
// Async initialization is outside the store transaction. Refusals retain codes only. // Async initialization is outside the store transaction. Refusals retain codes only.
return Promise.resolve().then(() => this.#proxyInit(args)).then(result => { return Promise.resolve().then(() => this.#proxyInit(args)).then(result => {
if (!object(result) || Object.keys(result).sort().join(',') !== 'binary,port' if (!object(result) || Object.keys(result).some(k => !['binary', 'port', 'rotateGraceMinutes', 'firstOutputSeconds'].includes(k))
|| !Number.isInteger(result.port) || result.port < 1024 || result.port > 65535 || result.port === 8317 || !Number.isInteger(result.port) || result.port < 1024 || result.port > 65535 || result.port === 8317
|| typeof result.binary !== 'string' || !result.binary.startsWith('/')) fail('proxy-init-failed'); || typeof result.binary !== 'string' || !result.binary.startsWith('/')) fail('proxy-init-failed');
try { proxyTiming(result); } catch (error) { fail(error.code); }
this.#store.transaction(() => this.#event(s, 'action.allowed', { call: 'proxy.init', port: result.port }, null)); this.#store.transaction(() => this.#event(s, 'action.allowed', { call: 'proxy.init', port: result.port }, null));
return result; return result;
}).catch(error => { throw this.#refused(s, new BusError(/^proxy-[a-z-]+$/.test(error.code ?? '') ? error.code : 'proxy-init-failed'), request); }); }).catch(error => { throw this.#refused(s, new BusError(/^proxy-[a-z-]+$/.test(error.code ?? '') ? error.code : 'proxy-init-failed'), request); });
@@ -4882,7 +4910,10 @@ export class Broker {
} }
if (CREDENTIAL_VERBS.includes(request.verb)) return this.#requestCredential(s, request.verb, structuredClone(args)); if (CREDENTIAL_VERBS.includes(request.verb)) return this.#requestCredential(s, request.verb, structuredClone(args));
if (request.verb === 'instance.hire' || request.verb === 'instance.fire') if (request.verb === 'instance.hire' || request.verb === 'instance.fire')
return this.#chiefLifecycle(s, request.verb, structuredClone(args)); {
const result = this.#chiefLifecycle(s, request.verb, structuredClone(args));
return result?.then ? result.catch(error => { throw this.#refused(s, error, request); }) : result;
}
if (request.verb === 'grants.scope' || request.verb === 'grants.scope.set') if (request.verb === 'grants.scope' || request.verb === 'grants.scope.set')
return this.#grantScopeVerb(s, request.verb, structuredClone(args)); return this.#grantScopeVerb(s, request.verb, structuredClone(args));
if (["limits", "limits.set", "organization.set", "organization.revision", "plugins", "plugins.set", "instances", "instances.set", "routes", "routes.set", "veto.set", "library.limits.set"].includes(request.verb)) { if (["limits", "limits.set", "organization.set", "organization.revision", "plugins", "plugins.set", "instances", "instances.set", "routes", "routes.set", "veto.set", "library.limits.set"].includes(request.verb)) {
@@ -5083,7 +5114,8 @@ export class Broker {
const { reason: _, ...written } = args; const { reason: _, ...written } = args;
const write = (op) => { const write = (op) => {
try { try {
return this.#organization(s.business, op, written, actor, context); const result = this.#organization(s.business, op, written, actor, context);
return result?.then ? result.catch(error => this.#organizationFail(error, verb)) : result;
} catch (e) { } catch (e) {
this.#organizationFail(e, verb); this.#organizationFail(e, verb);
} }
@@ -5091,8 +5123,11 @@ export class Broker {
if (hire) { if (hire) {
this.#hireable(s, args.definition); this.#hireable(s, args.definition);
const result = write('instance.hire'); const result = write('instance.hire');
this.#store.transaction(() => this.#consumeAuthority(s, action, authority, { reason: args.reason, transaction: result.transaction })); const finish = result => {
return result; this.#store.transaction(() => this.#consumeAuthority(s, action, authority, { reason: args.reason, transaction: result.transaction }));
return result;
};
return result?.then ? result.then(finish) : finish(result);
} }
if (!Object.hasOwn(b.roles, args.name)) fail('unknown-role'); if (!Object.hasOwn(b.roles, args.name)) fail('unknown-role');
if (END_PROTECTED.includes(b.roles[args.name].definition)) fail('organization-definition-refused'); if (END_PROTECTED.includes(b.roles[args.name].definition)) fail('organization-definition-refused');
@@ -5142,11 +5177,14 @@ export class Broker {
// Step B: the delist. An instance already delisted counts as done. // Step B: the delist. An instance already delisted counts as done.
const result = write('instance.fire'); const result = write('instance.fire');
// Step C: log the fire and record it done. A failure at B or C leaves I firing until a repeat. // Step C: log the fire and record it done. A failure at B or C leaves I firing until a repeat.
this.#store.transaction(() => { const finish = result => {
this.#consumeAuthority(s, action, authority, { reason: args.reason, transaction: result.transaction ?? null }); this.#store.transaction(() => {
this.#event(s, 'action.allowed', { action: 'fire.done', transaction: result.transaction ?? null }, args.name); this.#consumeAuthority(s, action, authority, { reason: args.reason, transaction: result.transaction ?? null });
}); this.#event(s, 'action.allowed', { action: 'fire.done', transaction: result.transaction ?? null }, args.name);
return { ...result, ...stopped }; });
return { ...result, ...stopped };
};
return result?.then ? result.then(finish) : finish(result);
} }
// The shape of hire and fire arguments. // The shape of hire and fire arguments.
#hireFireArgs(action, args) { #hireFireArgs(action, args) {
@@ -5179,6 +5217,7 @@ export class Broker {
// The bus code for an organization writer's refusal. Anything the writer didn't refuse by name is // The bus code for an organization writer's refusal. Anything the writer didn't refuse by name is
// outcome-unknown: the write may have committed. // outcome-unknown: the write may have committed.
#organizationFail(e, verb) { #organizationFail(e, verb) {
if (['proxy-revoke-revision-conflict', 'proxy-key-issue-failed', 'proxy-key-revoking', 'proxy-key-refused', 'proxy-down', 'proxy-open'].includes(e.code)) fail(e.code);
// Row 239 (#1745): the writer marks an error raised after a fire's or a delete's revoke ran, so // Row 239 (#1745): the writer marks an error raised after a fire's or a delete's revoke ran, so
// the human reads that the tokens may already be revoked. Without the mark, today's code. // the human reads that the tokens may already be revoked. Without the mark, today's code.
if (e.revoked) fail(e.code === "revision-conflict" ? "revision-conflict-revoked" : "outcome-unknown-revoked"); if (e.revoked) fail(e.code === "revision-conflict" ? "revision-conflict-revoked" : "outcome-unknown-revoked");
+190 -9
View File
@@ -1,11 +1,12 @@
import { import {
openSync, closeSync, fstatSync, readFileSync, lstatSync, realpathSync, constants, mkdirSync, writeSync, fsyncSync, openSync, closeSync, fstatSync, readSync, readFileSync, lstatSync, realpathSync, constants, mkdirSync, writeSync, fsyncSync,
renameSync, unlinkSync, rmdirSync, readdirSync, renameSync, unlinkSync, rmdirSync, readdirSync,
} from 'node:fs'; } from 'node:fs';
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { isDeepStrictEqual } from 'node:util';
import { BusError } from './broker.mjs'; import { BusError } from './broker.mjs';
import { credentialKind, isHostKind } from '../../business/src/vocabulary.mjs'; import { credentialKind, isHostKind, ID_PATTERN } from '../../business/src/vocabulary.mjs';
const deny = (code) => { const deny = (code) => {
throw new BusError(code); throw new BusError(code);
}; };
@@ -111,6 +112,8 @@ export const privateDir = (path, { missing = false } = {}) => {
}; };
export class Credentials { export class Credentials {
#tokens = new Map(); #tokens = new Map();
#modelProofs = new Map();
#modelTokens = new WeakMap();
// Row 199: every reference the bus loaded, so issue, rotate, revoke and status work per entry. // Row 199: every reference the bus loaded, so issue, rotate, revoke and status work per entry.
#refs = new Map(); #refs = new Map();
#revoked = new Set(); #revoked = new Set();
@@ -125,7 +128,7 @@ export class Credentials {
// storeRoot is <configDir>/secrets/agents (design D1); a reference with store: true must name the // storeRoot is <configDir>/secrets/agents (design D1); a reference with store: true must name the
// path under it for its own business, agent and service. `held` lists "instance:service" keys whose // path under it for its own business, agent and service. `held` lists "instance:service" keys whose
// reference loads and whose file doesn't (#436b section 5). // reference loads and whose file doesn't (#436b section 5).
constructor({ references = {}, repoRoots = [], dataRoot, env = process.env, storeRoot = null, held = [] }) { constructor({ references = {}, repoRoots = [], dataRoot, env = process.env, storeRoot = null, held = [], modelAgents = [] }) {
this.#storeRoot = storeRoot; this.#storeRoot = storeRoot;
this.#repoRoots = repoRoots; this.#repoRoots = repoRoots;
this.#dataRoot = dataRoot; this.#dataRoot = dataRoot;
@@ -139,6 +142,7 @@ export class Credentials {
const value = this.#read(ref, env, service); const value = this.#read(ref, env, service);
if (value !== null) this.#tokens.set(instance + ':' + service, { ...value, date }); if (value !== null) this.#tokens.set(instance + ':' + service, { ...value, date });
} }
for (const instance of modelAgents) this.registerModel(instance);
} catch (e) { } catch (e) {
this.close(); this.close();
throw e; throw e;
@@ -146,7 +150,7 @@ export class Credentials {
} }
// One reference's checks, for the constructor and register (row 235, r6 X4). Returns its date. // One reference's checks, for the constructor and register (row 235, r6 X4). Returns its date.
#checkRef(instance, service, ref) { #checkRef(instance, service, ref) {
if (!credentialKind(service) || !ref || typeof ref !== 'object') deny('credential-reference'); if (!(credentialKind(service) || service === 'model') || !ref || typeof ref !== 'object') deny('credential-reference');
const date = service === 'vikunja' ? ref.expires : ref.rotateBy; const date = service === 'vikunja' ? ref.expires : ref.rotateBy;
if (!day(date)) deny('credential-date'); if (!day(date)) deny('credential-date');
if ( if (
@@ -167,6 +171,7 @@ export class Credentials {
const k = instance + ':' + service; const k = instance + ':' + service;
if (this.#tokens.has(k)) deny('credential-reference'); if (this.#tokens.has(k)) deny('credential-reference');
const date = this.#checkRef(instance, service, ref); const date = this.#checkRef(instance, service, ref);
this.#invalidateModelProof(instance, service);
this.#refs.set(k, { ref, date }); this.#refs.set(k, { ref, date });
this.#revoked.delete(k); this.#revoked.delete(k);
} }
@@ -174,14 +179,159 @@ export class Credentials {
unregister(instance, service) { unregister(instance, service) {
const k = instance + ':' + service; const k = instance + ':' + service;
if (this.#tokens.has(k)) deny('credential-reference'); if (this.#tokens.has(k)) deny('credential-reference');
this.#invalidateModelProof(instance, service);
this.#refs.delete(k); this.#refs.delete(k);
this.#revoked.delete(k); this.#revoked.delete(k);
} }
// Host-only implicit kind. It is deliberately absent from role service vocabulary:
// neither role authority nor an agent's credential reference can opt into it.
registerModel(instance) {
const parts = typeof instance === 'string' ? instance.split('/') : [];
if (parts.length !== 2 || parts.some(p => !ID_PATTERN.test(p))) deny('credential-reference');
const k = instance + ':model';
if (this.#refs.has(k)) return;
const file = this.#storePath(instance, 'model');
const ref = { store: true, file, rotateBy: '9999-12-31' };
this.#invalidateModelProof(instance);
this.#refs.set(k, { ref, date: ref.rotateBy });
// An instance may have no credential directory before its first hire/launch.
if (!privateDir(this.#storeRoot, { missing: true })) return;
const value = this.#read(ref, {}, 'model');
if (value) {
this.#tokens.set(k, { ...value, date: value.model.rotateBy });
this.#refs.set(k, { ref: { ...ref, rotateBy: value.model.rotateBy }, date: value.model.rotateBy });
}
}
// Trusted lifecycle use only; no socket verb returns this private record.
modelRecord(instance) {
const entry = this.#tokens.get(instance + ':model');
return entry?.model ? structuredClone(entry.model) : null;
}
writeModel(instance, record) {
this.registerModel(instance);
this.issue(instance, 'model', JSON.stringify(record), record?.rotateBy);
return this.status().find(e => e.instance === instance && e.service === 'model');
}
// Host-only proof state, never returned by status or an agent/IPC verb.
#invalidateModelProof(instance, service = 'model') {
if (service === 'model') this.#modelProofs.delete(instance);
}
#modelFileSnapshot(file, optional = false) {
let fd;
try {
try { fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); }
catch (error) { if (optional && error.code === 'ENOENT') return null; throw error; }
const first = fstatSync(fd, { bigint: true });
if (!first.isFile() || first.nlink !== 1n || first.uid !== BigInt(process.getuid()) || (first.mode & 0o777n) !== 0o600n || first.size > 16384n) deny('credential-revoke-failed');
const bytes = Buffer.alloc(16385); let length = 0;
while (length < bytes.length) {
const count = readSync(fd, bytes, length, bytes.length - length, null);
if (!count) break;
length += count;
}
const identity = stat => ({ dev: stat.dev, ino: stat.ino, size: stat.size, mode: stat.mode,
uid: stat.uid, nlink: stat.nlink, mtime: stat.mtimeNs, ctime: stat.ctimeNs });
const last = fstatSync(fd, { bigint: true }), path = lstatSync(file, { bigint: true });
if (length > 16384 || BigInt(length) !== first.size || !isDeepStrictEqual(identity(first), identity(last))
|| !isDeepStrictEqual(identity(first), identity(path))) deny('credential-revoke-failed');
const content = Buffer.from(bytes.subarray(0, length));
return { identity: identity(first), content, model: Credentials.#format('model', content.toString('utf8')).model };
} catch { deny('credential-revoke-failed'); }
finally { if (fd !== undefined) closeSync(fd); }
}
#modelRecoveryPaths(instance) {
const known = this.#known(instance, 'model'), file = this.#storePath(instance, 'model');
if (!known.ref.store || known.ref.file !== file) deny('credential-revoke-failed');
const directories = [this.#storeRoot, dirname(dirname(file)), dirname(file)].map(path => {
privateDir(path);
const stat = lstatSync(path, { bigint: true });
if (!stat.isDirectory() || stat.uid !== BigInt(process.getuid()) || (stat.mode & 0o777n) !== 0o700n) deny('credential-revoke-failed');
return { dev: stat.dev, ino: stat.ino, mode: stat.mode, uid: stat.uid };
});
return { file, directories };
}
#modelRecoveryFence(file) {
// Read-only refusal, never a generic cleanup/deletion allowlist.
if (readdirSync(dirname(file)).some(name => name !== 'model' && name !== 'model.tmp' && !credentialKind(name.replace(/\.tmp$/, '')))) deny('credential-revoke-failed');
}
#modelProof(instance, token) {
const proof = this.#modelTokens.get(token);
if (!proof || proof.instance !== instance || this.#modelProofs.get(instance) !== proof) deny('credential-revoke-failed');
return proof;
}
#verifyModelProof(instance, proof, temporaryExpected) {
const paths = this.#modelRecoveryPaths(instance), k = instance + ':model';
if (!isDeepStrictEqual(paths.directories, proof.directories) || this.#held.has(k) || this.#inflight.has(k)
|| !isDeepStrictEqual(this.modelRecord(instance), proof.final.model)) deny('credential-revoke-failed');
this.#modelRecoveryFence(paths.file);
const final = this.#modelFileSnapshot(paths.file), temporary = this.#modelFileSnapshot(paths.file + '.tmp', true);
if (!isDeepStrictEqual(final, proof.final) || !isDeepStrictEqual(temporary, temporaryExpected)) deny('credential-revoke-failed');
return paths.file;
}
captureModelRevocation(instance) {
this.#invalidateModelProof(instance);
try {
const { file, directories } = this.#modelRecoveryPaths(instance), k = instance + ':model';
const final = this.#modelFileSnapshot(file), temporary = this.#modelFileSnapshot(file + '.tmp', true);
const record = this.modelRecord(instance);
if (record?.state !== 'revoking' || this.#held.has(k) || this.#inflight.has(k)
|| !isDeepStrictEqual(record, final.model) || (temporary && !isDeepStrictEqual(final.model, temporary.model))) deny('credential-revoke-failed');
this.#modelRecoveryFence(file);
const token = Object.freeze({}), proof = { instance, directories, final, temporary, phase: 'captured' };
this.#modelProofs.set(instance, proof); this.#modelTokens.set(token, proof);
return token;
} catch { deny('credential-revoke-failed'); }
}
prepareModelCompletion(instance, token) {
try {
const proof = this.#modelProof(instance, token);
if (!['captured', 'tmp-removed', 'prepared'].includes(proof.phase)) deny('credential-revoke-failed');
const expected = proof.phase === 'captured' ? proof.temporary : null;
const file = this.#verifyModelProof(instance, proof, expected);
if (expected) {
// Cooperating single-controller/private0700 model writers only. NO await
// or callback after final verification. NOT inode/content CAS against an
// arbitrary external same-UID path replacement in the final syscall gap.
unlinkSync(file + '.tmp');
proof.phase = 'tmp-removed'; // Own successful unlink provenance before absence read.
try { lstatSync(file + '.tmp'); deny('credential-revoke-failed'); }
catch (error) { if (error.code !== 'ENOENT') throw error; }
}
proof.phase = 'prepared';
} catch { deny('credential-revoke-failed'); }
}
finishModelRevocation(instance, token) {
try {
const proof = this.#modelProof(instance, token), k = instance + ':model';
if (proof.phase === 'finished') {
// Exact same private completion provenance, invalidated by every model
// rewrite/reload/ref mutation/close. Not proof from initial absence.
const { file, directories } = this.#modelRecoveryPaths(instance);
if (!isDeepStrictEqual(directories, proof.directories) || this.#tokens.has(k) || this.#held.has(k) || this.#inflight.has(k)) deny('credential-revoke-failed');
this.#modelRecoveryFence(file);
for (const path of [file, file + '.tmp']) {
try { lstatSync(path); deny('credential-revoke-failed'); }
catch (error) { if (error.code !== 'ENOENT') throw error; }
}
return;
}
if (proof.phase !== 'prepared') deny('credential-revoke-failed');
const file = this.#verifyModelProof(instance, proof, null);
// Same qualified path-unlink limitation as tmp removal; no callback/await.
unlinkSync(file);
// ONE final unlink only. NO further fallible filesystem operation or abort
// callback after success. Ref/date remain as generic revoke bookkeeping;
// model held/inflight slots were required absent BEFORE final verification.
this.#tokens.delete(k); this.#revoked.add(k); this.#held.delete(k);
proof.phase = 'finished'; proof.final = null; proof.temporary = null;
} catch { deny('credential-revoke-failed'); }
}
// r6 X5: the executor holds the entry's #inflight slot for its steps, so a host.run refuses host-busy // r6 X5: the executor holds the entry's #inflight slot for its steps, so a host.run refuses host-busy
// and revoke's abort() reaches the executor. A slot already held refuses host-busy. Returns the signal. // and revoke's abort() reaches the executor. A slot already held refuses host-busy. Returns the signal.
claim(instance, service) { claim(instance, service) {
const k = instance + ':' + service; const k = instance + ':' + service;
if (this.#inflight.has(k)) deny('host-busy'); if (this.#inflight.has(k)) deny('host-busy');
this.#invalidateModelProof(instance, service);
const controller = new AbortController(); const controller = new AbortController();
this.#inflight.set(k, controller); this.#inflight.set(k, controller);
return controller.signal; return controller.signal;
@@ -271,6 +421,20 @@ export class Credentials {
} }
// { token } for gitea and vikunja, { token, key } for host.<id> (design D2). // { token } for gitea and vikunja, { token, key } for host.<id> (design D2).
static #format(service, token) { static #format(service, token) {
if (service === 'model') {
let m;
try { m = JSON.parse(token); } catch { deny('credential-format'); }
const keyOk = k => typeof k === 'string' && /^msk-[a-z0-9]+-[A-Za-z0-9_-]{43}$/.test(k);
const keyId = k => createHash('sha256').update(k).digest('hex').slice(0, 12);
const instant = s => typeof s === 'string' && !Number.isNaN(Date.parse(s)) && new Date(s).toISOString() === s;
if (!m || Array.isArray(m) || Object.keys(m).some(k => !['key', 'keyId', 'issuedAt', 'rotateBy', 'state', 'revokingAt', 'retiring'].includes(k))
|| !keyOk(m.key) || m.keyId !== keyId(m.key) || !instant(m.issuedAt) || !day(m.rotateBy)
|| !['valid', 'revoking'].includes(m.state) || (m.state === 'revoking' ? !instant(m.revokingAt) : m.revokingAt !== undefined)
|| !Array.isArray(m.retiring) || m.retiring.length > 64 || m.retiring.some(r => !r || Array.isArray(r)
|| Object.keys(r).sort().join(',') !== 'issuedAt,key,keyId,retireAt'
|| !keyOk(r.key) || r.keyId !== keyId(r.key) || !instant(r.issuedAt) || !instant(r.retireAt))) deny('credential-format');
return { token: m.key, model: m };
}
if (isHostKind(service)) { if (isHostKind(service)) {
const key = parseHostKey(token); const key = parseHostKey(token);
return { token: key.text, key }; return { token: key.text, key };
@@ -292,6 +456,7 @@ export class Credentials {
this.#known(instance, service); this.#known(instance, service);
if (!day(date)) deny('credential-date'); if (!day(date)) deny('credential-date');
const value = Credentials.#format(service, token); const value = Credentials.#format(service, token);
this.#invalidateModelProof(instance, service);
const file = this.#storePath(instance, service); const file = this.#storePath(instance, service);
const agentDir = dirname(file); const agentDir = dirname(file);
mkdirSync(this.#storeRoot, { recursive: true, mode: 0o700 }); mkdirSync(this.#storeRoot, { recursive: true, mode: 0o700 });
@@ -309,7 +474,7 @@ export class Credentials {
let fd; let fd;
try { try {
fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
writeSync(fd, value.key ? value.token : value.token + '\n'); writeSync(fd, service === 'model' ? JSON.stringify(value.model) + '\n' : value.key ? value.token : value.token + '\n');
fsyncSync(fd); fsyncSync(fd);
} finally { } finally {
if (fd !== undefined) closeSync(fd); if (fd !== undefined) closeSync(fd);
@@ -335,6 +500,7 @@ export class Credentials {
const { ref } = this.#known(instance, service); const { ref } = this.#known(instance, service);
if (this.#held.has(k) || !ref.store) deny('credential-reference'); if (this.#held.has(k) || !ref.store) deny('credential-reference');
if (!day(date)) deny('credential-date'); if (!day(date)) deny('credential-date');
this.#invalidateModelProof(instance, service);
const value = this.#read(ref, {}, service); const value = this.#read(ref, {}, service);
if (value === null) deny('credential-file'); if (value === null) deny('credential-file');
this.#refs.set(k, { ref: { ...ref, rotateBy: date }, date }); this.#refs.set(k, { ref: { ...ref, rotateBy: date }, date });
@@ -347,6 +513,7 @@ export class Credentials {
hold(instance, service) { hold(instance, service) {
const k = instance + ':' + service; const k = instance + ':' + service;
this.#known(instance, service); this.#known(instance, service);
this.#invalidateModelProof(instance, service);
this.#tokens.delete(k); this.#tokens.delete(k);
this.#held.add(k); this.#held.add(k);
this.#inflight.get(k)?.abort(); this.#inflight.get(k)?.abort();
@@ -380,8 +547,10 @@ export class Credentials {
} }
// Memory first, then the store file (design section 6, D12). A file or env reference outside the // Memory first, then the store file (design section 6, D12). A file or env reference outside the
// store is never unlinked; the caller turns its reference into store, so a restart finds nothing. // store is never unlinked; the caller turns its reference into store, so a restart finds nothing.
revoke(instance, service) { revoke(instance, service, { modelConfirmed = false } = {}) {
const { ref } = this.#known(instance, service); const { ref } = this.#known(instance, service);
if (service === 'model' && this.#tokens.has(instance + ':model') && !modelConfirmed) deny('credential-revoke-failed');
this.#invalidateModelProof(instance, service);
this.#tokens.delete(instance + ':' + service); this.#tokens.delete(instance + ':' + service);
this.#revoked.add(instance + ':' + service); this.#revoked.add(instance + ':' + service);
this.#held.delete(instance + ':' + service); this.#held.delete(instance + ':' + service);
@@ -399,7 +568,12 @@ export class Credentials {
// revoke for every service, then the agent's dir. A file other than a token or its tmp refuses. // revoke for every service, then the agent's dir. A file other than a token or its tmp refuses.
remove(instance) { remove(instance) {
const services = [...this.#refs.keys()].filter((k) => k.startsWith(instance + ':')).map((k) => k.slice(instance.length + 1)); const services = [...this.#refs.keys()].filter((k) => k.startsWith(instance + ':')).map((k) => k.slice(instance.length + 1));
for (const service of services) this.revoke(instance, service); for (const service of services) {
const proof = service === 'model' ? this.#modelProofs.get(instance) : null;
const completedModel = proof?.instance === instance && proof.phase === 'finished'
&& !this.#tokens.has(instance + ':model') && !this.#held.has(instance + ':model') && !this.#inflight.has(instance + ':model');
if (!completedModel) this.revoke(instance, service);
}
const agentDir = dirname(this.#storePath(instance, 'gitea')); const agentDir = dirname(this.#storePath(instance, 'gitea'));
let names; let names;
try { try {
@@ -426,7 +600,8 @@ export class Credentials {
} catch { } catch {
deny('invalid-request'); deny('invalid-request');
} }
for (const { token } of this.#tokens.values()) if (encoded?.includes(token)) deny('credential-leak'); for (const { token, model } of this.#tokens.values())
if ([token, ...(model?.retiring ?? []).map(r => r.key)].some(k => encoded?.includes(k))) deny('credential-leak');
} }
// Row 210 (design D5): whether a host.run for this agent and host is in flight. // Row 210 (design D5): whether a host.run for this agent and host is in flight.
busy(instance, service) { busy(instance, service) {
@@ -438,6 +613,7 @@ export class Credentials {
const k = instance + ':' + service; const k = instance + ':' + service;
const entry = this.#tokens.get(k); const entry = this.#tokens.get(k);
if (!entry) deny('credential-unavailable'); if (!entry) deny('credential-unavailable');
if (entry.model?.state === 'revoking') deny('proxy-key-revoking');
if (service === 'vikunja' && Date.now() >= Date.parse(entry.date + 'T00:00:00Z')) if (service === 'vikunja' && Date.now() >= Date.parse(entry.date + 'T00:00:00Z'))
deny('credential-expired'); deny('credential-expired');
if (!entry.key) { if (!entry.key) {
@@ -474,6 +650,7 @@ export class Credentials {
status() { status() {
return [...this.#refs.entries()].map(([key, { date }]) => { return [...this.#refs.entries()].map(([key, { date }]) => {
const hostKey = this.#tokens.get(key)?.key; const hostKey = this.#tokens.get(key)?.key;
const model = this.#tokens.get(key)?.model;
const i = key.lastIndexOf(':'), const i = key.lastIndexOf(':'),
service = key.slice(i + 1), service = key.slice(i + 1),
left = Date.parse(date + 'T00:00:00Z') - Date.now(); left = Date.parse(date + 'T00:00:00Z') - Date.now();
@@ -482,13 +659,16 @@ export class Credentials {
service, service,
date, date,
...(hostKey ? { fingerprint: hostKey.fingerprint, publicKey: hostKey.publicLine } : {}), ...(hostKey ? { fingerprint: hostKey.fingerprint, publicKey: hostKey.publicLine } : {}),
...(model ? { keyId: model.keyId, issuedAt: model.issuedAt,
...(model.revokingAt ? { revokingAt: model.revokingAt } : {}),
retiring: model.retiring.map(({ keyId, issuedAt, retireAt }) => ({ keyId, issuedAt, retireAt })) } : {}),
state: !this.#tokens.has(key) state: !this.#tokens.has(key)
? this.#held.has(key) ? this.#held.has(key)
? 'held' ? 'held'
: this.#revoked.has(key) : this.#revoked.has(key)
? 'revoked' ? 'revoked'
: 'not-issued' : 'not-issued'
: service !== 'vikunja' : model?.state === 'revoking' ? 'revoking' : service !== 'vikunja'
? left <= 0 ? left <= 0
? 'rotation-due' ? 'rotation-due'
: 'valid' : 'valid'
@@ -501,6 +681,7 @@ export class Credentials {
}); });
} }
close() { close() {
this.#modelProofs.clear(); this.#modelTokens = new WeakMap();
this.#tokens.clear(); this.#tokens.clear();
this.#refs.clear(); this.#refs.clear();
} }
+316
View File
@@ -0,0 +1,316 @@
// P2: one broker holds this lifecycle controller after Store acquired its writer lock.
// Every management-list read/modify/write is serialized. Only Credentials owns files.
import { randomBytes, createHash } from 'node:crypto';
import { performance } from 'node:perf_hooks';
import { isDeepStrictEqual } from 'node:util';
import { BusError } from './broker.mjs';
export const REVOKE_TIMEOUT_MS = 5000;
export const REVOKE_POLL_MS = 25;
export const MODEL_STOP_TIMEOUT_MS = 5000;
const keyId = key => createHash('sha256').update(key).digest('hex').slice(0, 12);
const fail = code => { throw new BusError(code); };
export class ModelKeys {
#credentials; #client; #baseUrl; #prefix; #receipt; #now; #runs; #stopRuns; #grace;
#tail = Promise.resolve(); #ready = false; #failure = 'proxy-down'; #confirmed = new Map();
constructor({ credentials, client, configDir, baseUrl, receipt = () => {}, now = () => Date.now(),
runs = () => [], stopRuns = async () => {}, rotateGraceMinutes = 120 }) {
this.#credentials = credentials; this.#client = client; this.#baseUrl = baseUrl;
this.#prefix = `msk-${keyId(configDir)}-`; this.#receipt = receipt; this.#now = now;
this.#runs = runs; this.#stopRuns = stopRuns; this.#grace = rotateGraceMinutes * 60_000;
}
#exclusive(operation) {
const result = this.#tail.then(operation);
this.#tail = result.catch(() => {});
return result;
}
#instances() { return this.#credentials.status().filter(s => s.service === 'model').map(s => s.instance); }
#record(instance) { return this.#credentials.modelRecord(instance); }
hasRecord(instance) { return this.#record(instance) !== null; }
assertLaunchable(instance) { this.#launchable(instance); }
#sentinel(keys) {
if (!keys.some(k => k.startsWith(this.#prefix + 'sentinel-') && /^[A-Za-z0-9_-]{43}$/.test(k.slice((this.#prefix + 'sentinel-').length)))) {
this.#ready = false; this.#failure = 'proxy-open'; fail('proxy-open');
}
}
#launchable(instance) {
if (this.#record(instance)?.state === 'revoking') fail('proxy-key-revoking');
if (!this.#ready) fail(this.#failure);
}
async #replace(before, after) {
this.#sentinel(before); this.#sentinel(after);
await this.#client.putKeys(after);
const confirmed = await this.#client.getKeys();
this.#sentinel(confirmed);
const removed = before.filter(k => !after.includes(k));
if (after.some(k => !confirmed.includes(k)) || removed.some(k => confirmed.includes(k))) fail('proxy-client-response');
}
async #pollRevoked(keys) {
const deadline = performance.now() + REVOKE_TIMEOUT_MS;
for (const key of keys) {
for (;;) {
const remaining = deadline - performance.now();
if (remaining <= 0) fail('credential-revoke-failed');
let status;
try { status = await this.#client.probeKey(key, { timeout: remaining }); }
catch { status = 0; }
if (status === 401) break;
const left = deadline - performance.now();
if (left <= 0) fail('credential-revoke-failed');
await new Promise(resolve => setTimeout(resolve, Math.min(REVOKE_POLL_MS, left)));
}
}
}
async #removeKeys(keys, confirm = () => {}) {
const before = await this.#client.getKeys();
confirm();
await this.#replace(before, before.filter(k => !keys.includes(k)));
await this.#pollRevoked(keys);
}
#captureRevocation(instance) {
const record = this.#record(instance);
if (record?.state !== 'revoking' || [record.key, ...record.retiring.map(r => r.key)].some(key => !key.startsWith(this.#prefix))) fail('credential-revoke-failed');
return { record, snapshot: this.#credentials.captureModelRevocation(instance) };
}
#confirmRevocation(instance, proof) {
if (!isDeepStrictEqual(this.#record(instance), proof.record)) fail('credential-revoke-failed');
// A prior tmp removal may leave only final at retry/restart. Neither its
// absence nor a revoking label proves exit. Fresh views grant NO stop authority.
if (this.#runs(instance).length) fail('proxy-stop-failed');
}
async reconcile() {
return this.#exclusive(async () => {
this.#ready = false;
try {
const before = await this.#client.getKeys(); this.#sentinel(before);
const records = this.#instances().map(instance => ({ instance, record: this.#record(instance) })).filter(e => e.record);
const retained = records.filter(e => e.record.state !== 'revoking').flatMap(e => [e.record.key, ...e.record.retiring.map(r => r.key)]);
const revoking = records.filter(e => e.record.state === 'revoking');
const proofs = new Map(revoking.map(({ instance }) => { this.#confirmed.delete(instance); return [instance, this.#captureRevocation(instance)]; }));
const confirm = () => { for (const [instance, proof] of proofs) this.#confirmRevocation(instance, proof); };
confirm();
const intended = revoking.flatMap(e => [e.record.key, ...e.record.retiring.map(r => r.key)]);
const removed = before.filter(k => k.startsWith(this.#prefix) && !k.startsWith(this.#prefix + 'sentinel-') && !retained.includes(k));
const after = [...new Set([...before.filter(k => !removed.includes(k) && !intended.includes(k)), ...retained])];
const added = after.filter(k => !before.includes(k));
if (added.length || removed.length || intended.length) await this.#replace(before, after);
await this.#pollRevoked([...new Set([...removed, ...intended])]);
confirm();
for (const { instance } of revoking) {
const proof = proofs.get(instance);
this.#credentials.prepareModelCompletion(instance, proof.snapshot);
this.#confirmed.set(instance, proof);
this.completeRevoke(instance);
}
this.#ready = true;
const receipt = { action: 'proxy.reconcile', addedKeyIds: added.map(keyId), removedKeyIds: [...new Set([...removed, ...intended])].map(keyId) };
this.#receipt(receipt); return receipt;
} catch (error) {
this.#failure = error.code === 'proxy-open' ? 'proxy-open' : 'proxy-down';
this.#receipt({ action: 'proxy.reconcile', code: this.#failure });
fail(this.#failure);
}
});
}
async #issue(instance, rotate = false, currentKeys = null) {
this.#launchable(instance); this.#credentials.registerModel(instance); this.#launchable(instance);
const old = this.#record(instance);
let before;
try { before = currentKeys ?? await this.#client.getKeys(); this.#sentinel(before); }
catch (error) { if (error.code === 'proxy-open') throw error; fail('proxy-key-issue-failed'); }
if (old && !rotate && before.includes(old.key)) return { baseUrl: this.#baseUrl, key: old.key };
const key = this.#prefix + randomBytes(32).toString('base64url');
const now = this.#now(), issuedAt = new Date(now).toISOString();
const date = new Date(now + 30 * 86400_000).toISOString().slice(0, 10);
const retiring = old ? [...old.retiring, ...(rotate && before.includes(old.key) ? [{ key: old.key, keyId: old.keyId, issuedAt: old.issuedAt, retireAt: new Date(now + this.#grace).toISOString() }] : [])] : [];
try {
await this.#replace(before, [...new Set([...before, key])]);
this.#credentials.writeModel(instance, { key, keyId: keyId(key), issuedAt, rotateBy: date, state: 'valid', retiring });
this.#receipt({ action: rotate ? 'proxy.key.rotated' : 'proxy.key.issued', keyIds: [keyId(key)], instance });
return { baseUrl: this.#baseUrl, key };
} catch (error) {
if (error.code === 'proxy-open') throw error;
fail('proxy-key-issue-failed');
}
}
ensure(instance) { return this.#exclusive(() => this.#issue(instance)); }
rotate(instance) { return this.#exclusive(() => this.#issue(instance, true)); }
// Host-only callbacks are the existing synchronous organization path.
hire(instance, check, publish) {
return this.#exclusive(async () => {
check(); // Fresh validation and captured revision AFTER controller queue.
const existed = this.hasRecord(instance);
try {
await this.#issue(instance);
check(); // Recheck captured revision AFTER HTTP, before publication.
return publish();
} catch (error) {
// A newly issued generation has not escaped to queued admission yet.
if (!existed && this.hasRecord(instance)) {
try { await this.#prepareRevoke(instance); this.completeRevoke(instance); } catch {}
}
throw error;
}
});
}
async preflight(instance) {
// The revoking check must run before even an issue/preflight request.
this.#launchable(instance);
const proxy = await this.#exclusive(async () => {
this.#launchable(instance);
let keys;
try { keys = await this.#client.getKeys(); } catch { fail('proxy-down'); }
this.#sentinel(keys);
return this.#issue(instance, false, keys);
});
let status;
try { status = await this.#client.probeKey(proxy.key); }
catch { fail('proxy-down'); }
if (status < 200 || status >= 300) fail('proxy-key-refused');
return proxy;
}
async #prepareRevoke(instance) {
const record = this.#record(instance);
if (!record) { if (this.#confirmed.has(instance)) fail('credential-revoke-failed'); return; }
this.#confirmed.delete(instance);
if (record.state !== 'revoking') this.#credentials.writeModel(instance, { ...record, state: 'revoking', revokingAt: new Date(this.#now()).toISOString() });
try {
const proof = this.#captureRevocation(instance), confirm = () => this.#confirmRevocation(instance, proof);
confirm();
await this.#removeKeys([record.key, ...record.retiring.map(r => r.key)], confirm);
confirm();
this.#credentials.prepareModelCompletion(instance, proof.snapshot);
this.#confirmed.set(instance, proof);
this.#receipt({ action: 'proxy.key.revoked', keyIds: [record.keyId, ...record.retiring.map(r => r.keyId)], remoteState: 'revoked', localState: 'kept' });
} catch { fail('credential-revoke-failed'); }
}
prepareRevoke(instance) { return this.#exclusive(() => this.#prepareRevoke(instance)); }
completeRevoke(instance) {
try {
const record = this.#record(instance), proof = this.#confirmed.get(instance);
if (!record && !proof) return; // Ordinary no-record/no-supplied-proof idempotence.
if (!record || !proof || record.state !== 'revoking') fail('credential-revoke-failed');
this.#confirmRevocation(instance, proof);
this.#credentials.finishModelRevocation(instance, proof.snapshot);
this.#confirmed.delete(instance);
} catch { fail('credential-revoke-failed'); }
}
revisionConflict(instance) {
const record = this.#record(instance);
this.#receipt({ action: 'proxy.key.revoke-conflict', keyIds: record ? [record.keyId] : [], remoteState: 'revoked', localState: 'kept' });
fail('proxy-revoke-revision-conflict');
}
sweep() {
return this.#exclusive(async () => {
let firstError;
for (const instance of this.#instances()) {
try {
let record = this.#record(instance);
if (!record || record.state === 'revoking') continue;
if (this.#now() >= Date.parse(record.rotateBy + 'T00:00:00Z')) {
await this.#issue(instance, true); record = this.#record(instance);
}
for (const old of record.retiring) {
const runs = this.#runs(instance).filter(r => r.keyId === old.keyId);
if (runs.length && this.#now() < Date.parse(old.retireAt)) continue;
if (runs.length) {
try { await this.#stopRuns(runs); } catch { fail('proxy-stop-failed'); }
}
if (this.#runs(instance).some(r => r.keyId === old.keyId)) fail('proxy-stop-failed');
const confirm = () => {
if (this.#runs(instance).some(r => r.keyId === old.keyId)) fail('proxy-stop-failed');
};
await this.#removeKeys([old.key], confirm);
confirm();
const current = this.#record(instance);
this.#credentials.writeModel(instance, { ...current, retiring: current.retiring.filter(r => r.keyId !== old.keyId) });
this.#receipt({ action: 'proxy.key.retired', keyIds: [old.keyId] });
}
} catch (error) {
firstError ??= error;
this.#receipt({ action: 'proxy.sweep', instance, outcome: 'refused', code: error.code ?? 'proxy-stop-failed' });
}
}
if (firstError) throw firstError;
});
}
}
// No new processing-control stop reason/schema: stopSession owns bounded SIGTERM
// and start-time verification. This layer additionally checks agent/key ownership.
export async function stopModelRuns({ runs, sessions, stop, receipt, reason = 'proxy-rotation', timeoutMs = MODEL_STOP_TIMEOUT_MS }) {
for (const run of runs) {
let timer;
const ids = typeof run.keyId === 'string' && /^[a-f0-9]{12}$/.test(run.keyId) ? [run.keyId] : [];
try {
if (!ids.length) fail('proxy-stop-failed');
const entry = sessions().find(s => s.business === run.business && s.run === run.run);
if (!entry || entry.instance !== run.role || entry.modelKeyId !== run.keyId
|| entry.pid !== run.pid || entry.startTime !== run.startTime) fail('proxy-stop-failed');
if (entry.live) {
const result = await Promise.race([
stop(run.run),
new Promise((_, reject) => { timer = setTimeout(() => reject(new BusError('proxy-stop-failed')), timeoutMs); }),
]);
if (!result?.stopped || sessions().some(s => s.business === run.business && s.run === run.run && s.live)) fail('proxy-stop-failed');
}
receipt({ action: reason, keyIds: ids, business: run.business, agent: run.role, run: run.run, outcome: 'stopped' });
} catch {
receipt({ action: reason, code: 'proxy-stop-failed', keyIds: ids, run: run.run });
fail('proxy-stop-failed');
} finally { clearTimeout(timer); }
}
}
// Option C: HTTP is outside the organization lock. This adapter preserves write's
// synchronous contract and passes the captured revision into its existing lock check.
// Neither organization.mjs nor its synchronous settings callers needs modification.
export function modelOrganization({ modelKeys, writer }) {
const pending = new Map();
const adapter = {
...writer,
write(business, document, options) {
const prepare = pending.get(business);
return writer.write(business, document, prepare ? { ...options, revision: prepare.revision } : options);
},
};
const revoke = original => (business, agent, how, services) => {
const prepare = pending.get(business), instance = business + '/' + agent;
if (!prepare || prepare.instance !== instance) fail('credential-revoke-failed');
// For an already-delisted repeat there is no write callback/lock, but the
// revision still must match before completing the model credential cleanup.
if (writer.read(business).revision !== prepare.revision) modelKeys.revisionConflict(instance);
modelKeys.completeRevoke(instance);
return original(business, agent, how, services);
};
const wrap = invoke => (business, verb, args, actor, context) => {
const isRevoke = verb === 'instance.fire' || (verb === 'instances.set' && args.op === 'delete');
if (!isRevoke && verb !== 'instance.hire') return invoke(business, verb, args, actor, context);
return (async () => {
if (pending.has(business)) fail('organization-busy');
const agent = args.name ?? args.instance, instance = business + '/' + agent;
if (verb === 'instance.fire') invoke(business, 'instance.fireCheck', { name: agent }, actor, context);
if (verb === 'instances.set') invoke(business, 'instances.check', { instance: agent }, actor, context);
const { revision } = writer.read(business);
if (args.revision !== undefined && args.revision !== revision) fail('revision-conflict');
const checkHire = () => {
if (writer.read(business).revision !== revision) fail('revision-conflict');
const checked = invoke(business, 'instance.hireCheck', args, actor, context);
if (checked.revision !== revision) fail('revision-conflict');
return checked;
};
if (!isRevoke) checkHire(); // No registration or proxy I/O before shared checks.
const prepare = { instance, revision, remoteRevoked: false };
pending.set(business, prepare);
const existed = modelKeys.hasRecord(instance);
try {
if (isRevoke) { await modelKeys.prepareRevoke(instance); prepare.remoteRevoked = existed; }
else return await modelKeys.hire(instance, checkHire, () => invoke(business, verb, args, actor, context));
return invoke(business, verb, args, actor, context);
} catch (error) {
if (isRevoke && prepare.remoteRevoked && (error.code === 'revision-conflict' || error.code === 'revision-conflict-revoked')) modelKeys.revisionConflict(instance);
throw error;
} finally { pending.delete(business); }
})();
};
return { writer: adapter, revoke, wrap };
}
+6
View File
@@ -7,7 +7,10 @@ const LAUNCH_OPS = new Set([
'authorizeLaunch', 'authorizeLaunch',
'refuse', 'refuse',
'endLaunch', 'endLaunch',
'recoverLaunch',
'credentialStatus', 'credentialStatus',
'modelPreflight',
'modelStall',
'checkPush', 'checkPush',
'recordPush', 'recordPush',
'reviewFetch', 'reviewFetch',
@@ -40,6 +43,7 @@ function launchOp(m) {
if (m.op === 'authorizeLaunch') return runtime.authorizeLaunch(m.cap, m.instance); if (m.op === 'authorizeLaunch') return runtime.authorizeLaunch(m.cap, m.instance);
if (m.op === 'refuse') return runtime.refuse(m.cap, m.code, m.subject); if (m.op === 'refuse') return runtime.refuse(m.cap, m.code, m.subject);
if (m.op === 'endLaunch') return runtime.endLaunch(m.record); if (m.op === 'endLaunch') return runtime.endLaunch(m.record);
if (m.op === 'recoverLaunch') return runtime.recoverLaunch(m.record);
// Row 93 (#1587): the git socket's ops. // Row 93 (#1587): the git socket's ops.
if (m.op === 'checkPush') return runtime.checkPush(m.cap, m.task_ref); if (m.op === 'checkPush') return runtime.checkPush(m.cap, m.task_ref);
if (m.op === 'recordPush') return runtime.recordPush(m.cap, m.push); if (m.op === 'recordPush') return runtime.recordPush(m.cap, m.push);
@@ -76,6 +80,8 @@ function launchOp(m) {
if (m.op === 'sessionClearRetire') return runtime.sessionClearRetire(m.record); if (m.op === 'sessionClearRetire') return runtime.sessionClearRetire(m.record);
if (m.op === 'sessionClearStart') return runtime.sessionClearStart(m.record); if (m.op === 'sessionClearStart') return runtime.sessionClearStart(m.record);
if (m.op === 'credentialStatus') return runtime.credentialStatus(m.business, m.role); if (m.op === 'credentialStatus') return runtime.credentialStatus(m.business, m.role);
if (m.op === 'modelPreflight') return runtime.modelPreflight(m);
if (m.op === 'modelStall') return runtime.modelStall(m);
// Every LAUNCH_OPS name has its line above. A missing one used to answer as credentialStatus, // Every LAUNCH_OPS name has its line above. A missing one used to answer as credentialStatus,
// whose [] for an unknown role read as "no holders" for landHolders (row 156 mutant P02). // whose [] for an unknown role read as "no holders" for landHolders (row 156 mutant P02).
throw new BusError('launch-op-refused'); throw new BusError('launch-op-refused');
@@ -0,0 +1,6 @@
// Pure broker/runner parsing vocabulary. No host management or state imports.
export const PROXY_PAGE_VERBS = Object.freeze(['proxy.accounts', 'proxy.login.start', 'proxy.login.submit',
'proxy.login.status', 'proxy.login.cancel', 'proxy.apikey.add', 'proxy.priority.set', 'proxy.keys']);
export const PROXY_PAGE_CODES = Object.freeze(['not-configured', 'human-required', 'provider-not-allowed',
'login-state-unknown', 'callback-url-invalid', 'apikey-invalid', 'priority-invalid', 'priority-unconfirmed',
'priority-write-refused', 'proxy-down', 'invalid-request']);
+176
View File
@@ -0,0 +1,176 @@
// P4: bus-only native Console operations. No upstream credential is persisted here.
import { randomBytes } from 'node:crypto';
import { sha256, refuse } from '../../model-proxy/src/common.mjs';
import { PROXY_PAGE_CODES } from './proxy-page-vocabulary.mjs';
export { PROXY_PAGE_VERBS, PROXY_PAGE_CODES } from './proxy-page-vocabulary.mjs';
const object = x => x !== null && typeof x === 'object' && !Array.isArray(x);
function fields(args, names) {
if (!object(args) || Object.keys(args).length !== names.length || names.some(k => !Object.hasOwn(args, k))) refuse('invalid-request');
}
const priorityOk = n => Number.isInteger(n) && n >= -1000 && n <= 1000;
const identifier = x => typeof x === 'string' && x.length > 0 && x.length <= 512;
const stateOk = x => typeof x === 'string' && /^[a-zA-Z0-9_-]{1,512}$/.test(x);
const keyId = value => sha256(value).slice(0, 12);
const ZAI_URL = 'https://api.z.ai/api/anthropic';
const providerName = provider => ({ claude: 'Claude', codex: 'Codex', 'z.ai': 'z.ai', other: 'Other' })[provider];
// Credential labels/emails and group names are arbitrary upstream strings. A stable
// generated display name provides identification without reflecting a misplaced token.
function view(id, provider, status, priority) {
return { accountId: keyId(id), provider, displayName: `${providerName(provider)} account ${keyId(id)}`,
status, priority: priorityOk(priority) ? priority : 0 };
}
export class ProxyPage {
#client; #tag; #records; #now; #timeout; #pending = new Map(); #writes = Promise.resolve();
constructor({ client, instanceTag, keyRecords = () => [], now = Date.now, loginTimeoutSeconds = 300 }) {
if (!client || !/^[a-zA-Z0-9_-]{1,64}$/.test(instanceTag) || typeof keyRecords !== 'function' || typeof now !== 'function'
|| !Number.isSafeInteger(loginTimeoutSeconds) || loginTimeoutSeconds <= 0) throw new TypeError('invalid proxy page settings');
this.#client = client; this.#tag = instanceTag; this.#records = keyRecords; this.#now = now; this.#timeout = loginTimeoutSeconds * 1000;
}
// Serializes read-modify-write provider config within the bus. P2 owns the
// independent client-key list. Store's lock admits only one bus per data root.
#write(operation) {
const next = this.#writes.then(operation); this.#writes = next.catch(() => {}); return next;
}
async #accounts() {
const response = await this.#client.getCredentials();
const groups = await this.#client.getUpstreamGroups();
if (!Array.isArray(response?.files) || !Array.isArray(groups)) refuse('proxy-down');
const rows = [];
for (const row of response.files) {
if (!identifier(row?.id) || !identifier(row?.name)) continue;
const provider = ['claude', 'codex'].includes(row.provider) ? row.provider : 'other';
const status = row.disabled === true ? 'disabled' : row.unavailable === true ? 'unavailable'
: ['ready', 'active', 'error', 'disabled', 'unavailable'].includes(row.status) ? row.status : 'unknown';
rows.push({ ...view(row.id, provider, status, row.priority), source: 'oauth', name: row.name });
}
for (const [groupIndex, group] of groups.entries()) {
if (!Array.isArray(group?.keys)) continue;
for (const [keyIndex, key] of group.keys.entries()) {
// auth_index is generated by the proxy. Never fall back to hashing api-key.
if (!identifier(key?.auth_index)) continue;
const provider = group['base-url'] === ZAI_URL ? 'z.ai' : 'other';
const status = key.disabled === true || group.disabled === true ? 'disabled' : 'active';
rows.push({ ...view(key.auth_index, provider, status, key.priority ?? group.priority), source: 'apikey', groupIndex, keyIndex });
}
}
return { rows, groups };
}
#public(row) {
const { accountId, provider, displayName, status, priority } = row;
return { accountId, provider, displayName, status, priority };
}
#session(state, scope, { reportTimeout = false } = {}) {
const entry = stateOk(state) ? this.#pending.get(state) : null;
if (!entry || entry.business !== scope.business || entry.human !== scope.human) refuse('login-state-unknown');
if (entry.expires <= this.#now()) {
this.#pending.delete(state);
if (reportTimeout) return null;
refuse('login-state-unknown');
}
return entry;
}
async request(verb, args, scope) {
try { return await this.#request(verb, args, scope); }
catch (error) {
// A client or fixture may put request values in its errors. Only locally
// enumerated refusal codes cross the broker, never a raw exception/body.
refuse(PROXY_PAGE_CODES.includes(error.code) ? error.code : 'proxy-down');
}
}
async #request(verb, args, scope) {
if (verb === 'proxy.accounts') {
fields(args, []);
return { accounts: (await this.#accounts()).rows.map(row => this.#public(row)) };
}
if (verb === 'proxy.login.start') {
fields(args, ['provider']);
if (!['claude', 'codex'].includes(args.provider)) refuse('provider-not-allowed');
for (const [state, value] of this.#pending) if (value.expires <= this.#now()) this.#pending.delete(state);
if (this.#pending.size >= 128) refuse('proxy-down');
const answer = await this.#client.oauthStart(args.provider);
let url; try { url = new URL(answer.url); } catch { refuse('proxy-down'); }
const host = args.provider === 'claude' ? 'claude.ai' : 'auth.openai.com';
if (url.protocol !== 'https:' || url.hostname !== host || url.username || url.password || url.port
|| url.href.length > 8192 || !stateOk(answer.state) || url.searchParams.getAll('state').length !== 1
|| url.searchParams.get('state') !== answer.state || this.#pending.has(answer.state)) refuse('proxy-down');
this.#pending.set(answer.state, { business: scope.business, human: scope.human, expires: this.#now() + this.#timeout });
return { url: url.href, state: answer.state };
}
if (verb === 'proxy.login.submit') {
fields(args, ['redirect_url']);
let url;
try {
if (typeof args.redirect_url !== 'string' || args.redirect_url.length > 8192) throw Error();
url = new URL(args.redirect_url);
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash || url.searchParams.getAll('state').length !== 1) throw Error();
} catch { refuse('callback-url-invalid'); }
const state = url.searchParams.get('state'); this.#session(state, scope);
await this.#client.oauthCallback(args.redirect_url);
// Callback acceptance is explicitly not proof of credential persistence.
return { status: 'wait' };
}
if (verb === 'proxy.login.status') {
fields(args, ['state']);
if (!this.#session(args.state, scope, { reportTimeout: true })) return { status: 'error', reason: 'login-timeout' };
const answer = await this.#client.oauthStatus(args.state);
if (answer?.status === 'wait') return { status: 'wait' };
if (!['ok', 'error'].includes(answer?.status)) refuse('proxy-down');
this.#pending.delete(args.state);
return answer.status === 'ok' ? { status: 'ok' } : { status: 'error', reason: 'login-failed' };
}
if (verb === 'proxy.login.cancel') {
fields(args, ['state']); this.#session(args.state, scope);
await this.#client.oauthCancel(args.state); this.#pending.delete(args.state);
return { status: 'cancelled' };
}
if (verb === 'proxy.apikey.add') {
fields(args, ['provider', 'key']);
if (args.provider !== 'z.ai') refuse('provider-not-allowed');
if (typeof args.key !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(args.key)) refuse('apikey-invalid');
return this.#write(async () => {
const groups = await this.#client.getUpstreamGroups();
if (!Array.isArray(groups)) refuse('proxy-down');
const group = { name: `mosaic-zai-${randomBytes(6).toString('hex')}`, 'base-url': ZAI_URL,
priority: 0, keys: [{ 'api-key': args.key }] };
await this.#client.putUpstreamGroups([...groups, group]);
return { added: true };
});
}
if (verb === 'proxy.priority.set') {
fields(args, ['accountId', 'priority']);
if (typeof args.accountId !== 'string' || !/^[a-f0-9]{12}$/.test(args.accountId) || !priorityOk(args.priority)) refuse('priority-invalid');
return this.#write(async () => {
const { rows, groups } = await this.#accounts();
const matches = rows.filter(row => row.accountId === args.accountId);
if (matches.length !== 1) refuse('priority-invalid');
const row = matches[0];
try {
if (row.source === 'oauth') await this.#client.patchCredential(row.name, args.priority);
else {
groups[row.groupIndex].keys[row.keyIndex].priority = args.priority;
await this.#client.putUpstreamGroups(groups);
}
} catch (error) {
refuse(error.code === 'proxy-client-refused' || error.code === 'proxy-client-redirect' ? 'priority-write-refused' : 'proxy-down');
}
const after = (await this.#accounts()).rows.find(r => r.accountId === args.accountId);
if (!after || after.priority !== args.priority) refuse('priority-unconfirmed');
return { accountId: row.accountId, priority: args.priority };
});
}
if (verb === 'proxy.keys') {
fields(args, []);
const values = await this.#client.getKeys();
if (!Array.isArray(values) || values.some(k => typeof k !== 'string')) refuse('proxy-down');
const records = await this.#records(scope.business);
const prefix = `msk-${this.#tag}-`;
return { keys: values.map(value => {
const id = keyId(value), sentinel = value.startsWith(prefix + 'sentinel-');
const record = records.find(r => r.keyId === id);
return { keyId: id, management: sentinel ? 'sentinel' : value.startsWith(prefix) ? 'stack-managed' : 'not stack-managed',
agent: record?.agent ?? null, issuedAt: record?.issuedAt ?? null, state: record?.state ?? null };
}) };
}
refuse('invalid-request');
}
}
+186 -10
View File
@@ -1,4 +1,4 @@
import { lstatSync, readdirSync } from 'node:fs'; import { lstatSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { Store } from './store.mjs'; import { Store } from './store.mjs';
@@ -8,13 +8,16 @@ import { Broker, BusError, LIBRARY_FETCH_FIX } from './broker.mjs';
import { Credentials } from './credentials.mjs'; import { Credentials } from './credentials.mjs';
import { serve } from './server.mjs'; import { serve } from './server.mjs';
import { launchSettings } from '../../business/src/launch-settings.mjs'; import { launchSettings } from '../../business/src/launch-settings.mjs';
import { readSessions } from '../../seat/src/session.mjs'; import { readSessions, sessionsFile, stopSession } from '../../seat/src/session.mjs';
import { compactionCount, storeDir } from '../../seat/src/compactions.mjs'; import { compactionCount, storeDir } from '../../seat/src/compactions.mjs';
import { publishRemote, workingRepo, WorkingError } from '../../seat/src/working.mjs'; import { publishRemote, workingRepo, WorkingError } from '../../seat/src/working.mjs';
import { createLoader, hasModule } from '../../plugins/src/loader.mjs'; import { createLoader, hasModule } from '../../plugins/src/loader.mjs';
import { pluginSettings } from '../../business/src/plugin-settings.mjs'; import { pluginSettings } from '../../business/src/plugin-settings.mjs';
import { loadManagementClient } from '../../model-proxy/src/client.mjs'; import { loadManagementClient } from '../../model-proxy/src/client.mjs';
import { ProxyPage } from './proxy-page.mjs';
import { sha256 } from '../../model-proxy/src/common.mjs';
import { initializeProxy } from '../../model-proxy/src/settings.mjs'; import { initializeProxy } from '../../model-proxy/src/settings.mjs';
import { ModelKeys, modelOrganization, stopModelRuns, MODEL_STOP_TIMEOUT_MS } from './model-keys.mjs';
import { instanceSettings } from '../../business/src/instance-settings.mjs'; import { instanceSettings } from '../../business/src/instance-settings.mjs';
import { organizationWriter, OrganizationError, setReplyLimits } from '../../business/src/organization.mjs'; import { organizationWriter, OrganizationError, setReplyLimits } from '../../business/src/organization.mjs';
import { credentialVerbs } from './credential-verbs.mjs'; import { credentialVerbs } from './credential-verbs.mjs';
@@ -121,6 +124,110 @@ export function createLibraryLookup({ dataRoot, sync, now = () => Date.now() })
}; };
} }
// B1: discovery is evidence of a process, never authority to stop it. Keep this
// host-only inventory across the launcher's Registry[] recovery gap and endLaunch.
// A failed boot read cannot be repaired by a later empty registry: that would lose
// the very prior runs whose existence this inventory must establish.
function modelRunInventory(dataRoot, bound, credentials) {
const known = new Map(), terminal = new Map(), validated = new Map();
const refuse = () => { throw new BusError('proxy-stop-failed'); };
const same = (a, b) => ['business', 'role', 'run', 'pid', 'startTime', 'runnerPid', 'runnerStartTime', 'keyId'].every(k => a[k] === b[k]);
const shape = s => {
if (!s || ['business', 'instance', 'run'].some(k => typeof s[k] !== 'string' || !s[k])
|| ['pid', 'runnerPid'].some(k => !Number.isSafeInteger(s[k]) || s[k] < 2)
|| ['startTime', 'runnerStartTime'].some(k => typeof s[k] !== 'string' || !/^\d+$/.test(s[k]))
|| (s.modelKeyId !== undefined && (typeof s.modelKeyId !== 'string' || !/^[a-f0-9]{12}$/.test(s.modelKeyId)))) refuse();
return { business: s.business, role: s.instance, instance: s.business + '/' + s.instance,
run: s.run, pid: s.pid, startTime: s.startTime, runnerPid: s.runnerPid,
runnerStartTime: s.runnerStartTime, keyId: s.modelKeyId };
};
const refresh = () => {
const file = sessionsFile(dataRoot), stat = lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink()) refuse();
const entries = readSessions(dataRoot).map(shape), seen = new Set();
const after = lstatSync(file);
if (!['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs'].every(k => stat[k] === after[k])) refuse();
// Validate the whole snapshot before merging any of it. Missing entries do
// not erase known identities; the host may have just rewritten Registry[].
for (const r of entries) {
const key = r.business + ':' + r.run;
if (seen.has(key) || (known.has(key) && !same(known.get(key), r))) refuse();
seen.add(key);
}
for (const r of entries) known.set(r.business + ':' + r.run, r);
};
let bootChecked = false;
try { refresh(); bootChecked = true; } catch { /* hold retirement, not the human/control surface */ }
// startTimeOf/live alone conflate absence and read errors. Only a missing proc
// directory, or a zombie with the SAME recorded start ticks, confirms an exit.
const liveIdentity = (pid, ticks, proof, part) => {
if (proof[part]) return false;
let raw;
try { raw = readFileSync(`/proc/${pid}/stat`, 'utf8'); }
catch (error) {
if (error.code !== 'ENOENT') refuse();
try { lstatSync(`/proc/${pid}`); } catch (missing) { if (missing.code === 'ENOENT') { proof[part] = true; return false; } }
refuse();
}
const fields = raw.slice(raw.lastIndexOf(')') + 2).trim().split(/\s+/);
if (!/^[A-Z]$/.test(fields[0]) || !/^\d+$/.test(fields[19]) || fields[19] !== ticks) refuse();
if (fields[0] === 'Z') { proof[part] = true; return false; }
return true;
};
const runs = instance => {
try {
if (!bootChecked) refuse();
refresh();
const live = [], checked = [];
for (const r of known.values()) {
if (r.instance !== instance) continue;
const key = r.business + ':' + r.run;
const proof = terminal.get(key) ?? { outer: false, runner: false };
terminal.set(key, proof);
let associated = false;
if (!proof.outer || !proof.runner) {
const record = credentials.modelRecord(instance);
associated = !!record && [record.keyId, ...record.retiring.map(k => k.keyId)].includes(r.keyId);
const prior = validated.get(key);
if (!associated && (!prior || !same(prior, r))) refuse();
}
const outerLive = liveIdentity(r.pid, r.startTime, proof, 'outer'), runnerLive = liveIdentity(r.runnerPid, r.runnerStartTime, proof, 'runner');
// Never interpret an unknown/wrong key on a surviving prior run as no
// old-key users. A confirmed exit may be retired without key guessing.
if (outerLive || runnerLive) {
// Provenance permits strict exit confirmation, NEVER filtering away a
// surviving run whose model-key association has disappeared.
if (!associated) refuse();
const current = bound.get(r.business + ':' + r.run);
if (current && (current.instance !== r.instance || current.keyId !== r.keyId
|| current.pid !== r.pid || current.startTime !== r.startTime)) refuse();
live.push(r);
}
if (associated && (outerLive || runnerLive)) checked.push(r);
}
// A newly bound launch whose registry publication has not happened is
// uncertain, not absent. Do not grant retirement through that startup gap.
for (const r of bound.values()) if (r.instance === instance && !known.has(r.business + ':' + r.run)) refuse();
// Commit ONLY after the entire same-instance view passed both identities,
// live binding consistency, later known entries and unpublished bindings.
// Existing M1 terminal bits remain independently monotonic and unchanged.
for (const r of checked) validated.set(r.business + ':' + r.run, { ...r });
return live;
} catch { refuse(); }
};
return {
runs,
authorizeStop(requests) {
for (const r of requests) {
const current = bound.get(r.business + ':' + r.run);
if (!current || current.instance !== r.instance || current.keyId !== r.keyId
|| current.pid !== r.pid || current.startTime !== r.startTime) refuse();
if (!runs(r.instance).some(found => same(found, r))) refuse();
}
},
};
}
export async function startBroker({ export async function startBroker({
dataRoot, dataRoot,
businesses, businesses,
@@ -158,7 +265,9 @@ export async function startBroker({
|| (Object.hasOwn(librarySync, 'intervalMs') && !(Number.isSafeInteger(librarySync.intervalMs) && librarySync.intervalMs > 0)) || (Object.hasOwn(librarySync, 'intervalMs') && !(Number.isSafeInteger(librarySync.intervalMs) && librarySync.intervalMs > 0))
|| (Object.hasOwn(librarySync, 'configDir') && typeof librarySync.configDir !== 'string'))) || (Object.hasOwn(librarySync, 'configDir') && typeof librarySync.configDir !== 'string')))
throw new BusError('invalid-host-request'); throw new BusError('invalid-host-request');
let modelProxyClient = null; let modelProxyClient = null, proxyPage = null;
let modelKeys = null, modelTimer = null, modelSweep = null;
const modelRuns = new Map();
let store, credentials, server, adapter, processing, syncTimer = null, syncPass = null, accessTimer = null; let store, credentials, server, adapter, processing, syncTimer = null, syncPass = null, accessTimer = null;
try { try {
const proxyReceipt = entry => process.stderr.write(`model-proxy: ${JSON.stringify(entry)}\n`); const proxyReceipt = entry => process.stderr.write(`model-proxy: ${JSON.stringify(entry)}\n`);
@@ -198,7 +307,34 @@ export async function startBroker({
// Row 236 (#1742, #436b r5 section 5): each gitea grant record gives its agent a store reference to // Row 236 (#1742, #436b r5 section 5): each gitea grant record gives its agent a store reference to
// gitea, and `held` names the files memory must not load until reconcile or a revoke. // gitea, and `held` names the files memory must not load until reconcile or a revoke.
const { held } = storeRoot ? giteaStart(store, storeRoot, businesses, references) : { held: [] }; const { held } = storeRoot ? giteaStart(store, storeRoot, businesses, references) : { held: [] };
credentials = new Credentials({ references, repoRoots: protectedRoots, dataRoot, storeRoot, held }); credentials = new Credentials({ references, repoRoots: protectedRoots, dataRoot, storeRoot, held,
modelAgents: proxy ? Object.entries(businesses).flatMap(([id, b]) => Object.keys(b.roles).map(role => id + '/' + role)) : [] });
if (proxy) {
const inventory = modelRunInventory(dataRoot, modelRuns, credentials);
modelKeys = new ModelKeys({ credentials, client: modelProxyClient, configDir: organization.dir,
baseUrl: `http://127.0.0.1:${proxy.port}`, receipt: proxyReceipt, rotateGraceMinutes: proxy.rotateGraceMinutes ?? 120,
runs: inventory.runs,
stopRuns: async runs => {
inventory.authorizeStop(runs);
await stopModelRuns({ runs, sessions: () => readSessions(dataRoot), stop: run => stopSession(dataRoot, run, { timeoutMs: MODEL_STOP_TIMEOUT_MS }), receipt: proxyReceipt });
for (const r of runs) {
if (inventory.runs(r.instance).some(found => found.run === r.run)) throw new BusError('proxy-stop-failed');
}
for (const r of runs) modelRuns.delete(r.business + ':' + r.run);
} });
// Keep the control/human surface available, but refuse every launch on failed reconcile.
try { await modelKeys.reconcile(); } catch (error) { proxyReceipt({ action: 'proxy.boot-refused', code: error.code }); }
}
const configureProxyPage = () => {
proxyPage = new ProxyPage({ client: modelProxyClient, instanceTag: sha256(organization.dir).slice(0, 12),
// P2 supplies the model metadata; only the matching business's record
// participates in this owner view. No credential value is read here.
keyRecords: business => credentials.status().filter(r => r.service === 'model' && r.instance?.startsWith(business + '/'))
.map(r => ({ keyId: r.keyId, agent: r.instance.slice(business.length + 1), issuedAt: r.issuedAt, state: r.state })),
});
};
if (modelProxyClient) configureProxyPage();
// vikunja:19/305: the plug-in switch through the same human-only path as the limits: the verbs // vikunja:19/305: the plug-in switch through the same human-only path as the limits: the verbs
// plugins and plugins.set, logged in changes.jsonl with the actor, a missing module refused. // plugins and plugins.set, logged in changes.jsonl with the actor, a missing module refused.
const plugins=organization?pluginSettings({dir:organization.dir,rolesDir:organization.rolesDir,exists:hasModule}):null; const plugins=organization?pluginSettings({dir:organization.dir,rolesDir:organization.rolesDir,exists:hasModule}):null;
@@ -240,7 +376,9 @@ export async function startBroker({
return { revoked: services, outsideStore: how === 'fire' ? outside : [] }; return { revoked: services, outsideStore: how === 'fire' ? outside : [] };
} }
// Row 165 (#1662): the human adds and changes role instances through instances and instances.set. // Row 165 (#1662): the human adds and changes role instances through instances and instances.set.
const instances=organization?instanceSettings({dir:organization.dir,rolesDir:organization.rolesDir,system:organization.system,levels,revoke}):null; const proxyOrganization = modelKeys ? modelOrganization({ modelKeys, writer: organizationWriter({ dir: organization.dir, rolesDir: organization.rolesDir }) }) : null;
const instances=organization?instanceSettings({dir:organization.dir,rolesDir:organization.rolesDir,system:organization.system,levels,revoke,
...(proxyOrganization ? { writer: proxyOrganization.writer, revoke: proxyOrganization.revoke(revoke) } : {})}):null;
const usage = await createUsage({ store, dataRoot, businesses, const usage = await createUsage({ store, dataRoot, businesses,
log: entry => process.stderr.write(`usage: ${JSON.stringify(entry)}\n`) }); log: entry => process.stderr.write(`usage: ${JSON.stringify(entry)}\n`) });
// Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): the limit checks read the organization file at // Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): the limit checks read the organization file at
@@ -393,7 +531,7 @@ export async function startBroker({
}, },
// limits and limits.set (organization.set is limits.set) carry each token limit's count; a // limits and limits.set (organization.set is limits.set) carry each token limit's count; a
// plugins.set reloads the business's limit checks in the background. // plugins.set reloads the business's limit checks in the background.
organization:settings?(id,verb,args,actor,context)=>{ organization:settings?(proxyOrganization?.wrap ?? (operation => operation))((id,verb,args,actor,context)=>{
if(verb==='limits')return withTokens(id,settings.read(id)); if(verb==='limits')return withTokens(id,settings.read(id));
if(verb==='plugins')return plugins.read(id); if(verb==='plugins')return plugins.read(id);
// Row 256 (#1761): the actor is the reader, so endProtected is the delete check for that reader. // Row 256 (#1761): the actor is the reader, so endProtected is the delete check for that reader.
@@ -406,6 +544,8 @@ export async function startBroker({
if(verb==='instances.check')return instances.checkDelete(id,args.instance,actor); if(verb==='instances.check')return instances.checkDelete(id,args.instance,actor);
// Row 194 b6 (webby-0094 B1): a fire's checks against the file, before step A. // Row 194 b6 (webby-0094 B1): a fire's checks against the file, before step A.
if(verb==='instance.fireCheck')return instances.checkFire(id,args.name); if(verb==='instance.fireCheck')return instances.checkFire(id,args.name);
// Internal read-only path: no reload, journal, credential or publication.
if(verb==='instance.hireCheck')return instances.checkHire(id,args);
let result; let result;
if(verb==='limits.set')result=withTokens(id,settings.set(id,args,actor)); if(verb==='limits.set')result=withTokens(id,settings.set(id,args,actor));
else if(verb==='instances.set')result=instances.set(id,args,actor); else if(verb==='instances.set')result=instances.set(id,args,actor);
@@ -428,16 +568,21 @@ export async function startBroker({
// took, so a refused one (credentials, roots, ...) shows the restart line for a keep or a // took, so a refused one (credentials, roots, ...) shows the restart line for a keep or a
// hire too. The other writers' answers carry no restart field. // hire too. The other writers' answers carry no restart field.
return INSTANCE_VERBS.has(verb)?{...result,restart:!live}:result; return INSTANCE_VERBS.has(verb)?{...result,restart:!live}:result;
}:null, }):null,
// Row 194 (D2.3): the role files as they stand, for the hire checks. // Row 194 (D2.3): the role files as they stand, for the hire checks.
roleFile: organization ? (name) => loadRole(organization.rolesDir, name) : null, roleFile: organization ? (name) => loadRole(organization.rolesDir, name) : null,
secretCheck: (value) => credentials.assertClean(value), secretCheck: (value) => credentials.assertClean(value),
credentialStatus: () => credentials.status(), credentialStatus: () => credentials.status(),
// Row 199 (#1697, design section 6): issue, rotate, revoke and delete, live with no restart; each // Row 199 (#1697, design section 6): issue, rotate, revoke and delete, live with no restart; each
// rewrites or records through the organization writer, so changes.jsonl holds the receipt. // rewrites or records through the organization writer, so changes.jsonl holds the receipt.
proxyPage: async (verb, args, scope) => {
if (!proxyPage) throw new BusError('not-configured');
return proxyPage.request(verb, args, scope);
},
proxyInit: organization ? async args => { proxyInit: organization ? async args => {
const settings = await initializeProxy({ configDir: organization.dir, dataRoot, ...args }); const settings = await initializeProxy({ configDir: organization.dir, dataRoot, ...args });
modelProxyClient = await loadManagementClient({ configDir: organization.dir, proxy: settings, receipt: proxyReceipt }); modelProxyClient = await loadManagementClient({ configDir: organization.dir, proxy: settings, receipt: proxyReceipt });
configureProxyPage();
return settings; return settings;
} : null, } : null,
credentialVerbs: organization credentialVerbs: organization
@@ -487,8 +632,15 @@ export async function startBroker({
!/^\d+$/.test(r.startTime) !/^\d+$/.test(r.startTime)
) )
throw new BusError('invalid-launch-process'); throw new BusError('invalid-launch-process');
const cap = broker.bindLaunch(r); const { modelKeyId, ...record } = r;
if (modelKeys) {
modelKeys.assertLaunchable(r.business + '/' + r.role);
const m = credentials.modelRecord(r.business + '/' + r.role);
if (!m || ![m.keyId, ...m.retiring.map(k => k.keyId)].includes(modelKeyId)) throw new BusError('proxy-key-refused');
}
const cap = broker.bindLaunch(record);
launchRecords.push({ ...r }); launchRecords.push({ ...r });
if (modelKeys) modelRuns.set(r.business + ':' + r.run, { ...r, instance: r.business + '/' + r.role, keyId: modelKeyId });
return { business: r.business, run: r.run, cap }; return { business: r.business, run: r.run, cap };
} }
const bound = launches.map(bindLaunch); const bound = launches.map(bindLaunch);
@@ -649,7 +801,7 @@ export async function startBroker({
return credentials return credentials
.status() .status()
.filter((c) => c.instance === business + '/' + role) .filter((c) => c.instance === business + '/' + role)
.map(({ service, date, state }) => ({ service, date, state })); .map(({ service, date, state, keyId, issuedAt }) => ({ service, date, state, ...(service === 'model' ? { keyId, issuedAt } : {}) }));
} }
// C2-03a: each library entry with a sync user fetches once at start, then every 60 s. A pass // C2-03a: each library entry with a sync user fetches once at start, then every 60 s. A pass
// still running skips the tick. Only codes are logged, never git's output. // still running skips the tick. Only codes are logged, never git's output.
@@ -694,6 +846,13 @@ export async function startBroker({
}, access.intervalMs ?? ACCESS_SWEEP_MS); }, access.intervalMs ?? ACCESS_SWEEP_MS);
accessTimer.unref(); accessTimer.unref();
} }
if (modelKeys) {
modelTimer = setInterval(() => {
if (modelSweep) return;
modelSweep = modelKeys.sweep().catch(error => proxyReceipt({ action: 'proxy.sweep-refused', code: error.code ?? 'proxy-key-issue-failed' })).finally(() => { modelSweep = null; });
}, 1000);
modelTimer.unref();
}
return { return {
broker, broker,
credentials, credentials,
@@ -701,7 +860,21 @@ export async function startBroker({
get modelProxy() { return modelProxyClient; }, get modelProxy() { return modelProxyClient; },
path, path,
bindLaunch, bindLaunch,
endLaunch: (r) => broker.endLaunch(r), recoverLaunch: ({ modelKeyId, ...record }) => broker.recoverLaunch(record),
endLaunch: (r) => { const out = broker.endLaunch(r); modelRuns.delete(r.business + ':' + r.run); return out; },
modelPreflight: async ({ business, role }) => {
const current = settings ? settings.current(business) : businesses[business];
if (!Object.hasOwn(current?.roles ?? {}, role)) throw new BusError('unknown-role');
return modelKeys ? modelKeys.preflight(business + '/' + role) : null;
},
modelStall: async ({ business, role, run, keyId }) => {
const record = modelRuns.get(business + ':' + run);
if (!record || record.role !== role || record.keyId !== keyId) throw new BusError('proxy-stop-failed');
await stopModelRuns({ runs: [record], reason: 'proxy-stall', sessions: () => readSessions(dataRoot),
stop: id => stopSession(dataRoot, id, { timeoutMs: MODEL_STOP_TIMEOUT_MS }), receipt: proxyReceipt });
modelRuns.delete(business + ':' + run);
return { stopped: true };
},
threadsState: (business) => broker.threadsState(business), threadsState: (business) => broker.threadsState(business),
threadIdle: (r) => broker.threadIdle(r), threadIdle: (r) => broker.threadIdle(r),
// Row 125 (#1619, REQ-LIMIT-4): the launcher asks the limit checks before every launch. // Row 125 (#1619, REQ-LIMIT-4): the launcher asks the limit checks before every launch.
@@ -743,6 +916,8 @@ export async function startBroker({
launches: bound, launches: bound,
readers: readCaps, readers: readCaps,
async close() { async close() {
clearInterval(modelTimer);
await modelSweep;
clearInterval(accessTimer); clearInterval(accessTimer);
await stopSync(); await stopSync();
await server.close(); await server.close();
@@ -753,6 +928,7 @@ export async function startBroker({
}, },
}; };
} catch (e) { } catch (e) {
clearInterval(modelTimer);
clearInterval(accessTimer); clearInterval(accessTimer);
await server?.close(); await server?.close();
await adapter?.close?.(); await adapter?.close?.();
@@ -0,0 +1,67 @@
// #656/1745 STATIC-only authored tests; no measured outcome until separately cleared.
// Generic fallback retains its current behavior; absent cache is NOT a universal refusal.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { dirname, join } from 'node:path';
import { revocationFixture, outcome, id } from './model-revocation-fixture.mjs';
import { Credentials } from '../src/credentials.mjs';
const timeout=30000;
async function prepared(f){f.seed({matchingTmp:false});await f.prepare();}
async function finished(f){await prepared(f);f.complete();}
function proof(f){assert.equal(f.proofs.length,1,'real controller minted exact opaque model completion proof');return f.proofs[0];}
test('656 generic remove after exact completion skips only redundant model unlink and keeps metadata',{timeout},async t=>{
const f=await revocationFixture(t);await finished(f);const before=f.credentials.status(),count=f.counts.unlinkFinal;
const result=f.credentials.remove(f.instance);assert.equal(f.counts.unlinkFinal,count,'generic remove does not perform SECOND model unlink after exact completion');assert.deepEqual(result.services,['model']);assert.deepEqual(f.credentials.status(),before,'reference date and revoked status metadata unchanged');assert.equal(f.exists(f.file)||f.exists(f.temporary),false);f.valueFree();
});
for(const entry of ['model','model.tmp'])test('656 '+entry+' appearing after completed proof is preserved by unchanged unknown fence',{timeout},async t=>{
const f=await revocationFixture(t);await finished(f);const file=join(dirname(f.file),entry),body='owned unrecognized\n';fs.writeFileSync(file,body,{mode:0o600,flag:'wx'});const count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.remove(f.instance));
assert.equal(f.counts.unlinkFinal,count,'finished proof never authorizes another model pathname unlink');assert.equal(result.ok,false);assert.equal(result.code,'credential-file');assert.equal(id(fs.readFileSync(file)),id(body),'postcompletion unknown entry preserved');f.valueFree();
});
for(const state of ['valid','revoking'])test('656 '+state+' cached unconfirmed model keeps existing generic removal guard',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false,valid:state==='valid'});const before=id(fs.readFileSync(f.file)),count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.remove(f.instance));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);assert.equal(id(fs.readFileSync(f.file)),before);assert.equal(f.record()?.state,state);f.valueFree();
});
test('656 unconfirmed prepared model still cannot use finished-proof skip',{timeout},async t=>{
const f=await revocationFixture(t);await prepared(f);const count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.remove(f.instance));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);assert.equal(f.record()?.state,'revoking');f.valueFree();
});
test('656 no-proof absent-cache model retains existing generic file removal behavior',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});f.credentials.hold(f.instance,'model');const count=f.counts.unlinkFinal,result=f.credentials.remove(f.instance);assert.deepEqual(result.services,['model']);assert.equal(f.counts.unlinkFinal-count,1,'ordinary generic no-cache behavior remains allowed');assert.equal(f.exists(f.file),false);f.valueFree();
});
test('656 no-record no-proof generic remove retains existing ENOENT behavior',{timeout},async t=>{
const f=await revocationFixture(t),count=f.counts.unlinkFinal,result=f.credentials.remove(f.instance);assert.deepEqual(result.services,['model']);assert.equal(f.counts.unlinkFinal-count,1,'no proof does not invent completed-proof skip');f.valueFree();
});
for(const action of ['register','unregister','reload','hold','claim','revoke','rewrite','close'])test('656 '+action+' invalidates latest completed proof without granting fallback authority',{timeout},async t=>{
const f=await revocationFixture(t);await finished(f);const token=proof(f),c=f.credentials;let signal;
if(action==='register')c.register(f.instance,'model',{store:true,file:f.file,rotateBy:'2099-01-01'});
if(action==='unregister')c.unregister(f.instance,'model');
if(action==='reload'){const result=await outcome(()=>c.reload(f.instance,'model','2099-01-01'));assert.equal(result.ok,false);assert.equal(result.code,'credential-file');}
if(action==='hold')c.hold(f.instance,'model');
if(action==='claim')signal=c.claim(f.instance,'model');
if(action==='revoke')c.revoke(f.instance,'model');
if(action==='rewrite')f.seed({matchingTmp:false,valid:true});
if(action==='close')c.close();
const result=await outcome(()=>c.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');if(signal)c.release(f.instance,'model',signal);if(action==='rewrite')assert.equal(f.exists(f.file),true);f.valueFree();
});
test('656 new snapshot invalidates old token even if later capture refuses on absent final',{timeout},async t=>{
const f=await revocationFixture(t);await finished(f);const token=proof(f),capture=await outcome(()=>f.credentials.captureModelRevocation(f.instance));assert.equal(capture.ok,false);assert.equal(capture.code,'credential-revoke-failed');const result=await outcome(()=>f.credentials.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');f.valueFree();
});
test('656 wrong instance cannot reuse a completed token',{timeout},async t=>{
const f=await revocationFixture(t);await finished(f);const token=proof(f),count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.finishModelRevocation('demo/other',token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);f.credentials.finishModelRevocation(f.instance,token);assert.equal(f.counts.unlinkFinal,count);f.valueFree();
});
test('656 nonmodel memory-first failure and retry remain unchanged after model completion',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});const file=join(dirname(f.file),'gitea'),value='fixture-gitea-key';f.remember(value);f.credentials.register(f.instance,'gitea',{store:true,file,rotateBy:'2099-01-01'});f.credentials.issue(f.instance,'gitea',value,'2099-01-01');await f.prepare();f.complete();const count=f.counts.unlinkFinal;f.faults.unlinkService=true;
const failure=await outcome(()=>f.credentials.remove(f.instance));assert.equal(f.counts.unlinkFinal,count,'nonmodel failure does not repeat model unlink');assert.equal(failure.ok,false);assert.equal(failure.code,'credential-file');assert.equal(f.credentials.status().find(s=>s.instance===f.instance&&s.service==='gitea').state,'revoked','nonmodel keeps memory-first semantics');assert.equal(f.exists(file),true,'failed nonmodel unlink remains honestly present');f.faults.unlinkService=false;
const result=f.credentials.remove(f.instance);assert.deepEqual([...result.services].sort(),['gitea','model']);assert.equal(f.counts.unlinkFinal,count);assert.equal(f.counts.unlinkService,2,'one failed then one successful service unlink; model remains completed');f.valueFree();
});
test('656 replaced private agent directory cannot reuse exact historical completion proof',{timeout},async t=>{
const f=await revocationFixture(t);await finished(f);const token=proof(f),directory=dirname(f.file);fs.renameSync(directory,join(f.root,'former-owned-agent-dir'));fs.mkdirSync(directory,{mode:0o700});const count=f.counts.unlinkFinal,result=await outcome(()=>f.credentials.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);f.valueFree();
});
test('656 another Credentials epoch cannot use a former objects completed token',{timeout},async t=>{
const f=await revocationFixture(t);await finished(f);const token=proof(f);let other;t.after(()=>other?.close());other=new Credentials({storeRoot:f.storeRoot,dataRoot:f.dataRoot,modelAgents:[f.instance]});const count=f.counts.unlinkFinal,result=await outcome(()=>other.finishModelRevocation(f.instance,token));assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');assert.equal(f.counts.unlinkFinal,count);f.credentials.finishModelRevocation(f.instance,token);assert.equal(f.counts.unlinkFinal,count);f.valueFree();
});
@@ -0,0 +1,76 @@
// P2 RED: model is implicit, durable/private, and status never exposes current/retiring values.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomBytes, createHash } from 'node:crypto';
import { Credentials } from '../src/credentials.mjs';
function fixture(t) {
const root = fs.mkdtempSync(join(tmpdir(), 'p2-store-'));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const storeRoot = join(root, 'config', 'secrets', 'agents');
fs.mkdirSync(storeRoot, { recursive: true, mode: 0o700 });
const open = () => new Credentials({ storeRoot, dataRoot: join(root, 'data'), modelAgents: ['demo/coder'] });
const file = join(storeRoot, 'demo', 'coder', 'model');
return { root, storeRoot, open, file };
}
const key = () => 'msk-fixture-' + randomBytes(32).toString('base64url');
const id = value => createHash('sha256').update(value).digest('hex').slice(0, 12);
const record = value => ({ key: value, keyId: id(value), issuedAt: '2026-10-24T00:00:00.000Z', rotateBy: '2026-11-23', state: 'valid', retiring: [] });
test('implicit model kind starts not-issued without a business or role reference', t => {
const f = fixture(t), c = f.open(); t.after(() => c.close());
const status = c.status().find(s => s.instance === 'demo/coder' && s.service === 'model');
assert.equal(status?.state, 'not-issued');
assert.equal(fs.existsSync(f.file), false);
});
test('model record persists at 0600 in 0700 dirs and reloads ids/issue time/state only in status', t => {
const f = fixture(t), c = f.open(), value = key(); t.after(() => c.close());
assert.equal(typeof c.writeModel, 'function');
c.writeModel('demo/coder', record(value));
assert.equal(fs.statSync(f.file).mode & 0o777, 0o600);
for (const dir of [f.storeRoot, join(f.storeRoot, 'demo'), join(f.storeRoot, 'demo', 'coder')]) assert.equal(fs.statSync(dir).mode & 0o777, 0o700);
c.close();
const again = f.open(); t.after(() => again.close());
const status = again.status().find(s => s.service === 'model');
assert.equal(status.keyId === id(value), true);
assert.equal(status.issuedAt, '2026-10-24T00:00:00.000Z');
assert.equal(status.state, 'valid');
assert.equal(JSON.stringify(again.status()).includes(value), false);
assert.equal(again.modelRecord('demo/coder').key === value, true);
});
test('revoking model metadata survives restart without losing the private record', t => {
const f = fixture(t), c = f.open(), value = key(); t.after(() => c.close());
assert.equal(typeof c.writeModel, 'function');
c.writeModel('demo/coder', { ...record(value), state: 'revoking', revokingAt: '2026-10-24T01:00:00.000Z' });
c.close(); const again = f.open(); t.after(() => again.close());
assert.equal(again.status().find(s => s.service === 'model').state, 'revoking');
assert.equal(again.modelRecord('demo/coder').key === value, true);
assert.equal(JSON.stringify(again.status()).includes(value), false);
});
test('model record wrong file mode refuses startup rather than inventing a replacement', t => {
const f = fixture(t), c = f.open(); t.after(() => c.close());
assert.equal(typeof c.writeModel, 'function'); c.writeModel('demo/coder', record(key())); c.close();
fs.chmodSync(f.file, 0o640);
assert.throws(() => f.open(), e => e.code === 'credential-file');
});
test('non-proxy Credentials has no implicit model kind', t => {
const f = fixture(t), c = new Credentials({ storeRoot: f.storeRoot, dataRoot: join(f.root, 'data') });
t.after(() => c.close()); assert.deepEqual(c.status(), []);
});
test('credential-revoke-failed: generic remove cannot bypass remote model confirmation', t => {
const f = fixture(t), c = f.open(); t.after(() => c.close());
c.writeModel('demo/coder', record(key()));
assert.throws(() => c.remove('demo/coder'), e => e.code === 'credential-revoke-failed');
assert.equal(c.modelRecord('demo/coder').state, 'valid'); assert.equal(fs.existsSync(f.file), true);
});
test('retiring keys reload privately and remain covered by the credential leak guard', t => {
const f = fixture(t), c = f.open(), value = key(), old = key(); t.after(() => c.close());
c.writeModel('demo/coder', { ...record(value), retiring: [{ key: old, keyId: id(old), issuedAt: '2026-10-23T00:00:00.000Z', retireAt: '2026-10-24T02:00:00.000Z' }] });
c.close(); const again = f.open(); t.after(() => again.close());
assert.equal(again.modelRecord('demo/coder').retiring[0].key === old, true);
const status = JSON.stringify(again.status()); assert.equal(status.includes(old), false); assert.equal(status.includes(value), false);
assert.throws(() => again.assertClean({ value: old }), e => e.code === 'credential-leak');
});
@@ -0,0 +1,83 @@
// #656 STATIC unit proof/bookkeeping tests: real controller, real HTTP/private files.
// Empty injected view here is NOT runtime identity evidence; runtime file tests it.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { tmpdir } from 'node:os';
import { fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { revocationFixture, outcome, id, bounded } from './model-revocation-fixture.mjs';
const timeout=30000;
const cached=f=>assert.equal(f.record()?.state==='revoking',true,'failed final completion retains cached revoking admission fence');
async function completed(f){await f.prepare();f.complete();}
test('656 ordinary present final completes with exactly one final pathname unlink',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});const before=f.counts.unlinkFinal;await completed(f);assert.equal(f.record()===null,true);assert.equal(f.exists(f.file),false);assert.equal(f.counts.unlinkFinal-before,1,'one final unlink with no generic second unlink');const state=f.credentials.status().find(s=>s.instance===f.instance&&s.service==='model');assert.equal(state.state,'revoked');assert.equal(state.date,'2099-01-01');f.complete();assert.equal(f.counts.unlinkFinal-before,1,'ordinary no-record/no-supplied-proof repeat is a no-op');f.valueFree();
});
test('656 completion without confirmed remote preparation remains refused',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});const result=await outcome(()=>f.complete());assert.equal(result.ok,false);assert.equal(result.code,'credential-revoke-failed');cached(f);assert.equal(f.exists(f.file),true);f.valueFree();
});
test('656 failed final unlink retains cache disk confirmation and exact retry without another remote operation',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();const calls=f.state.calls.length,before=f.counts.unlinkFinal;f.faults.unlinkFinal=true;const failure=await outcome(()=>f.complete());cached(f);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),true);assert.throws(()=>f.manager.assertLaunchable(f.instance),e=>e.code==='proxy-key-revoking');
f.faults.unlinkFinal=false;f.complete();assert.equal(f.record()===null,true);assert.equal(f.exists(f.file),false);assert.equal(f.counts.unlinkFinal-before,2,'one failed then one successful final unlink, no second unlink after cache clear');assert.equal(f.state.calls.length,calls,'exact retained confirmation permits local retry without HTTP under writer lock');f.valueFree();
});
test('656 final read failure keeps cached intent and closes all opened model descriptors',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();f.faults.readFinal=true;const failure=await outcome(()=>f.complete());cached(f);assert.equal(f.counts.readFinal>0,true,'owned bounded final read fault was reached');assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),true);assert.equal(f.openModelFds(),0,'bounded final reader closes descriptor on error');f.faults.readFinal=false;f.complete();assert.equal(f.record()===null,true);assert.equal(f.exists(f.file),false);f.valueFree();
});
for(const mutation of ['inode replacement','content change','invalid shape'])test('656 '+mutation+' before final completion is not keyID-only confirmation',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();
if(mutation==='inode replacement'){const replacement=join(f.root,'owned-final-replacement');fs.writeFileSync(replacement,fs.readFileSync(f.file),{mode:0o600,flag:'wx'});fs.renameSync(replacement,f.file);}else if(mutation==='content change'){fs.writeFileSync(f.file,JSON.stringify({...f.record(),rotateBy:'2098-01-01'})+'\n',{mode:0o600});}else fs.writeFileSync(f.file,'{',{mode:0o600});
const before=id(fs.readFileSync(f.file)),failure=await outcome(()=>f.complete());cached(f);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),true);assert.equal(id(fs.readFileSync(f.file)),before,'detected existing bytes are never overwritten or deleted');f.valueFree();
});
test('656 initially missing final never mints completion proof from temp or label',{timeout},async t=>{
const f=await revocationFixture(t);f.seed();fs.unlinkSync(f.file);const before=f.state.calls.length,failure=await outcome(()=>f.prepare());assert.equal(!failure.ok&&failure.code==='credential-revoke-failed',true,'initially missing final cannot authorize completion preparation');cached(f);assert.equal(f.exists(f.file),false,'missing input is not restored');assert.equal(f.exists(f.temporary),true);assert.equal(f.state.calls.length,before);f.valueFree();
});
test('656 final missing after confirmed preparation is not treated as proven prior completion',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();fs.unlinkSync(f.file);const failure=await outcome(()=>f.complete());cached(f);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),false,'external missing final is not falsely reported retained or restored');f.valueFree();
});
test('656 same keyID but changed FULL cached and durable record refuses stale confirmation',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();const changed={...f.record(),revokingAt:'2026-10-25T00:00:01.000Z'};f.credentials.writeModel(f.instance,changed);const failure=await outcome(()=>f.complete());cached(f);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),true);assert.equal(f.record().revokingAt,changed.revokingAt);f.valueFree();
});
for(const action of ['reload','rewrite'])test('656 '+action+' invalidates prior proof even for identical FULL record',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();const record=f.record();if(action==='reload')f.credentials.reload(f.instance,'model',record.rotateBy);else f.credentials.writeModel(f.instance,record);const failure=await outcome(()=>f.complete());cached(f);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),true);await completed(f);assert.equal(f.record()===null,true);assert.equal(f.exists(f.file),false);f.valueFree();
});
test('656 close invalidates supplied confirmation and absent cache is not silent success',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();f.credentials.close();const failure=await outcome(()=>f.complete());assert.equal(!failure.ok&&failure.code==='credential-revoke-failed',true,'absent cache and reference cannot silently complete supplied confirmation');assert.equal(f.exists(f.file),true);f.valueFree();
});
test('656 model hold invalidates supplied proof without deleting final',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();f.credentials.hold(f.instance,'model');const failure=await outcome(()=>f.complete());assert.equal(!failure.ok&&failure.code==='credential-revoke-failed',true,'held absent cache cannot silently complete old confirmation');assert.equal(f.exists(f.file),true);assert.equal(f.credentials.held(f.instance,'model'),true);f.valueFree();
});
test('656 model in-flight slot is uncertainty and never an abort callback after final unlink',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});const signal=f.credentials.claim(f.instance,'model');let aborted=0;signal.addEventListener('abort',()=>{aborted++;});const failure=await outcome(()=>f.prepare());assert.equal(!failure.ok&&failure.code==='credential-revoke-failed',true,'unproven model inflight slot refuses preparation');cached(f);assert.equal(f.exists(f.file),true);assert.equal(aborted,0);f.credentials.release(f.instance,'model',signal);await completed(f);assert.equal(aborted,0,'model-only completion invokes no abort callback');assert.equal(f.credentials.abortClaim(f.instance,'model').pending,false);f.valueFree();
});
test('656 new tmp after preparation refuses before final removal',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();fs.copyFileSync(f.file,f.temporary);fs.chmodSync(f.temporary,0o600);const failure=await outcome(()=>f.complete());cached(f);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file)&&f.exists(f.temporary),true);await completed(f);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);f.valueFree();
});
test('656 failed tmp absence confirmation retains final intent and fresh retry rechecks before completion',{timeout},async t=>{
const f=await revocationFixture(t);f.seed();f.faults.absenceTmp=true;const failure=await outcome(()=>f.prepare());assert.equal(!failure.ok&&failure.code==='credential-revoke-failed',true,'uncertain absence does not mint local completion confirmation');assert.equal(f.counts.absenceTmp>0,true,'owned post-unlink absence fault was reached');cached(f);assert.equal(f.exists(f.file),true);assert.equal(f.exists(f.temporary),false,'own successful tmp unlink is not falsely rolled back');f.faults.absenceTmp=false;await completed(f);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);f.valueFree();
});
test('656 exact prior opaque completion proof alone permits already-completed repeat',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();assert.equal(f.proofs.length,1,'real controller produced complete-record completion proof');const proof=f.proofs[0];f.complete();const count=f.counts.unlinkFinal;f.credentials.finishModelRevocation(f.instance,proof);assert.equal(f.counts.unlinkFinal,count,'same proven completion repeat does not unlink a path');
const forged=await outcome(()=>f.credentials.finishModelRevocation(f.instance,Object.freeze({})));assert.equal(forged.ok,false);assert.equal(forged.code,'credential-revoke-failed');assert.equal(f.exists(f.file)||f.exists(f.temporary),false);f.valueFree();
});
test('656 a newer snapshot invalidates an older confirmed token and grants no remote authority',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();assert.equal(f.proofs.length,1,'real controller produced complete-record completion proof');f.credentials.captureModelRevocation(f.instance);const failure=await outcome(()=>f.complete());cached(f);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),true);await completed(f);assert.equal(f.record()===null,true);f.valueFree();
});
test('656 FIFO snapshot is nonblocking in owned bounded child and retains intent',{timeout},async t=>{
let child,closed,ownedRoot,watchdog,answer,startNice,spawnFailed=false,badRoot=false,timedOut=false;let readyResolve;const ready=new Promise(resolve=>{readyResolve=resolve;});
t.after(async()=>{clearTimeout(watchdog);const errors=[];try{if(child?.exitCode===null&&child.signalCode===null)child.kill('SIGTERM');if(closed)await bounded(closed,'owned FIFO child cleanup timeout');}catch{errors.push('owned-child-close');}try{if(ownedRoot&&fs.existsSync(ownedRoot))fs.rmSync(ownedRoot,{recursive:true,force:true});}catch{errors.push('owned-root-close');}if(errors.length)throw new AggregateError(errors.map(code=>Error(code)),'owned FIFO parent cleanup failed');});
child=fork(fileURLToPath(new URL('./model-revocation-fifo-child.mjs',import.meta.url)),[],{execArgv:[],env:{...process.env},stdio:['ignore','ignore','ignore','ipc']});
closed=new Promise(resolve=>child.once('close',(code,signal)=>resolve({code,signal})));
child.once('error',()=>{spawnFailed=true;readyResolve();});
child.on('message',message=>{if(message?.kind==='owned-root'){if(typeof message.root==='string'&&dirname(message.root)===tmpdir()&&basename(message.root).startsWith('r656-'))ownedRoot=message.root;else badRoot=true;}if(message?.kind==='ready'){startNice=message.nice;watchdog=setTimeout(()=>{timedOut=true;child.kill('SIGTERM');},5000);readyResolve();}if(message?.kind==='result'||message?.kind==='error'){answer=message;readyResolve();}});
await bounded(ready,'owned FIFO child readiness unavailable',15000);const end=await bounded(closed,'owned FIFO child result unavailable',15000);clearTimeout(watchdog);
assert.equal(spawnFailed||badRoot,false,'only captured owned child/root accepted');assert.equal(timedOut,false,'FIFO read does not block owned child');assert.equal(end.code,0,'owned child setup and cleanup complete');assert.equal(end.signal,null);assert.equal(answer?.kind,'result','owned child reaches real controller outcome');assert.equal(startNice,19);assert.equal(answer.nice,19,'child priority is real measured19');assert.equal(answer.ownedCleanup,true);
assert.equal(answer.cachedRevoking,true,'FIFO cannot authorize clearing cached revoking intent');assert.equal(answer.code,'credential-revoke-failed');assert.equal(answer.finalPresent&&answer.tmpPresent,true);assert.equal(answer.openModelFds,0,'FIFO descriptor closed after type refusal');assert.equal(ownedRoot?fs.existsSync(ownedRoot):true,false,'owned child removed private fixture root');
});
test('656 successful later generation rewrite invalidates completed historical token',{timeout},async t=>{
const f=await revocationFixture(t);f.seed({matchingTmp:false});await f.prepare();assert.equal(f.proofs.length,1,'real controller produced complete-record completion proof');const proof=f.proofs[0];f.complete();const next=f.seed({matchingTmp:false,valid:true}),before=id(fs.readFileSync(f.file));const failure=await outcome(()=>f.credentials.finishModelRevocation(f.instance,proof));assert.equal(failure.ok,false);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.record()?.keyId,next.keyId);assert.equal(id(fs.readFileSync(f.file)),before);f.valueFree();
});
@@ -0,0 +1,126 @@
// #655 isolated STATIC fixture: actual managed fromSession caller + runtime/writer/controller.
import fs from 'node:fs';
import http from 'node:http';
import assert from 'node:assert/strict';
import { createHash, randomBytes } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { fixture, fakeHarnesses } from '../../cli/tests/helpers.mjs';
import { loadSystem, bootConfig } from '../../cli/src/config.mjs';
import { createLauncher } from '../../cli/src/launcher.mjs';
import { startBroker } from '../../bus/src/runtime.mjs';
import { gitEnv } from '../../seat/src/checkout.mjs';
import { organizationWriter } from '../../business/src/organization.mjs';
import { writeJson } from '../../business/tests/helpers.mjs';
import { readSessions, runsDir, sessionsFile, SESSIONS_VERSION, launchLogFile } from '../../seat/src/session.mjs';
export const waitFor = async (read, message, ms=15000) => { const end=Date.now()+ms;while(Date.now()<end){const value=await read();if(value)return value;await new Promise(r=>setTimeout(r,20));}throw Error(message); };
export const pause = ms => new Promise(r=>setTimeout(r,ms));
const id = value => createHash('sha256').update(value).digest('hex').slice(0,12);
export async function ownershipFixture(t,{launches=true,listed=true,keep=false,retired=false,noHire=false}={}){
const proxy=true,refuseProbe=false;
const root=fs.mkdtempSync(join(tmpdir(),'own655-')),closers=[],history=new Set(['fx655-manage']),proxyReceipts=[],logs=[];
let server,bus,launcher,stderr;const active=new Set(),gates=[];const track=p=>{active.add(p);p.then(()=>active.delete(p),()=>active.delete(p));return p;};const trace={preflightAttempts:0,successfulPreflights:0,enableBeforeSpawn:[],bindings:[],stalls:[]};
const checkHistory=entries=>{const text=JSON.stringify(entries);for(const value of history)if(text.includes(value))throw Error('fixture credential leaked');};
// Available before any acquired resource/setup can fail; no later f/launcher TDZ.
const launchLog=()=>{const file=launchLogFile(join(root,'data'),'acme');return fs.existsSync(file)?fs.readFileSync(file,'utf8').trim().split('\n').filter(Boolean).map(line=>JSON.parse(line)):[];};
t.after(async()=>{const failures=[];for(const g of gates)g.release();try{await waitFor(()=>active.size===0,'owned operations did not settle',5000);}catch{failures.push('owned-operation-close');}for(const close of [...closers].reverse()){try{await close();}catch{failures.push('owned-close');}}try{server?.closeAllConnections();if(server?.listening)await new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));}catch{failures.push('owned-server-close');}try{checkHistory({logs,proxyReceipts,trace});}catch{failures.push('value-free-receipts');}try{checkHistory({launch:launchLog()});}catch{failures.push('owned-launch-log-guard');}if(stderr)process.stderr.write=stderr;try{fs.rmSync(root,{recursive:true,force:true});}catch{failures.push('owned-root-close');}if(failures.length)throw new AggregateError(failures.map(code=>Error(code)),'owned managed fixture cleanup failed');});
const f=fixture(root,'acme',doc=>{doc.plugins={...doc.plugins,harness:['pi']};for(const role of Object.values(doc.roles))role.vars={...role.vars,harness:'pi'};doc.roles.chief.vars.model='claude-opus-5-5';doc.roles.aide={definition:'aide',vars:{harness:'pi',model:'claude-opus-5-5',thinking:'high'}};if(listed)doc.launch.instances.push('aide');if(keep)doc.launch.keep=['aide'];if(retired)doc.retired=['alpha'];if(noHire)doc.launch.noHire=['aide'];return doc;});
// The established scratch aide pattern only; repository roles/policy stay untouched.
for(const name of ['aide','helper']){fs.writeFileSync(join(f.roles,name+'.md'),'# Scratch aide\n');writeJson(join(f.roles,name+'.json'),{roleVersion:2,name,title:'Aide',contract:name+'.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});}
const chiefFile=join(f.roles,'chief-of-staff.json');writeJson(chiefFile,{...JSON.parse(fs.readFileSync(chiefFile,'utf8')),hireable:['aide','helper']});
const env={PATH:process.env.PATH,HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,MOSAIC_CONFIG:f.config,MOSAIC_ROLES_DIR:f.roles};
const configDir=join(root,'config'),prefix=`msk-${id(configDir)}-`,sentinel=prefix+'sentinel-'+randomBytes(32).toString('base64url');history.add(sentinel);
const state={keys:[sentinel],calls:[],refuseProbe,counts:{GET:0,PUT:0,PROBE:0},puts:[],failRemoval:false};
server=http.createServer(async(req,res)=>{try{
const supplied=req.headers.authorization?.slice(7);if(supplied)history.add(supplied);for(const key of state.keys)history.add(key);
if(req.url==='/v8/management/config/access/api-keys'){
state.calls.push(req.method);const ordinal=++state.counts[req.method];if(supplied!=='fx655-manage'){res.writeHead(401);res.end('{}');return;}
let values;if(req.method==='PUT'){const parts=[];for await(const part of req)parts.push(part);values=JSON.parse(Buffer.concat(parts));for(const value of values)history.add(value);state.puts.push([...values]);}
const gate=gates.find(g=>!g.used&&!g.cancelled&&g.method===req.method&&g.ordinal===ordinal);if(gate){gate.used=true;gate.seen=true;await gate.promise;}
if(res.destroyed)return;
if(req.method==='GET'){res.end(JSON.stringify(state.keys));return;}
if(state.failRemoval&&state.keys.some(key=>!values.includes(key))){res.writeHead(503);res.end('{}');return;}
state.keys=values;res.end('{"status":"ok"}');return;
}
state.calls.push('PROBE');state.counts.PROBE++;res.writeHead(!state.refuseProbe&&state.keys.includes(supplied)?200:401);res.end('{}');
}catch{if(!res.destroyed){res.writeHead(500);res.end('{}');}}});
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});const port=server.address().port;assert.notEqual(port,8317);
fs.mkdirSync(join(configDir,'secrets'),{recursive:true,mode:0o700});fs.writeFileSync(join(configDir,'secrets','proxy-management.key'),'fx655-manage',{mode:0o600});
fs.mkdirSync(join(f.dataRoot,'bus-host'),{recursive:true,mode:0o700});fs.writeFileSync(sessionsFile(f.dataRoot),JSON.stringify({sessionsVersion:SESSIONS_VERSION,sessions:[]}),{mode:0o600});
const git=(...args)=>{const result=spawnSync('git',['-c','user.name=test','-c','[email protected]','-C',f.doc.projects.stack.root,...args],{env:gitEnv(env),encoding:'utf8'});assert.equal(result.status,0,'owned local git fixture succeeds');};
git('init','-q','-b','main');fs.writeFileSync(join(f.doc.projects.stack.root,'file.txt'),'one\n');git('add','file.txt');git('commit','-q','-m','one');
const adapter=join(root,'adapter.sh');fs.writeFileSync(adapter,'case "$MOSAIC_REQUEST" in *SILENT*) exec /usr/bin/sleep 30;; *) printf %s fx655-output; printf "done\\n" > "$MOSAIC_TURN_MARKER";; esac\n',{mode:0o644});
const system={...loadSystem({env}),...(proxy?{proxy:{port,binary:'/fixture-unused',firstOutputSeconds:120}}:{})};
const {trackers,...boot}=bootConfig({system,businessId:'acme',env});assert.equal(trackers,undefined);
stderr=process.stderr.write;process.stderr.write=function(chunk,...args){if(String(chunk).startsWith('model-proxy: ')){proxyReceipts.push(String(chunk));const callback=args.find(a=>typeof a==='function');callback?.();return true;}return stderr.call(this,chunk,...args);};
bus=await startBroker(boot);closers.push(()=>bus.close());
const hooks=[];let expectedKeyId;
const actualOp=async m=>{
switch(m.op){
case 'modelPreflight':return bus.modelPreflight(m);
case 'modelStall':return bus.modelStall(m);
case 'identity':return bus.identity(m.cap);
case 'authorizeLaunch':return bus.authorizeLaunch(m.cap,m.instance);
case 'refuse':return bus.refuse(m.cap,m.code,m.subject);
case 'endLaunch':return bus.endLaunch(m.record);
case 'recoverLaunch':return bus.recoverLaunch(m.record);
case 'credentialStatus':return bus.credentialStatus(m.business,m.role);
case 'mayLaunch':return bus.mayLaunch(m);
case 'landHolders':return bus.landHolders(m.business);
case 'landHeld':return bus.landHeld(m.record);
case 'landRelease':return bus.landRelease(m.record);
case 'threadsState':return bus.threadsState(m.business);
case 'threadIdle':return bus.threadIdle(m.record);
case 'lifecycleRequests':return bus.lifecycleRequests(m.business);
case 'lifecycleTake':return bus.lifecycleTake(m.record);
case 'lifecycleDelete':return bus.lifecycleDelete(m.record);
case 'lifecycleDone':return bus.lifecycleDone(m.record);
case 'sessionClears':return bus.sessionClears(m.business);
default:throw Error('fixture unrequested trusted operation');
}
};
const host={business:'acme',path:bus.path,dataRoot:f.dataRoot,beforeClose:fn=>hooks.push(fn),
op:async m=>{
if(m.op==='modelPreflight'){trace.preflightAttempts++;const value=await actualOp(m);if(value){trace.successfulPreflights++;expectedKeyId=id(value.key);}return value;}
if(m.op==='landHolders'&&trace.successfulPreflights){
// Existing trusted host call occurs after enable publication and BEFORE bundle,
// registerRuntime/spawnSession/bind in the production startLocked sequence.
const dirs=fs.readdirSync(runsDir(f.dataRoot,'acme'));assert.equal(dirs.length,1);const file=join(runsDir(f.dataRoot,'acme'),dirs[0],'proxy-first-turn-enabled');
assert.equal(fs.readFileSync(file,'utf8'),'enabled\n','managed caller publishes enable before spawn');assert.equal(fs.statSync(file).mode&0o777,0o600);assert.equal(trace.bindings.length,0);assert.equal(readSessions(f.dataRoot).length,0);trace.enableBeforeSpawn.push(true);
}
if(m.op==='modelStall'){const result=await actualOp(m);trace.stalls.push({run:m.run,keyId:m.keyId,stopped:result.stopped});return result;}
return actualOp(m);
},
bindLaunch:async record=>{const result=bus.bindLaunch(record);trace.bindings.push({run:record.run,pid:record.pid,startTime:record.startTime,modelKeyId:record.modelKeyId});return result;}};
const makeLoader=await fakeHarnesses({after:close=>closers.push(close)},{pi:adapter});
launcher=createLauncher({host,system,env,launches,makeLoader,sessionDefaults:{pollInterval:50},harnessVersion:'0.85.1',log:line=>logs.push(line)});
// The existing trusted host hook owns all launcher-created children. No registry
// PID is signalled by fixture teardown; normal bounded launcher close is used.
closers.push(async()=>{for(const close of hooks)await close(0);});await launcher.listen();
const chief=bus.broker.bindLaunch({business:'acme',role:'chief',run:'own-chief',harness:'pi'});bus.broker.request(chief,{verb:'role.claim'});
const human=bus.broker.bindHuman({business:'acme',human:'jason',via:'cli',outsideAgent:true});
const valueFree=()=>checkHistory({logs,proxyReceipts,launch:launchLog(),trace});
const writer=organizationWriter({dir:configDir,rolesDir:f.roles});let revisionEdits=0;
const file=join(configDir,'businesses','acme.json');
const hold=(method='GET',ordinal=state.counts[method]+1)=>{let release;const promise=new Promise(r=>{release=r;});const gate={method,ordinal,seen:false,used:false,cancelled:false,promise,release};gates.push(gate);return {ordinal,seen:()=>gate.seen,entered:()=>waitFor(()=>gate.seen,'owned HTTP gate not reached',5000),release,cancel:()=>{gate.cancelled=true;release();}};};
const outcome=promise=>{const box={settled:false};box.promise=track(Promise.resolve(promise).then(value=>{box.settled=true;box.ok=true;box.value=value;return box;},error=>{box.settled=true;box.ok=false;box.code=error.code??'unexpected-error';return box;}));return box;};
const hire=(name='aide',extra={},cap=chief)=>{const called={value:false};const box=outcome(Promise.resolve().then(()=>{const operation=bus.broker.request(cap,{verb:'instance.hire',args:{name,definition:'aide',reason:'fixture ownership',...extra}});called.value=true;return operation;}));Object.defineProperty(box,'invoked',{get:()=>called.value});return box;};
const record=(name='aide')=>bus.credentials.modelRecord('acme/'+name);
const snapshot=()=>{const journal=join(configDir,'businesses','.history','acme','changes.jsonl');return {membership:id(fs.readFileSync(file)),journal:fs.existsSync(journal)?id(fs.readFileSync(journal)):null,credentials:id(JSON.stringify(bus.credentials.status())),model:record()?id(JSON.stringify(record())):null,remote:id(JSON.stringify(state.keys)),calls:state.calls.length};};
return {root,f,state,trace,proxyReceipts,logs,launcher,bus,chief,record,hold,outcome,hire,snapshot,file,launchLog,expectedKeyId:()=>expectedKeyId,valueFree,
launchAide:()=>outcome(launcher.fromSession(chief,'aide')),
preflight:(name='aide')=>outcome(bus.modelPreflight({business:'acme',role:name})),
listed:name=>JSON.parse(fs.readFileSync(file,'utf8')).launch.instances.includes(name),
bumpRevision:()=>{const {document,revision}=writer.read('acme');document.vars['tracker.pollSeconds']=60+(++revisionEdits);writer.write('acme',document,{revision,actor:'jason'});},
wrongActor:()=>{const cap=bus.broker.bindLaunch({business:'acme',role:'coder',run:'own-coder',harness:'pi'});bus.broker.request(cap,{verb:'role.claim'});return cap;},
healthy:async key=>{const response=await fetch('http://127.0.0.1:'+port+'/v1/models',{headers:{authorization:'Bearer '+key},signal:AbortSignal.timeout(5000)});await response.arrayBuffer();return response.status;},
assertModelAbsent:name=>{assert.equal(bus.credentials.modelRecord('acme/'+name)===null,true);assert.equal(fs.existsSync(join(configDir,'secrets','agents','acme',name,'model')),false);},
rotateBound:async()=>{const before=record();bus.credentials.writeModel('acme/aide',{...before,rotateBy:new Date(Date.now()-86400_000).toISOString().slice(0,10)});await waitFor(()=>record()?.retiring.some(r=>r.keyId===before.keyId),'original runtime sweep rotates bound generation');return before.keyId;},
explicitEnd:()=>bus.broker.request(chief,{verb:'role.revoke',args:{role:'aide',reason:'fixture explicit end'}}),
fire:()=>outcome(Promise.resolve().then(()=>bus.broker.request(chief,{verb:'instance.fire',args:{name:'aide',reason:'fixture explicit fire'}}))),
runDirectories:()=>fs.existsSync(runsDir(f.dataRoot,'acme'))?fs.readdirSync(runsDir(f.dataRoot,'acme')).map(name=>join(runsDir(f.dataRoot,'acme'),name)):[],
sessions:()=>readSessions(f.dataRoot),send:body=>bus.broker.request(human,{verb:'message.send',args:{to:'aide',body,class:'REQUEST'}}),
reply:sent=>waitFor(()=>bus.broker.request(human,{verb:'message.receive'}).find(m=>m.in_reply_to===sent.id),'real managed reply observed'),
assertHistoryGuard:()=>{try{checkHistory({probe:sentinel});}catch(e){if(e.message==='fixture credential leaked')return;}throw Error('fixture history guard failed');}};
}
@@ -0,0 +1,103 @@
// #655 STATIC test-first preparation, real runtime/writer/controller and actual launcher.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { sandboxProblem } from '../../seat/src/sandbox.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { ownershipFixture, waitFor } from './model-hire-ownership-fixture.mjs';
const unavailable=sandboxProblem()??false; // Any skip means UNVERIFIED, not green acceptance.
const done=async box=>{await box.promise;return box;};
const refused=async(box,code)=>{await done(box);assert.equal(box.ok,false,'real operation refuses');assert.equal(box.code,code,'real operation preserves exact refusal');};
const shape=record=>JSON.stringify(record);
test('655 listed duplicate with no record refuses before any model or management effects',{timeout:30000},async t=>{
const f=await ownershipFixture(t),before=f.snapshot();assert.equal(f.record()===null,true);
await refused(f.hire(),'organization-instance-exists');
assert.deepEqual(f.snapshot(),before,'duplicate hire makes no credential or management effects');f.assertHistoryGuard();f.valueFree();
});
test('655 actual launch own preflight generation and bound consumer survive concurrent duplicate hire',{skip:unavailable,timeout:30000},async t=>{
const f=await ownershipFixture(t),pGet=f.hold('GET'),p=f.launchAide();await pGet.entered();
// P's own acquisition GET is held. Its PUT confirmation GET is ordinal+1;
// only an OLD H reuse GET can be ordinal+2. The correct early refusal removes it.
const hGet=f.hold('GET',pGet.ordinal+2),h=f.hire();await waitFor(()=>h.invoked,'real H Broker request returned its asynchronous lifecycle operation',5000);pGet.release();
await waitFor(()=>h.settled||hGet.seen(),'duplicate either refuses early or reaches real reuse GET',5000);
await done(p);assert.equal(p.ok,true,'actual own launch call settles');assert.equal(p.value.ok,true,'same actual launch succeeds after its own preflight');
assert.equal(f.trace.successfulPreflights,1);assert.deepEqual(f.trace.enableBeforeSpawn,[true]);
const session=await waitFor(()=>f.sessions().find(s=>s.instance==='aide'&&s.live),'same P is bound and live');
const generation=f.record();assert.equal(session.modelKeyId,generation.keyId);assert.equal(f.trace.bindings[0].modelKeyId,generation.keyId);
const beforeFile=shape(generation),beforeCalls=f.state.calls.length;
if(hGet.seen())hGet.release();else hGet.cancel();await refused(h,'organization-instance-exists');
assert.equal(f.state.keys.includes(generation.key)&&shape(f.record())===beforeFile,true,'admitted launch generation remains installed after duplicate failure');
assert.equal(startTimeOf(session.runnerPid),session.runnerStartTime,'same owned bound runner remains live');assert.equal(f.sessions().some(s=>s.run===session.run&&s.live),true);
assert.equal(f.launchLog().some(e=>e.event==='end'&&e.run===session.run),false);assert.equal(f.trace.stalls.length,0);
if(!hGet.seen())assert.equal(f.state.calls.length,beforeCalls,'early refused H has no later management effects');
const sent=f.send('OUTPUT');assert.equal((await f.reply(sent)).body,'fx655-output');assert.equal(await f.healthy(generation.key),200);f.valueFree();
});
for(const [label,options,args,code] of [
['retired name',{retired:true},{name:'alpha'},'organization-instance-retired'],
['listed duplicate vars',{}, {vars:{model:'claude-opus-5-5'}},'organization-instance-exists'],
['fired mismatched legal definition',{listed:false},{definition:'helper'},'organization-instance-exists'],
['fired matching definition with vars',{listed:false},{vars:{model:'claude-opus-5-5'}},'organization-instance-exists'],
])test('655 shared validation '+label+' has no credential effects',{timeout:30000},async t=>{
const f=await ownershipFixture(t,options),before=f.snapshot(),{name='aide',...extra}=args;await refused(f.hire(name,extra),code);
assert.deepEqual(f.snapshot(),before,'shared hire refusal precedes credential registration or proxy I/O');f.valueFree();
});
for(const [label,options,extra,code] of [
['kept target',{keep:true},{},'instance-kept'],
['noHire definition',{noHire:true},{},'definition-not-hireable'],
['ineligible definition',{}, {definition:'security-reviewer'},'definition-not-hireable'],
['invalid vars',{}, {vars:{harness:'pi'}},'invalid-request'],
])test('655 existing Broker '+label+' refusal remains before model effects',{timeout:30000},async t=>{
const f=await ownershipFixture(t,options),before=f.snapshot();await refused(f.hire('aide',extra),code);assert.deepEqual(f.snapshot(),before);f.valueFree();
});
test('655 existing nonchief actor refusal remains before shared checks and model effects',{timeout:30000},async t=>{
const f=await ownershipFixture(t),cap=f.wrongActor(),before=f.snapshot();await refused(f.hire('aide',{},cap),'role-not-chief');assert.deepEqual(f.snapshot(),before);f.valueFree();
});
test('655 valid fired matching definition rehire publishes before queued acquisition',{timeout:30000},async t=>{
const f=await ownershipFixture(t,{listed:false}),put=f.hold('PUT'),h=f.hire();await put.entered();const p=f.preflight();assert.equal(f.listed('aide'),false);
put.release();await done(h);assert.equal(h.ok,true);assert.equal(typeof h.value.transaction,'string');await done(p);assert.equal(p.ok,true);assert.equal(f.listed('aide'),true);assert.equal(p.value.key===f.record().key,true);assert.equal(await f.healthy(p.value.key),200);f.valueFree();
});
test('655 preexisting admitted generation and retiring metadata survive duplicate failure',{skip:unavailable,timeout:30000},async t=>{
const f=await ownershipFixture(t),p=await done(f.launchAide());assert.equal(p.ok&&p.value.ok,true);const sent=f.send('OUTPUT');await f.reply(sent);
const retiring=await f.rotateBound(),before=shape(f.record()),puts=f.state.counts.PUT;
await refused(f.hire(),'organization-instance-exists');assert.equal(shape(f.record())===before,true);assert.equal(f.record().retiring[0].keyId,retiring);assert.equal(f.state.counts.PUT,puts);assert.equal(await f.healthy(f.record().key),200);assert.equal(f.sessions().some(s=>s.modelKeyId===retiring&&s.live),true);f.valueFree();
});
test('655 captured revision is checked after controller queue before generation issue',{timeout:30000},async t=>{
const f=await ownershipFixture(t),gate=f.hold('GET'),p=f.preflight('chief');await gate.entered();const h=f.hire('alpha');
// A following same-business operation proves H established its pending slot,
// rather than an arbitrary sleep before the external owner revision change.
await refused(f.hire('beta'),'organization-busy');f.bumpRevision();gate.release();await done(p);assert.equal(p.ok,true);await refused(h,'revision-conflict');
assert.equal(f.state.counts.PUT,1,'queued stale hire does not issue a generation');f.assertModelAbsent('alpha');assert.equal(f.listed('alpha'),false);f.valueFree();
});
test('655 genuine new generation revision failure cleans before queued acquisition receives a key',{timeout:30000},async t=>{
const f=await ownershipFixture(t,{listed:false}),put=f.hold('PUT'),h=f.hire();await put.entered();
const failedGeneration=f.state.puts[0].find(k=>!k.includes('-sentinel-'));assert.ok(failedGeneration,'real first issuance payload observed');
const p=f.preflight();f.bumpRevision();put.release();await refused(h,'revision-conflict');await done(p);
const valid=p.ok&&p.value.key!==failedGeneration&&f.record()?.key===p.value.key&&f.state.keys.includes(p.value.key)&&!f.state.keys.includes(failedGeneration);
assert.equal(valid,true,'queued acquisition receives a valid post-cleanup generation');assert.equal(f.listed('aide'),false);assert.equal(await f.healthy(p.value.key),200);f.valueFree();
});
test('655 failed cleanup retains revoking intent and refuses queued admission',{timeout:30000},async t=>{
const f=await ownershipFixture(t,{listed:false}),put=f.hold('PUT'),h=f.hire();await put.entered();const p=f.preflight();f.bumpRevision();f.state.failRemoval=true;put.release();
await refused(h,'revision-conflict');await done(p);assert.equal(f.record()?.state,'revoking','failed removal keeps durable nonlaunchable intent');
assert.equal(!p.ok&&p.code==='proxy-key-revoking',true,'queued admission refuses retained revoking intent');assert.equal(f.listed('aide'),false);assert.equal(fs.existsSync(join(f.root,'config','secrets','agents','acme','aide','model')),true);f.valueFree();
});
test('655 no-launch remains before own preflight and grants no consumer or stop authority',{timeout:30000},async t=>{
const f=await ownershipFixture(t,{launches:false}),before=f.snapshot(),p=await done(f.launchAide());assert.equal(p.ok,true);assert.deepEqual(p.value,{ok:false,error:'launches-disabled'});assert.deepEqual(f.snapshot(),before);assert.equal(f.sessions().length,0);assert.equal(f.trace.preflightAttempts,0);assert.equal(f.trace.bindings.length,0);f.valueFree();
});
test('655 explicit lifecycle end confirms own exit before ordinary fire removes generation',{skip:unavailable,timeout:30000},async t=>{
const f=await ownershipFixture(t),p=await done(f.launchAide());assert.equal(p.ok&&p.value.ok,true);const session=await waitFor(()=>f.sessions().find(s=>s.instance==='aide'&&s.live),'owned aide registered');const record=f.record();
const sent=f.send('OUTPUT');await f.reply(sent);f.explicitEnd();await f.launcher.tick();await waitFor(()=>!f.sessions().some(s=>s.run===session.run),'explicit lifecycle end reaches normal session exit');
assert.equal(startTimeOf(session.runnerPid),null,'only recorded owned runner exited before credential removal');const fire=await done(f.fire());assert.equal(fire.ok,true);assert.equal(f.record()===null,true);assert.equal(f.state.keys.includes(record.key),false);assert.equal(f.listed('aide'),false);f.valueFree();
});
@@ -0,0 +1,120 @@
// M2/#649: actual Store reopen and createLauncher recovery, no real proxy/config.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { createLauncher } from '../../cli/src/launcher.mjs';
import { sessionsFile, launchLogFile } from '../../seat/src/session.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { reviewFixture } from './model-review-fixtures.mjs';
async function historical(t, mode, { ended = false, live = false } = {}) {
const f = await reviewFixture(t);
const original = { ...f.entry };
if (mode === 'legacy') delete f.entry.modelKeyId;
const first = await f.boot({ proxyMode: false });
const record = { business: 'demo', role: 'chief', run: original.run, harness: 'pi', pid: original.pid, startTime: original.startTime };
const launched = first.bindLaunch(record);
first.broker.request(launched.cap, { verb: 'role.claim' });
const db = new DatabaseSync(join(f.dataRoot, 'bus', 'bus.sqlite'), { readOnly: true });
t.after(() => db.close());
const count = (kind, run = original.run) => db.prepare('SELECT count(*) n FROM events WHERE business=? AND kind=? AND subject=?').get('demo', kind, run).n;
const claim = () => db.prepare('SELECT op,holder_run FROM role_claims WHERE business=? AND role=? ORDER BY seq DESC LIMIT 1').get('demo', 'chief');
assert.equal(count('session.launched'), 1); assert.equal(claim().op, 'claim'); assert.equal(claim().holder_run, original.run);
if (ended) first.endLaunch({ business: 'demo', run: original.run, reason: 'host-lost', exitCode: null });
await f.closeBus();
// SQLite and immutable launch/claim history persist through the real runtime Store reopen.
if (!live) await f.exit();
f.writeSessions([f.entry]);
if (mode === 'down') f.state.failGet = true;
if (mode === 'open') f.state.hideSentinel = true;
if (mode === 'revoking' || mode === 'revoked') {
f.setModel({ state: 'revoking', revokingAt: new Date().toISOString() });
if (mode === 'revoking') f.state.failPut = true;
}
const bus = await f.boot();
assert.equal(count('session.launched'), 1);
assert.equal(claim().op, ended ? 'release' : 'claim', 'persisted claim state survives Store reopen');
const logs = [], binds = [], cleanupReplies = [];
const host = {
business: 'demo', beforeClose() {},
bindLaunch: async r => { const result = bus.bindLaunch(r); binds.push(true); return result; },
op: async m => {
if (m.op === 'endLaunch') return bus.endLaunch(m.record);
// Proposed trusted method/caller contract ONLY; old launcher never invokes
// this branch. No product implementation or admission bypass in this fixture.
if (m.op === 'recoverLaunch') { const result = bus.recoverLaunch(m.record); cleanupReplies.push(result); return result; }
if (m.op === 'lifecycleRequests') return bus.lifecycleRequests(m.business);
if (m.op === 'threadsState') return bus.threadsState(m.business);
throw Error('fixture-unrequested-op');
},
};
const recover = async ({ launches = true } = {}) => {
const launcher = createLauncher({ host, system: JSON.parse(fs.readFileSync(f.config, 'utf8')),
env: { MOSAIC_CONFIG: f.config, MOSAIC_ROLES_DIR: f.rolesDir, PATH: '/usr/bin' }, launches, log: line => logs.push(line) });
f.attachLauncher(launcher); await launcher.listen();
};
const endedOnce = () => {
assert.equal(count('session.ended'), 1, 'historical cleanup appends one end despite proxy admission refusal');
assert.equal(claim().op, 'release', 'historical cleanup releases persisted claim');
const body = JSON.parse(db.prepare("SELECT body FROM events WHERE kind='session.ended' AND subject=? ORDER BY seq DESC LIMIT 1").get(original.run).body);
assert.equal(body.reason, 'host-lost'); assert.equal(body.released, true);
const lines = fs.readFileSync(launchLogFile(f.dataRoot, 'demo'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
assert.equal(lines.filter(line => line.event === 'end' && line.run === original.run && line.reason === 'host-lost').length, 1);
};
const noCapability = () => {
assert.equal(binds.length, 0, 'historical cleanup never calls capability-minting bindLaunch');
assert.ok(cleanupReplies.every(reply => !Object.hasOwn(reply, 'cap')), 'cleanup result contains no capability');
};
return { f, bus, original, record, count, claim, logs, recover, endedOnce, noCapability };
}
for (const mode of ['down', 'open', 'revoking', 'revoked', 'legacy']) test(`M2 historical cleanup survives ${mode} proxy admission with persisted end and release`, async t => {
const h = await historical(t, mode); await h.recover(); h.endedOnce(); h.noCapability();
});
test('M2 healthy historical cleanup also mints no capability or model-run binding', async t => {
const h = await historical(t, 'healthy'); await h.recover(); h.endedOnce(); h.noCapability();
});
test('M2 fresh ordinary launch still requires current proxy admission', async t => {
const h = await historical(t, 'down');
await assert.rejects(async () => h.bus.bindLaunch({ ...h.record, run: 'fresh-m2' }), error => error.code === 'proxy-down');
assert.equal(h.count('session.launched', 'fresh-m2'), 0); assert.equal(h.count('session.ended', 'fresh-m2'), 0);
});
test('M2 forged historical recovery without immutable launch cannot create or end a run', async t => {
const h = await historical(t, 'healthy'); h.f.writeSessions([{ ...h.f.entry, run: 'forged-m2' }]);
await h.recover();
assert.equal(h.count('session.launched', 'forged-m2'), 0, 'cleanup cannot synthesize a fresh immutable launch');
assert.equal(h.count('session.ended', 'forged-m2'), 0); assert.equal(h.claim().op, 'claim');
});
for (const [field, value] of [['instance', 'coder'], ['harness', 'claude-code'], ['pid', 'runner'], ['startTime', '0'], ['thread', 'thread-m2'], ['session', 'session-m2']]) {
test(`M2 mismatched historical ${field} never releases or ends the immutable run`, async t => {
const h = await historical(t, 'healthy');
h.f.writeSessions([{ ...h.f.entry, [field]: value === 'runner' ? h.f.entry.runnerPid : value }]);
await h.recover(); assert.equal(h.count('session.ended'), 0); assert.equal(h.claim().op, 'claim');
});
}
test('M2 already-ended historical cleanup is classified before proxy admission without duplicate release', async t => {
const h = await historical(t, 'down', { ended: true }); await h.recover();
assert.equal(h.count('session.ended'), 1); assert.equal(h.claim().op, 'release'); h.noCapability();
assert.ok(h.logs.some(line => line.includes('already ended')), 'ended history is recognized despite proxy down');
});
test('M2 no-launch preserves prior live sessions and claim with no cleanup or binding', async t => {
const h = await historical(t, 'down', { live: true }); const before = fs.readFileSync(sessionsFile(h.f.dataRoot), 'utf8');
await h.recover({ launches: false });
assert.equal(h.count('session.ended'), 0); assert.equal(h.claim().op, 'claim'); h.noCapability();
assert.equal(startTimeOf(h.original.pid), h.original.startTime); assert.equal(startTimeOf(h.original.runnerPid), h.original.runnerStartTime);
assert.equal(fs.readFileSync(sessionsFile(h.f.dataRoot), 'utf8'), before);
});
test('M2 persisted claim blocks a new claim until actual historical cleanup releases it', async t => {
const h = await historical(t, 'healthy');
const model = h.bus.credentials.status().find(row => row.instance === 'demo/chief' && row.service === 'model');
const fresh = h.bus.bindLaunch({ ...h.record, run: 'claim-m2', modelKeyId: model.keyId });
assert.throws(() => h.bus.broker.request(fresh.cap, { verb: 'role.claim' }), error => error.code === 'role-already-held');
await h.recover();
assert.equal(h.claim().op, 'release');
h.bus.broker.request(fresh.cap, { verb: 'role.claim' });
assert.equal(h.claim().holder_run, 'claim-m2');
h.bus.endLaunch({ business: 'demo', run: 'claim-m2', reason: 'stopped', exitCode: null });
});
@@ -0,0 +1,110 @@
// M2/#649 actual trusted host -> forked bus/process.mjs -> runtime -> Broker route.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { createHash } from 'node:crypto';
import { startHost } from '../../cli/src/host.mjs';
import { createLauncher } from '../../cli/src/launcher.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { busBusiness } from '../src/business.mjs';
import { sessionsFile, launchLogFile } from '../../seat/src/session.mjs';
import { reviewFixture } from './model-review-fixtures.mjs';
const id = key => createHash('sha256').update(key).digest('hex').slice(0, 12);
async function ipcFixture(t, { down = true, ended = false, live = false } = {}) {
let host, db;
const owned = [];
t.after(async () => {
const errors = [];
for (const close of [() => host?.close(), () => db?.close(), ...owned]) {
try { await close(); } catch (error) { errors.push(error); }
}
if (errors.length) throw new AggregateError(errors, 'owned IPC fixture cleanup failed');
});
// Capture helper cleanup so real fork exits BEFORE the fake proxy/root disappear.
const f = await reviewFixture({ after: close => owned.push(close) });
const first = await f.boot();
const proxy = await first.modelPreflight({ business: 'demo', role: 'chief' });
const record = { business: 'demo', role: 'chief', run: f.entry.run, harness: 'pi', pid: f.entry.pid, startTime: f.entry.startTime };
const launch = first.bindLaunch({ ...record, modelKeyId: f.entry.modelKeyId });
first.broker.request(launch.cap, { verb: 'role.claim' });
if (ended) first.endLaunch({ business: 'demo', run: record.run, reason: 'host-lost', exitCode: null });
await f.closeBus(); if (!live) await f.exit(); f.writeSessions([f.entry]);
db = new DatabaseSync(join(f.dataRoot, 'bus', 'bus.sqlite'), { readOnly: true });
const count = (kind, run = record.run) => db.prepare('SELECT count(*) n FROM events WHERE business=? AND kind=? AND subject=?').get('demo', kind, run).n;
const claim = () => db.prepare('SELECT op,holder_run FROM role_claims WHERE business=? AND role=? ORDER BY seq DESC LIMIT 1').get('demo', 'chief');
assert.equal(count('session.launched'), 1); assert.equal(claim().op, ended ? 'release' : 'claim');
f.state.failGet = down;
const business = loadBusiness('demo', { dir: f.configDir, rolesDir: f.rolesDir }), resolved = {};
for (const instance of Object.keys(business.roles)) resolved[instance] = resolveInstance({ system: f.system, business, instance });
const logs = [];
host = await startHost({ business: 'demo', log: line => logs.push(line), boot: {
dataRoot: f.dataRoot, readers: ['demo'], businesses: { demo: busBusiness(business, resolved) },
organization: { dir: f.configDir, rolesDir: f.rolesDir, system: f.system },
proxy: { port: Number(new URL(proxy.baseUrl).port), binary: '/fixture-unused' },
} });
assert.equal(Number.isSafeInteger(host.pids.broker), true); assert.notEqual(host.pids.broker, process.pid, 'actual broker is a forked process');
const cleanup = input => host.op({ op: 'recoverLaunch', record: input });
const channelSurvives = async () => assert.equal(Array.isArray(await host.op({ op: 'credentialStatus', business: 'demo', role: 'chief' })), true);
return { f, host, record, logs, cleanup, channelSurvives, count, claim,
actualLauncher: async (launches = true) => {
const launcher = createLauncher({ host, launches, system: JSON.parse(fs.readFileSync(f.config, 'utf8')),
env: { MOSAIC_CONFIG: f.config, MOSAIC_ROLES_DIR: f.rolesDir, PATH: '/usr/bin' }, log: line => logs.push(line) });
await launcher.listen(); return launcher; // real host beforeClose owns its cleanup
} };
}
test('M2 IPC exact historical cleanup releases persisted claim with no cap or model-stall binding', async t => {
const h = await ipcFixture(t); const beforePut = h.f.state.put;
let reply;
await assert.doesNotReject(async () => { reply = await h.cleanup(h.record); }, 'actual trusted historical IPC cleanup must dispatch');
assert.deepEqual(Object.keys(reply), ['released'], 'historical IPC cleanup returns end metadata only');
assert.equal(reply.released, true); assert.equal(h.claim().op, 'release'); assert.equal(h.count('session.ended'), 1);
assert.equal(h.f.state.put, beforePut, 'cleanup does not mutate proxy keys');
await assert.rejects(h.host.op({ op: 'modelStall', business: 'demo', role: 'chief', run: h.record.run, keyId: h.f.entry.modelKeyId }), error => error.code === 'proxy-stop-failed');
await h.channelSurvives();
});
test('M2 IPC actual launcher recovery dispatches cleanup despite proxy-down admission', async t => {
const h = await ipcFixture(t); await h.actualLauncher();
assert.equal(h.count('session.ended'), 1, 'real fork/launcher recovery appends historical end');
assert.equal(h.claim().op, 'release');
const lines = fs.readFileSync(launchLogFile(h.f.dataRoot, 'demo'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
assert.equal(lines.filter(line => line.event === 'end' && line.run === h.record.run).length, 1);
await h.channelSurvives();
});
for (const [label, patch, code] of [
['fresh', { run: 'ipc-fresh' }, 'unknown-run'],
['forged role', { role: 'coder' }, 'launch-record-mismatch'],
['mismatched harness', { harness: 'claude-code' }, 'launch-record-mismatch'],
['mismatched start', { startTime: '0' }, 'launch-record-mismatch'],
]) test(`M2 IPC ${label} cannot use historical cleanup and leaves channel usable`, async t => {
const h = await ipcFixture(t);
await assert.rejects(h.cleanup({ ...h.record, ...patch }), error => error.code === code, `trusted historical IPC ${label} refusal uses ${code}`);
assert.equal(h.count('session.ended'), 0); assert.equal(h.claim().op, 'claim');
if (patch.run) assert.equal(h.count('session.launched', patch.run), 0);
await h.channelSurvives();
});
test('M2 IPC already-ended history refuses without duplicate release and preserves channel', async t => {
const h = await ipcFixture(t, { ended: true });
await assert.rejects(h.cleanup(h.record), error => error.code === 'run-ended', 'trusted historical IPC ended refusal uses run-ended');
assert.equal(h.count('session.ended'), 1); assert.equal(h.claim().op, 'release'); await h.channelSurvives();
});
test('M2 IPC ordinary fresh bind remains proxy-admitted and capable only through normal bind', async t => {
const h = await ipcFixture(t, { down: false });
const proxy = await h.host.op({ op: 'modelPreflight', business: 'demo', role: 'chief' });
const launch = await h.host.bindLaunch({ ...h.record, run: 'ipc-normal', modelKeyId: id(proxy.key) });
assert.equal(typeof launch.cap, 'string');
assert.equal(h.count('session.launched', 'ipc-normal'), 1);
await h.host.op({ op: 'endLaunch', record: { business: 'demo', run: 'ipc-normal', reason: 'stopped', exitCode: null } });
await h.channelSurvives();
});
test('M2 IPC ordinary fresh bind still refuses proxy-down and no-launch recovery remains inert', async t => {
const h = await ipcFixture(t, { live: true }); const before = fs.readFileSync(sessionsFile(h.f.dataRoot), 'utf8');
await assert.rejects(h.host.bindLaunch({ ...h.record, run: 'ipc-denied', modelKeyId: h.f.entry.modelKeyId }), error => error.code === 'proxy-down');
await h.actualLauncher(false);
assert.equal(h.count('session.ended'), 0); assert.equal(h.claim().op, 'claim');
assert.equal(fs.readFileSync(sessionsFile(h.f.dataRoot), 'utf8'), before); await h.channelSurvives();
});
@@ -0,0 +1,67 @@
// #652 diagnostic PREPARATION ONLY. Candidate6d3, not an observed regression.
// Real runtime/private records/fakeHTTP/owned OS children/original completed sweeps.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { reviewFixture } from './model-review-fixtures.mjs';
import { Credentials } from '../src/credentials.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
const agent='demo/chief';
const absent=pid=>assert.throws(()=>fs.lstatSync(`/proc/${pid}`),error=>error.code==='ENOENT','own scratch process reaped; OS exit alone is not acquired runtime proof');
async function setup(t,{noRun=false,corrupt=null}={}){
const f=await reviewFixture(t,{overdue:true});
const privateView=new Credentials({storeRoot:join(f.configDir,'secrets','agents'),dataRoot:f.dataRoot,modelAgents:[agent]});let initial;
try{initial=privateView.modelRecord(agent);}finally{privateView.close();}
// Existing seed has unrelated retiring key1 and current key2. Associate own live
// run with CURRENT key2; retiring key1 forces the first real authoritative view.
f.entry.modelKeyId=initial.keyId;
if(noRun){await f.exit();absent(f.entry.pid);absent(f.entry.runnerPid);f.writeSessions([]);}
else f.writeSessions([{...f.entry,...(corrupt?{[corrupt]:corrupt==='modelKeyId'?'fedcba987654':'0'}:{})}]);
const bus=await f.boot();const proxy=await bus.modelPreflight({business:'demo',role:'chief'});
const first=await f.observeNextSweep(),manager=f.sweptManager;
assert.equal(typeof manager?.ensure,'function','real runtime controller observed through unchanged original sweep');
if(!corrupt){assert.equal(first.outcome,'fulfilled');assert.equal(bus.credentials.modelRecord(agent).retiring.length,0,'unrelated retiring generation completed, current identity actually checked');}
else assert.equal(first.outcome,'proxy-stop-failed','initial corrupt identity refuses before proof');
const client=new ManagementClient({baseUrl:proxy.baseUrl,key:'fx647-manage'});
const loseCurrent=async verb=>{
const old=bus.credentials.modelRecord(agent),before=await client.getKeys();assert.equal(before.includes(old.key),true,'setup current remotely present before deliberate loss');
await client.putKeys(before.filter(key=>key!==old.key));const confirmed=await client.getKeys();assert.equal(confirmed.includes(old.key),false,'real HTTP confirms remote absence');assert.equal(await client.probeKey(old.key),401);
const answer=await manager[verb](agent),after=bus.credentials.modelRecord(agent);
assert.equal(answer.key===old.key,false,'accepted absent-current contract issued fresh generation');assert.equal(after.retiring.some(r=>r.keyId===old.keyId),false,'absent old current not appended');assert.equal(f.state.keys.includes(old.key),false,'old current never restored');assert.equal(f.state.keys.includes(answer.key),true);return after;
};
const laterRetirement=async()=>{
const retiring=bus.credentials.modelRecord(agent);await manager.rotate(agent);const record=bus.credentials.modelRecord(agent);
assert.equal(record.retiring.some(r=>r.keyId===retiring.keyId),true,'later actual rotation appends confirmed current');
bus.credentials.writeModel(agent,{...record,retiring:record.retiring.map(r=>({...r,retireAt:new Date(Date.now()-60_000).toISOString()}))});
return {key:retiring.key,keyId:retiring.keyId};
};
const retained=later=>{assert.equal(f.state.keys.includes(later.key),true,'later generation remains remote');assert.equal(bus.credentials.modelRecord(agent).retiring.some(r=>r.keyId===later.keyId),true,'later retiring metadata retained');};
const retired=later=>{assert.equal(f.state.keys.includes(later.key),false,'later generation retires after exact own outer and runner OS exits');assert.equal(bus.credentials.modelRecord(agent).retiring.some(r=>r.keyId===later.keyId),false);};
// Establish terminal proof only through an associated retiring key and separately
// observed ORIGINAL sweeps, never infer it merely from ChildProcess close.
const acquireProof=async({outerOnly=false}={})=>{
const current=bus.credentials.modelRecord(agent);await manager.rotate(agent);
await f.exitOuter();absent(f.entry.pid);let seen=await f.observeNextSweep();assert.equal(seen.outcome,'fulfilled');assert.equal(f.state.keys.includes(current.key),true,'live runner retains associated key before grace');
assert.equal(startTimeOf(f.entry.runnerPid)===f.entry.runnerStartTime,true,'runner counterpart still exact/live');
if(outerOnly)return;
await f.exitRunner();absent(f.entry.runnerPid);seen=await f.observeNextSweep();assert.equal(seen.outcome,'fulfilled');assert.equal(f.state.keys.includes(current.key),false,'ordinary sweep removed associated key after both strict exits');
};
return {...f,bus,manager,initial,loseCurrent,laterRetirement,retained,retired,acquireProof};
}
for(const verb of ['ensure','rotate'])for(const registry of ['stale','empty'])test(`652 ${verb} absent current then ${registry} registry distinguishes OS exits from acquired proof`,async t=>{
const f=await setup(t);assert.equal(startTimeOf(f.entry.pid)===f.entry.startTime,true);assert.equal(startTimeOf(f.entry.runnerPid)===f.entry.runnerStartTime,true);
await f.loseCurrent(verb);const later=await f.laterRetirement();f.writeSessions(registry==='empty'?[]:[f.entry]);
let seen=await f.observeNextSweep();assert.equal(seen.outcome,'proxy-stop-failed');f.retained(later);
await f.exitOuter();absent(f.entry.pid);seen=await f.observeNextSweep();assert.equal(seen.outcome,'proxy-stop-failed');f.retained(later);assert.equal(startTimeOf(f.entry.runnerPid)===f.entry.runnerStartTime,true,'unbound live runner never signalled');
await f.exitRunner();absent(f.entry.runnerPid);seen=await f.observeNextSweep();
// Desired liveness assertion is a DIAGNOSTIC expectation only until measured and
// owner adjudicated. Predicted RED here is not claimed as an accepted blocker.
f.retired(later);assert.equal(seen.outcome,'fulfilled');
});
for(const verb of ['ensure','rotate'])test(`652 ${verb} absent current with valid no-run inventory retires normally`,async t=>{const f=await setup(t,{noRun:true});await f.loseCurrent(verb);const later=await f.laterRetirement();const seen=await f.observeNextSweep();assert.equal(seen.outcome,'fulfilled');f.retired(later);});
for(const verb of ['ensure','rotate'])test(`652 ${verb} previously acquired pair proof survives later absent-current repair`,async t=>{const f=await setup(t);await f.acquireProof();await f.loseCurrent(verb);const later=await f.laterRetirement();const seen=await f.observeNextSweep();assert.equal(seen.outcome,'fulfilled');f.retired(later);});
test('652 outer proof alone never authorizes stop of live unbound runner after key loss',async t=>{const f=await setup(t);await f.acquireProof({outerOnly:true});await f.loseCurrent('ensure');const later=await f.laterRetirement();const seen=await f.observeNextSweep();assert.equal(seen.outcome,'proxy-stop-failed');f.retained(later);assert.equal(startTimeOf(f.entry.runnerPid)===f.entry.runnerStartTime,true,'live counterpart retained and never signalled');});
for(const field of ['modelKeyId','startTime','runnerStartTime'])test(`652 initial corrupt ${field} stays refused after actual OS exits`,async t=>{const f=await setup(t,{corrupt:field});await f.loseCurrent('ensure');const later=await f.laterRetirement();await f.exit();absent(f.entry.pid);absent(f.entry.runnerPid);const seen=await f.observeNextSweep();assert.equal(seen.outcome,'proxy-stop-failed');f.retained(later);});
test('652 prior pair proof never permits a changed immutable persisted identity',async t=>{const f=await setup(t);await f.acquireProof();await f.loseCurrent('rotate');const later=await f.laterRetirement();f.writeSessions([{...f.entry,modelKeyId:'fedcba987654'}]);const seen=await f.observeNextSweep();assert.equal(seen.outcome,'proxy-stop-failed');f.retained(later);});
@@ -0,0 +1,65 @@
// #652 STATIC preparation: no execution yet. Real runtime, HTTP and original sweeps.
// Interceptions affect ONLY recorded scratch-owned /proc identities; no synthetic signals.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { join } from 'node:path';
import { reviewFixture } from './model-review-fixtures.mjs';
import { Credentials } from '../src/credentials.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
const agent='demo/chief';
function procFault(t,pid,kind){
const original=fs.readFileSync;let active=true,reads=0;
fs.readFileSync=function(file,...args){
if(active&&file===`/proc/${pid}/stat`){reads++;if(kind==='read'){const error=Error('owned proc read refused');error.code='EACCES';throw error;}
const raw=original.call(this,file,'utf8'),end=raw.lastIndexOf(')')+2,fields=raw.slice(end).trim().split(/\s+/);fields[19]=String(BigInt(fields[19])+1n);const changed=raw.slice(0,end)+fields.join(' ')+'\n';return args[0]==='utf8'?changed:Buffer.from(changed);
}
return original.call(this,file,...args);
};syncBuiltinESMExports();
const restore=()=>{active=false;fs.readFileSync=original;syncBuiltinESMExports();};t.after(restore);
return {restore,reads:()=>reads};
}
async function ownPair(t){
const children=[];t.after(async()=>{const failures=[];for(const {child,closed} of children){try{if(child.exitCode===null&&child.signalCode===null)child.kill('SIGTERM');await closed;}catch{failures.push(Error('owned extra child cleanup'));}}if(failures.length)throw new AggregateError(failures,'owned pair cleanup failed');});
for(let i=0;i<2;i++){const child=spawn('/usr/bin/sleep',['120'],{stdio:'ignore'}),closed=new Promise(resolve=>child.once('close',resolve));children.push({child,closed});await once(child,'spawn');}
return {pid:children[0].child.pid,startTime:startTimeOf(children[0].child.pid),runnerPid:children[1].child.pid,runnerStartTime:startTimeOf(children[1].child.pid),exit:async()=>{for(const {child,closed} of children){if(child.exitCode===null&&child.signalCode===null)child.kill('SIGTERM');await closed;}}};
}
const absent=pid=>assert.throws(()=>fs.lstatSync(`/proc/${pid}`),e=>e.code==='ENOENT','own process actually reaped');
async function setup(t,{failure=null}={}){
const f=await reviewFixture(t,{overdue:true}),own=new Credentials({storeRoot:join(f.configDir,'secrets','agents'),dataRoot:f.dataRoot,modelAgents:[agent]});let record;try{record=own.modelRecord(agent);}finally{own.close();}
f.entry.modelKeyId=record.keyId;let entry={...f.entry},extra,fault,binding;
if(failure==='runner ticks')entry.runnerStartTime='0';
if(failure==='later entry')extra=await ownPair(t);
f.writeSessions([entry,...(extra?[{...entry,...extra,exit:undefined,run:'proof-second',modelKeyId:record.retiring[0].keyId}]:[])]);
if(failure==='runner read')fault=procFault(t,entry.runnerPid,'read');
if(extra)fault=procFault(t,extra.runnerPid,'read');
const bus=await f.boot();const proxy=await bus.modelPreflight({business:'demo',role:'chief'});
if(failure==='unpublished bound'||failure==='bound mismatch'){
binding={business:'demo',role:'chief',run:failure==='unpublished bound'?'proof-unpublished':entry.run,harness:'pi',pid:entry.runnerPid,startTime:entry.runnerStartTime,modelKeyId:record.keyId};bus.bindLaunch(binding);
}
const first=await f.observeNextSweep(),manager=f.sweptManager;
assert.equal(first.outcome,failure?'proxy-stop-failed':'fulfilled','first complete checked view outcome');
if(!failure)assert.equal(bus.credentials.modelRecord(agent).retiring.length,0,'associated live view actually completed');
const client=new ManagementClient({baseUrl:proxy.baseUrl,key:'fx647-manage'});
const lose=async()=>{
const old=bus.credentials.modelRecord(agent),before=await client.getKeys();assert.equal(before.includes(old.key),true);await client.putKeys(before.filter(k=>k!==old.key));assert.equal((await client.getKeys()).includes(old.key),false);assert.equal(await client.probeKey(old.key),401);
await manager.ensure(agent);const fresh=bus.credentials.modelRecord(agent);assert.notEqual(fresh.keyId,old.keyId);assert.equal(fresh.retiring.some(r=>r.keyId===old.keyId),false);assert.equal(f.state.keys.includes(old.key),false);
// Keep failure armed until association is gone; no accidental successful view.
if(binding)bus.endLaunch({business:'demo',run:binding.run,reason:'host-lost',exitCode:null});
await manager.rotate(agent);const current=bus.credentials.modelRecord(agent);bus.credentials.writeModel(agent,{...current,retiring:current.retiring.map(r=>({...r,retireAt:new Date(Date.now()-60000).toISOString()}))});return {key:fresh.key,keyId:fresh.keyId};
};
const retained=later=>{assert.equal(f.state.keys.includes(later.key),true,'later remote generation retained');assert.equal(bus.credentials.modelRecord(agent).retiring.some(r=>r.keyId===later.keyId),true,'later local retiring record retained');};
const exit=async()=>{await f.exit();await extra?.exit();absent(entry.pid);absent(entry.runnerPid);if(extra){absent(extra.pid);absent(extra.runnerPid);}};
return {...f,bus,entry,lose,retained,exit,fault};
}
for(const failure of ['runner ticks','runner read','later entry','unpublished bound','bound mismatch'])test(`652 provenance not committed after failed whole view: ${failure}`,async t=>{
const f=await setup(t,{failure}),later=await f.lose();await f.exit();f.fault?.restore();const seen=await f.observeNextSweep();assert.equal(seen.outcome,'proxy-stop-failed','failed first view cannot grant association provenance after key loss');f.retained(later);
});
for(const kind of ['read','ticks'])test(`652 prior validated identity still strictly checks ${kind} after association loss`,async t=>{
const f=await setup(t),later=await f.lose();f.writeSessions([]);const fault=procFault(t,f.entry.runnerPid,kind);const seen=await f.observeNextSweep();assert.equal(seen.outcome,'proxy-stop-failed');f.retained(later);
assert.ok(fault.reads()>0,'prior validated identity is rechecked strictly after association loss');fault.restore();assert.equal(startTimeOf(f.entry.runnerPid),f.entry.runnerStartTime,'live counterpart remains exact and unsignalled');await f.exit();const done=await f.observeNextSweep();assert.equal(done.outcome,'fulfilled');assert.equal(f.state.keys.includes(later.key),false,'strict exits after restored reads permit later retirement');
});
+272
View File
@@ -0,0 +1,272 @@
// P2: real private Credentials, fake loopback management/model server. No live config.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import http from 'node:http';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createHash, randomBytes } from 'node:crypto';
import { Credentials } from '../src/credentials.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { ModelKeys, REVOKE_TIMEOUT_MS, REVOKE_POLL_MS } from '../src/model-keys.mjs';
import { startBroker } from '../src/runtime.mjs';
import { busBusiness } from '../src/business.mjs';
import { Registry } from '../../seat/src/session.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
const hash = s => createHash('sha256').update(s).digest('hex').slice(0, 12);
const failCode = code => e => e.code === code;
async function fixture(t, { delayed = 0 } = {}) {
const root = fs.mkdtempSync(join(tmpdir(), 'p2-keys-')), configDir = join(root, 'config');
const storeRoot = join(configDir, 'secrets', 'agents'); fs.mkdirSync(storeRoot, { recursive: true, mode: 0o700 });
const prefix = `msk-${hash(configDir)}-`, makeKey = () => prefix + randomBytes(32).toString('base64url');
const sentinel = prefix + 'sentinel-' + randomBytes(32).toString('base64url');
const state = { keys: [sentinel], calls: [], delay: delayed, removed: new Map(), failGet: false, failPut: false, hideConfirm: false, probes: 0, status: null };
const server = http.createServer(async (req, res) => {
const management = req.url === '/v8/management/config/access/api-keys';
state.calls.push(management ? req.method : 'PROBE');
if (management) {
if (req.headers.authorization !== 'Bearer fx331-manage') { res.writeHead(401); res.end('{}'); return; }
if ((req.method === 'GET' && state.failGet) || (req.method === 'PUT' && state.failPut)) { res.writeHead(503); res.end('{}'); return; }
if (req.method === 'GET') { res.end(JSON.stringify(state.hideConfirm ? [sentinel] : state.keys)); return; }
const chunks = []; for await (const c of req) chunks.push(c);
const next = JSON.parse(Buffer.concat(chunks));
for (const k of state.keys.filter(k => !next.includes(k))) state.removed.set(k, performance.now());
state.keys = next; res.end('{"status":"ok"}'); return;
}
state.probes++;
const key = req.headers.authorization?.slice(7);
const accepted = state.keys.includes(key) || (state.removed.has(key) && performance.now() - state.removed.get(key) < state.delay);
state.onProbe?.(state.status ?? (accepted ? 200 : 401));
res.writeHead(state.status ?? (accepted ? 200 : 401)); res.end('{}');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`, receipts = [], stopped = [], runs = [];
let now = Date.parse('2026-10-24T00:00:00.000Z');
const credentials = new Credentials({ storeRoot, dataRoot: join(root, 'data'), modelAgents: ['demo/coder', 'demo/other'] });
const client = new ManagementClient({ baseUrl, key: 'fx331-manage', receipt: r => receipts.push(r) });
const create = () => new ModelKeys({ credentials, client, configDir, baseUrl, receipt: r => receipts.push(r), now: () => now,
runs: instance => runs.filter(r => r.instance === instance), stopRuns: async rs => { stopped.push(...rs.map(r => r.run)); for (const r of rs) runs.splice(runs.indexOf(r), 1); } });
const manager = create();
const closers = [];
t.after(async () => { for (const close of closers) await close(); credentials.close(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); fs.rmSync(root, { recursive: true, force: true }); });
return { root, configDir, credentials, client, server, closers, state, manager, create, makeKey, sentinel, receipts, baseUrl, stopped, runs, advance: ms => { now += ms; } };
}
test('issue confirms GET/PUT/GET before private file and returns P3 shape with only own key', async t => {
const f = await fixture(t); await f.manager.reconcile(); f.state.calls.length = 0;
const answer = await f.manager.ensure('demo/coder');
assert.deepEqual(Object.keys(answer).sort(), ['baseUrl', 'key']); assert.equal(answer.baseUrl, f.baseUrl);
assert.deepEqual(f.state.calls, ['GET', 'PUT', 'GET']);
assert.equal(f.state.keys.includes(answer.key), true);
assert.equal(f.credentials.status().find(s => s.instance === 'demo/coder').date, '2026-11-23');
assert.equal(JSON.stringify(f.receipts).includes(answer.key), false);
const other = await f.manager.ensure('demo/other'); assert.equal(other.key === answer.key, false);
});
for (const fault of ['failGet', 'failPut', 'hideConfirm']) test(`proxy-key-issue-failed: ${fault} writes no model record`, async t => {
const f = await fixture(t); await f.manager.reconcile(); f.state[fault] = true;
await assert.rejects(f.manager.ensure('demo/coder'), failCode('proxy-key-issue-failed'));
assert.equal(f.credentials.modelRecord('demo/coder') === null, true);
assert.equal(fs.existsSync(join(f.configDir, 'secrets', 'agents', 'demo', 'coder', 'model')), false);
});
test('credential-revoke-failed: missing confirming GET keeps model file and revoking status', async t => {
const f = await fixture(t); await f.manager.reconcile(); await f.manager.ensure('demo/coder');
f.state.failGet = true;
await assert.rejects(f.manager.prepareRevoke('demo/coder'), failCode('credential-revoke-failed'));
assert.equal(f.credentials.status().find(s => s.instance === 'demo/coder').state, 'revoking');
});
test('revoke confirms absence then polls delayed client refusal before local removal', async t => {
const f = await fixture(t, { delayed: 105 }); await f.manager.reconcile(); const { key } = await f.manager.ensure('demo/coder');
f.state.calls.length = 0; const before = performance.now();
await f.manager.prepareRevoke('demo/coder');
assert.ok(performance.now() - before >= 100); assert.deepEqual(f.state.calls.slice(0, 3), ['GET', 'PUT', 'GET']);
assert.ok(f.state.probes >= 2); assert.equal(f.state.keys.includes(key), false);
assert.equal(f.credentials.modelRecord('demo/coder')?.state, 'revoking');
f.manager.completeRevoke('demo/coder'); assert.equal(f.credentials.modelRecord('demo/coder') === null, true);
assert.equal(JSON.stringify(f.receipts).includes(key), false);
});
test('reconcile restores missing recorded keys, removes own unknown, and preserves foreign and sentinel', async t => {
const f = await fixture(t); await f.manager.reconcile(); const { key } = await f.manager.ensure('demo/coder');
const unknown = f.makeKey(), foreign = 'fx331-human'; f.state.keys = [f.sentinel, unknown, foreign]; f.state.calls.length = 0;
await f.create().reconcile();
assert.equal(f.state.keys.includes(key), true); assert.equal(f.state.keys.includes(unknown), false);
assert.equal(f.state.keys.includes(foreign), true); assert.equal(f.state.keys.includes(f.sentinel), true);
assert.deepEqual(f.state.calls.slice(0, 3), ['GET', 'PUT', 'GET']);
});
test('reconcile revoking never restores key and completes delayed revocation after restart', async t => {
const f = await fixture(t, { delayed: 105 }); await f.manager.reconcile(); const { key } = await f.manager.ensure('demo/coder');
const record = f.credentials.modelRecord('demo/coder');
f.credentials.writeModel('demo/coder', { ...record, state: 'revoking', revokingAt: '2026-10-24T00:00:00.000Z' });
await f.create().reconcile();
assert.equal(f.state.keys.includes(key), false); assert.equal(f.credentials.modelRecord('demo/coder') === null, true);
});
test('proxy-open: no own sentinel blocks reconcile and every launch', async t => {
const f = await fixture(t); f.state.keys = ['fx331-human'];
await assert.rejects(f.manager.reconcile(), failCode('proxy-open'));
await assert.rejects(f.manager.preflight('demo/coder'), failCode('proxy-open'));
assert.equal(f.state.probes, 0);
});
test('proxy-key-revoking: launch hold occurs before any preflight call', async t => {
const f = await fixture(t); await f.manager.reconcile(); await f.manager.ensure('demo/coder');
const record = f.credentials.modelRecord('demo/coder'); f.credentials.writeModel('demo/coder', { ...record, state: 'revoking', revokingAt: '2026-10-24T00:00:00.000Z' });
await assert.rejects(f.manager.preflight('demo/coder'), failCode('proxy-key-revoking')); assert.equal(f.state.probes, 0);
});
test('proxy-revoke-revision-conflict keeps local revoking record and ids-only remote-revoked receipt', async t => {
const f = await fixture(t); await f.manager.reconcile(); const { key } = await f.manager.ensure('demo/coder'); await f.manager.prepareRevoke('demo/coder');
assert.throws(() => f.manager.revisionConflict('demo/coder'), failCode('proxy-revoke-revision-conflict'));
assert.equal(f.credentials.modelRecord('demo/coder').state, 'revoking');
assert.equal(f.state.keys.includes(key), false);
assert.ok(f.receipts.some(r => r.remoteState === 'revoked' && r.localState === 'kept'));
assert.equal(JSON.stringify(f.receipts).includes(key), false);
await f.create().reconcile(); assert.equal(f.credentials.modelRecord('demo/coder') === null, true);
});
for (const status of [401, 503, 302]) test(`proxy-key-refused: preflight rejects HTTP ${status} with exactly one probe`, async t => {
const f = await fixture(t); await f.manager.reconcile(); await f.manager.ensure('demo/coder'); f.state.status = status;
await assert.rejects(f.manager.preflight('demo/coder'), failCode('proxy-key-refused')); assert.equal(f.state.probes, 1);
});
test('rotate keeps old key while its run lives, new launches use new, exit removes old', async t => {
const f = await fixture(t); await f.manager.reconcile(); const first = await f.manager.ensure('demo/coder');
f.runs.push({ instance: 'demo/coder', run: 'old', keyId: hash(first.key) });
const next = await f.manager.rotate('demo/coder'); assert.equal(next.key === first.key, false);
assert.equal(f.state.keys.includes(first.key), true); assert.equal((await f.manager.ensure('demo/coder')).key === next.key, true);
await f.manager.sweep(); assert.equal(f.state.keys.includes(first.key), true);
f.runs.length = 0; await f.manager.sweep(); assert.equal(f.state.keys.includes(first.key), false);
});
test('rotation grace stops surviving old-key runs before remote revocation', async t => {
const f = await fixture(t); await f.manager.reconcile(); const first = await f.manager.ensure('demo/coder');
f.runs.push({ instance: 'demo/coder', run: 'old', keyId: hash(first.key) }); await f.manager.rotate('demo/coder');
f.advance(120 * 60_000); await f.manager.sweep(); assert.deepEqual(f.stopped, ['old']); assert.equal(f.state.keys.includes(first.key), false);
});
test('revocation polling constants retain the approved five-second bound and 25ms spacing', () => {
assert.equal(REVOKE_TIMEOUT_MS, 5000); assert.equal(REVOKE_POLL_MS, 25);
});
test('credential-revoke-failed: polling cannot treat management absence as client revocation', async t => {
const f = await fixture(t, { delayed: Infinity }); await f.manager.reconcile(); await f.manager.ensure('demo/coder');
const start = performance.now();
await assert.rejects(f.manager.prepareRevoke('demo/coder'), failCode('credential-revoke-failed'));
const elapsed = performance.now() - start;
assert.ok(elapsed >= 4900 && elapsed < 6500, 'five-second total revocation polling bound');
assert.equal(f.credentials.modelRecord('demo/coder').state, 'revoking');
});
test('proxy-down: unreachable listener refuses preflight', async t => {
const f = await fixture(t); await f.manager.reconcile(); await f.manager.ensure('demo/coder');
await new Promise(resolve => f.server.close(resolve));
await assert.rejects(f.manager.preflight('demo/coder'), failCode('proxy-down'));
});
test('proxy-stop-failed: old proxy key and retiring record survive a failed grace stop, next sweep retries', async t => {
const f = await fixture(t); await f.manager.reconcile(); const first = await f.manager.ensure('demo/coder');
f.runs.push({ instance: 'demo/coder', run: 'old', keyId: hash(first.key) });
let stopped = false;
const manager = new ModelKeys({ credentials: f.credentials, client: f.client, configDir: f.configDir, baseUrl: f.baseUrl,
now: () => Date.parse('2026-10-24T03:00:00.000Z'), runs: () => f.runs,
stopRuns: async () => { if (!stopped) throw Error('fx331-stop'); f.runs.length = 0; } });
await manager.reconcile(); await manager.rotate('demo/coder');
// This manager created a fresh 120-minute window at 03:00; use a second view
// with time beyond that deadline to exercise failure then retry of the same record.
const overdue = new ModelKeys({ credentials: f.credentials, client: f.client, configDir: f.configDir, baseUrl: f.baseUrl,
now: () => Date.parse('2026-10-24T06:00:00.000Z'), runs: () => f.runs,
stopRuns: async () => { if (!stopped) throw Error('fx331-stop'); f.runs.length = 0; } });
await overdue.reconcile();
await assert.rejects(overdue.sweep(), failCode('proxy-stop-failed'));
assert.equal(f.state.keys.includes(first.key), true); assert.equal(f.credentials.modelRecord('demo/coder').retiring.length, 1);
stopped = true; await overdue.sweep(); assert.equal(f.state.keys.includes(first.key), false);
});
async function bootBusiness(f) {
const rolesDir = rolesCopy(f.root), system = systemFor(f.root), doc = businessDoc(f.root, 'demo');
// Aide is needed because shipped chief hireable:[] and security-reviewer has review.verdict outside HIRE_ALLOWED.
fs.writeFileSync(join(rolesDir, 'aide.md'), '# Aide\n');
writeJson(join(rolesDir, 'aide.json'), {
roleVersion: 2, name: 'aide', title: 'Aide', contract: 'aide.md', tools: ['read'], network: 'api-only',
authority: { withinRole: ['message.send'], crossRole: [] }, credentials: [],
});
const chiefFile = join(rolesDir, 'chief-of-staff.json');
writeJson(chiefFile, { ...JSON.parse(fs.readFileSync(chiefFile, 'utf8')), hireable: ['aide'] });
doc.plugins = { ...doc.plugins, harness: ['pi'] };
doc.roles['sec-1'] = { definition: 'security-reviewer' }; doc.launch.instances.push('sec-1');
const file = writeJson(join(f.configDir, 'businesses', 'demo.json'), doc);
fs.writeFileSync(join(f.configDir, 'secrets', 'proxy-management.key'), 'fx331-manage', { mode: 0o600 });
const business = loadBusiness('demo', { dir: f.configDir, rolesDir }), resolved = {};
for (const instance of Object.keys(business.roles)) resolved[instance] = resolveInstance({ system, business, instance });
fs.mkdirSync(join(f.root, 'data'), { mode: 0o700 });
// Fresh mkdtemp fixture + exclusive mkdir above: no prior session registry can exist.
new Registry(join(f.root, 'data'));
const bus = await startBroker({ dataRoot: join(f.root, 'data'), businesses: { demo: busBusiness(business, resolved) },
organization: { dir: f.configDir, rolesDir, system }, proxy: { port: Number(new URL(f.baseUrl).port), binary: '/fixture-unused' } });
f.closers.push(() => bus.close());
const chief = bus.broker.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' });
bus.broker.request(chief, { verb: 'role.claim' });
const fire = () => bus.broker.request(chief, { verb: 'instance.fire', args: { name: 'sec-1', reason: 'fixture fire' } });
const listed = () => JSON.parse(fs.readFileSync(file, 'utf8')).launch.instances.includes('sec-1');
return { bus, chief, fire, listed, file };
}
test('bus fire proves remote 401 before delisting the agent and deleting model file', async t => {
const f = await fixture(t, { delayed: 105 }); await f.manager.reconcile(); const issued = await f.manager.ensure('demo/sec-1');
const b = await bootBusiness(f); let sawRefusal = false;
f.state.onProbe = status => {
assert.equal(b.listed(), true); assert.equal(b.bus.credentials.modelRecord('demo/sec-1')?.state, 'revoking');
if (status === 401) sawRefusal = true;
};
const result = await b.fire(); f.state.onProbe = null;
assert.equal(sawRefusal, true); assert.equal(result.delisted, true); assert.equal(b.listed(), false);
assert.equal(f.state.keys.includes(issued.key), false); assert.equal(b.bus.credentials.modelRecord('demo/sec-1') === null, true);
});
test('credential-revoke-failed: bus fire on failed remote PUT keeps membership and model record', async t => {
const f = await fixture(t); await f.manager.reconcile(); const issued = await f.manager.ensure('demo/sec-1'); const b = await bootBusiness(f);
const before = fs.readFileSync(b.file, 'utf8'); f.state.failPut = true;
await assert.rejects(b.fire(), failCode('credential-revoke-failed'));
assert.equal(fs.readFileSync(b.file, 'utf8'), before); assert.equal(b.listed(), true);
assert.equal(f.state.keys.includes(issued.key), true); assert.equal(b.bus.credentials.modelRecord('demo/sec-1')?.state, 'revoking');
});
test('proxy-revoke-revision-conflict: racing organization write keeps record, next boot finishes intent', async t => {
const f = await fixture(t); await f.manager.reconcile(); const issued = await f.manager.ensure('demo/sec-1'); const b = await bootBusiness(f);
let raced = false;
f.state.onProbe = status => { if (status === 401 && !raced) { raced = true; fs.appendFileSync(b.file, '\n'); } };
await assert.rejects(b.fire(), failCode('proxy-revoke-revision-conflict')); f.state.onProbe = null;
assert.equal(b.listed(), true); assert.equal(f.state.keys.includes(issued.key), false);
assert.equal(b.bus.credentials.modelRecord('demo/sec-1')?.state, 'revoking');
await b.bus.close(); f.closers.length = 0;
const record = new Credentials({ storeRoot: join(f.configDir, 'secrets', 'agents'), dataRoot: join(f.root, 'data'), modelAgents: ['demo/sec-1'] });
const manager = new ModelKeys({ credentials: record, client: f.client, configDir: f.configDir, baseUrl: f.baseUrl });
await manager.reconcile(); assert.equal(record.modelRecord('demo/sec-1') === null, true); record.close();
});
test('bus hire issues and confirms implicit model key before publishing new membership', async t => {
const f = await fixture(t); const b = await bootBusiness(f); const calls = f.state.calls.length;
const result = await b.bus.broker.request(b.chief, { verb: 'instance.hire', args: { name: 'sec-new', definition: 'aide', reason: 'fixture hire' } });
assert.equal(typeof result.transaction, 'string');
assert.equal(JSON.parse(fs.readFileSync(b.file, 'utf8')).launch.instances.includes('sec-new'), true);
assert.equal(b.bus.credentials.modelRecord('demo/sec-new')?.state, 'valid');
assert.deepEqual(f.state.calls.slice(calls, calls + 3), ['GET', 'PUT', 'GET']);
});
test('proxy-key-issue-failed: bus hire failure publishes neither membership nor model file', async t => {
const f = await fixture(t); const b = await bootBusiness(f); f.state.failPut = true;
const before = fs.readFileSync(b.file, 'utf8');
await assert.rejects(b.bus.broker.request(b.chief, { verb: 'instance.hire', args: { name: 'sec-new', definition: 'aide', reason: 'fixture hire' } }), failCode('proxy-key-issue-failed'));
assert.equal(fs.readFileSync(b.file, 'utf8'), before); assert.equal(b.bus.credentials.modelRecord('demo/sec-new') === null, true);
});
test('proxy-open: removing sentinel after boot refuses the next launch before models probe', async t => {
const f = await fixture(t); await f.manager.reconcile(); await f.manager.ensure('demo/coder');
f.state.keys = f.state.keys.filter(k => k !== f.sentinel);
await assert.rejects(f.manager.preflight('demo/coder'), failCode('proxy-open')); assert.equal(f.state.probes, 0);
});
test('newly hired role can use runtime preflight without a broker restart', async t => {
const f = await fixture(t); const b = await bootBusiness(f);
await b.bus.broker.request(b.chief, { verb: 'instance.hire', args: { name: 'sec-new', definition: 'aide', reason: 'fixture hire' } });
const proxy = await b.bus.modelPreflight({ business: 'demo', role: 'sec-new' });
assert.equal(proxy.baseUrl, f.baseUrl); assert.equal(f.state.keys.includes(proxy.key), true);
});
test('first launch issues implicit key and performs exactly one successful models probe', async t => {
const f = await fixture(t); await f.manager.reconcile();
const answer = await f.manager.preflight('demo/coder');
assert.equal(f.state.probes, 1); assert.equal(f.state.keys.includes(answer.key), true);
});
test('steady boot reconcile reads management list once and writes nothing', async t => {
const f = await fixture(t); await f.manager.reconcile(); assert.deepEqual(f.state.calls, ['GET']);
});
test('thirty-day rotation due automatically replaces and retires an unused old key', async t => {
const f = await fixture(t); await f.manager.reconcile(); const first = await f.manager.ensure('demo/coder');
f.advance(30 * 86400_000); await f.manager.sweep();
const current = await f.manager.ensure('demo/coder'); assert.equal(current.key === first.key, false);
assert.equal(f.state.keys.includes(first.key), false); assert.equal(f.state.keys.includes(current.key), true);
});
@@ -0,0 +1,38 @@
// Task651 test-only real controller/private store/fake HTTP; no live instance.
import fs from 'node:fs';
import http from 'node:http';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createHash, randomBytes } from 'node:crypto';
import { Credentials } from '../src/credentials.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { ModelKeys } from '../src/model-keys.mjs';
import { BusError } from '../src/broker.mjs';
export const id = value => createHash('sha256').update(value).digest('hex').slice(0, 12);
export async function lifecycleFixture(t) {
const root=fs.mkdtempSync(join(tmpdir(),'p2-low-')); let server; const owned=[],resources=[],history=new Set(['fx651-manage']);
t.after(async()=>{const errors=[];for(const close of [...resources].reverse()){try{await close();}catch{errors.push('owned-resource-close');}}for(const c of owned){try{c.close();}catch{errors.push('credential-close');}}try{server?.closeAllConnections();if(server?.listening)await new Promise((resolve,reject)=>server.close(error=>error?reject(error):resolve()));}catch{errors.push('server-close');}try{fs.rmSync(root,{recursive:true,force:true});}catch{errors.push('root-cleanup');}if(errors.length)throw new AggregateError(errors.map(code=>Error(code)),'owned lifecycle cleanup failed');});
const configDir=join(root,'config'),storeRoot=join(configDir,'secrets','agents');fs.mkdirSync(storeRoot,{recursive:true,mode:0o700});
const prefix=`msk-${id(configDir)}-`,sentinel=prefix+'sentinel-'+randomBytes(32).toString('base64url');
history.add(sentinel);
const state={keys:[sentinel],calls:[],failGet:false,failPut:false,stopFailure:true,runs:[],stopAttempts:[],discoveryFaults:new Map(),discoveries:[]},receipts=[];
server=http.createServer(async(req,res)=>{
const management=req.url==='/v8/management/config/access/api-keys';state.calls.push(management?req.method:'PROBE');for(const key of state.keys)history.add(key);const supplied=req.headers.authorization?.slice(7);if(supplied)history.add(supplied);
if(management){
if(req.headers.authorization!=='Bearer fx651-manage'){res.writeHead(401);res.end('{}');return;}
if(req.method==='GET'&&state.failGet){res.writeHead(503);res.end('{}');return;}
if(req.method==='GET'){res.end(JSON.stringify(state.keys));return;}
const chunks=[];for await(const c of req)chunks.push(c);const attempted=JSON.parse(Buffer.concat(chunks));for(const key of attempted)history.add(key);state.observePut?.();if(state.failPut){res.writeHead(503);res.end('{}');return;}state.keys=attempted;res.end('{"status":"ok"}');return;
}
res.writeHead(state.keys.includes(req.headers.authorization?.slice(7))?200:401);res.end('{}');
});await new Promise(r=>server.listen(0,'127.0.0.1',r));
const baseUrl=`http://127.0.0.1:${server.address().port}`;let now=Date.parse('2026-10-25T00:00:00Z');
function create(agents=['demo/coder','demo/other']){
const credentials=new Credentials({storeRoot,dataRoot:join(root,'data'),modelAgents:agents});owned.push(credentials);
const manager=new ModelKeys({credentials,configDir,baseUrl,client:new ManagementClient({baseUrl,key:'fx651-manage',receipt:r=>receipts.push(r)}),receipt:r=>receipts.push(r),now:()=>now,
runs:instance=>{state.discoveries.push(instance);if(state.discoveryFaults.has(instance))throw state.discoveryFaults.get(instance);return state.runs.filter(r=>r.instance===instance);},stopRuns:async runs=>{state.stopAttempts.push(...runs.map(r=>r.run));if(state.stopFailure)throw new BusError('proxy-stop-failed');state.runs=state.runs.filter(r=>!runs.includes(r));}});
return {credentials,manager};
}
const valueFree=(entries=receipts)=>{const text=JSON.stringify(entries);for(const value of history)if(text.includes(value))throw Error('credential leaked');};
return {root,configDir,storeRoot,baseUrl,state,sentinel,receipts,create,valueFree,own:close=>resources.push(close),assertHistoryGuard:()=>{try{valueFree([{canary:sentinel}]);}catch(error){if(error.message==='credential leaked')return;}throw Error('history guard missed canary');},advance:ms=>{now+=ms;}};
}
@@ -0,0 +1,28 @@
// #654 CURRENT reachable branch coverage; expected GREEN, no invented pre-fix RED.
import test from 'node:test';
import assert from 'node:assert/strict';
import { pendingFixture } from './model-pending-fixture.mjs';
test('654 second same-business lifecycle call refuses organization-busy before effects while first completes and gate clears',{timeout:30000},async t=>{
const f=await pendingFixture(t),gate=f.holdNext(),first=f.hire('demo','alpha');await gate.entered();const before=f.snapshot('demo');assert.equal(f.listed('demo','alpha'),false);
await f.busy(f.hire('demo','beta'));assert.deepEqual(f.snapshot('demo'),before,'busy refusal changes no model remote or membership/publication state');assert.equal(f.bus.credentials.modelRecord('demo/beta')===null,true);assert.equal(f.listed('demo','beta'),false);
gate.release();assert.equal(typeof (await first).transaction,'string','first authorized operation remains valid');assert.equal(f.listed('demo','alpha'),true);await f.healthy('demo','alpha');assert.equal(f.listed('demo','beta'),false);
assert.equal(typeof (await f.hire('demo','beta')).transaction,'string','pending gate clears after first successful operation');await f.healthy('demo','beta');f.assertHistoryGuard();f.valueFree();
});
test('654 independent business owns its pending gate while global controller waits then both publish serially',{timeout:30000},async t=>{
const f=await pendingFixture(t,{twoBusinesses:true}),gate=f.holdNext(),first=f.hire('demo','alpha');await gate.entered();const otherBefore=f.snapshot('other'),second=f.hire('other','beta');
// A third call proves OTHER's first call reached its own pending wrapper while
// queued behind the controller; this does not invent concurrent HTTP completion.
await f.busy(f.hire('other','gamma'));assert.deepEqual(f.snapshot('other'),otherBefore);assert.equal(f.listed('other','beta'),false);assert.equal(f.listed('demo','alpha'),false);assert.equal(f.bus.credentials.modelRecord('other/beta')===null,true);
gate.release();assert.equal(typeof (await first).transaction,'string');assert.equal(typeof (await second).transaction,'string');assert.equal(f.listed('demo','alpha'),true);assert.equal(f.listed('other','beta'),true);await f.healthy('demo','alpha');await f.healthy('other','beta');
assert.equal(typeof (await f.hire('other','gamma')).transaction,'string','other pending gate clears independently');f.valueFree();
});
test('654 failed first remote issue releases pending gate without publishing and later authorized call succeeds',{timeout:30000},async t=>{
const f=await pendingFixture(t),gate=f.holdNext(),first=f.hire('demo','alpha');await gate.entered();const before=f.snapshot('demo');await f.busy(f.hire('demo','beta'));assert.deepEqual(f.snapshot('demo'),before);
f.state.failPut=true;gate.release();await assert.rejects(first,e=>e.code==='proxy-key-issue-failed');assert.equal(f.listed('demo','alpha'),false);assert.equal(f.bus.credentials.modelRecord('demo/alpha')===null,true);assert.equal(f.listed('demo','beta'),false);
f.state.failPut=false;assert.equal(typeof (await f.hire('demo','beta')).transaction,'string','error path finally releases business gate');await f.healthy('demo','beta');f.valueFree();
});
test('654 production actor refusal never acquires pending gate or model effects and valid chief still succeeds',{timeout:30000},async t=>{
const f=await pendingFixture(t),cap=f.wrongActor(),before=f.snapshot('demo');await assert.rejects(f.hire('demo','alpha','aide',cap),e=>e.code==='role-not-chief');assert.deepEqual(f.snapshot('demo'),before);assert.equal(f.listed('demo','alpha'),false);
assert.equal(typeof (await f.hire('demo','alpha')).transaction,'string');await f.healthy('demo','alpha');f.valueFree();
});
@@ -0,0 +1,48 @@
// L3b/#651: orphan private file is not proof of current remote membership.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { lifecycleFixture } from './model-lifecycle-fixtures.mjs';
async function orphan(t,{revoking=false,withRetiring=false,currentPresent=false}={}){
const f=await lifecycleFixture(t),old=f.create(['demo/orphan']);await old.manager.reconcile();let answer=await old.manager.ensure('demo/orphan');if(withRetiring)answer=await old.manager.rotate('demo/orphan');const prior=old.credentials.modelRecord('demo/orphan');
if(revoking){const r=old.credentials.modelRecord('demo/orphan');old.credentials.writeModel('demo/orphan',{...r,state:'revoking',revokingAt:'2026-10-25T00:00:00.000Z'});}
old.credentials.close();f.assertHistoryGuard();const fresh=f.create();await fresh.manager.reconcile();assert.equal(f.state.keys.includes(answer.key),false,'boot removed unregistered remote orphan');if(currentPresent)f.state.keys.push(answer.key);
const file=join(f.storeRoot,'demo','orphan','model'),before=fs.readFileSync(file);f.state.calls=[];return {...f,...fresh,answer,file,before,prior};
}
test('L3b removed orphan rehire obtains fresh confirmed credential before publication',async t=>{
const f=await orphan(t);let published=false;const proxy=await f.manager.ensure('demo/orphan');
assert.equal(proxy.key===f.answer.key,false,'removed orphan credential must not be returned');
assert.equal(f.state.keys.includes(proxy.key),true);assert.equal(f.state.keys.includes(f.answer.key),false);published=true;
assert.equal(published,true);assert.deepEqual(f.state.calls,['GET','PUT','GET']);f.valueFree();
});
test('L3b registered orphan revoking state refuses before preflight or mutation',async t=>{
const f=await orphan(t,{revoking:true});await assert.rejects(f.manager.preflight('demo/orphan'),e=>e.code==='proxy-key-revoking','post-register revoking hold required');
assert.equal(f.state.calls.includes('PUT'),false);assert.equal(f.state.calls.includes('PROBE'),false);assert.equal(fs.readFileSync(f.file).equals(f.before),true);f.valueFree();
});
test('L3b orphan fresh issuance failure publishes nothing and preserves old private file',async t=>{
const f=await orphan(t);f.state.failPut=true;let published=false;
await assert.rejects(f.manager.ensure('demo/orphan').then(()=>{published=true;}),e=>e.code==='proxy-key-issue-failed','orphan needs confirmed fresh issuance');
assert.equal(published,false);assert.equal(f.state.keys.includes(f.answer.key),false);assert.equal(fs.readFileSync(f.file).equals(f.before),true);f.valueFree();
});
test('L3b cached orphan never bypasses missing sentinel',async t=>{
const f=await orphan(t);f.state.keys=[];await assert.rejects(f.manager.ensure('demo/orphan'),e=>e.code==='proxy-open','cached record still needs sentinel');assert.equal(f.state.calls.includes('PUT'),false);f.valueFree();
});
test('L3b cached orphan never bypasses unreadable remote membership',async t=>{
const f=await orphan(t);f.state.failGet=true;await assert.rejects(f.manager.ensure('demo/orphan'),e=>e.code==='proxy-key-issue-failed','cached record needs readable membership');assert.equal(f.state.calls.includes('PUT'),false);f.valueFree();
});
test('L3b existing remotely confirmed record is reused without key rotation',async t=>{
const f=await lifecycleFixture(t),c=f.create();await c.manager.reconcile();const first=await c.manager.ensure('demo/coder');f.state.calls=[];const next=await c.manager.ensure('demo/coder');
assert.equal(first.key===next.key,true);assert.equal(f.state.calls.includes('PUT'),false);f.valueFree();
});
for(const verb of ['ensure','rotate'])for(const present of [true,false])test(`L3b ${verb} current-${present?'present':'absent'} preserves prior retiring metadata and approved append rule`,async t=>{
const f=await orphan(t,{withRetiring:true,currentPresent:present});const proxy=await f.manager[verb]('demo/orphan'),record=f.credentials.modelRecord('demo/orphan');
assert.equal(JSON.stringify(record.retiring.slice(0,f.prior.retiring.length))===JSON.stringify(f.prior.retiring),true,'prior retiring identities and times preserved');
assert.equal(record.retiring.length,f.prior.retiring.length+(verb==='rotate'&&present?1:0),'only rotate with remotely present current appends retirement');
assert.equal(proxy.key===f.answer.key,verb==='ensure'&&present,'current reuse requires confirmed present ensure');assert.equal(f.state.keys.includes(proxy.key),true,'returned key confirmed remotely');
if(!present)assert.equal(f.state.keys.includes(f.answer.key),false,'absent old current never restored');
assert.equal(f.state.calls.filter(c=>c==='GET').length,verb==='ensure'&&present?1:2,'fresh serialized list required even for cached reuse');f.valueFree();
});
for(const verb of ['ensure','rotate'])test(`L3b ${verb} without prior record creates no retiring entries`,async t=>{const f=await lifecycleFixture(t),c=f.create();await c.manager.reconcile();const proxy=await c.manager[verb]('demo/coder');assert.equal(c.credentials.modelRecord('demo/coder').retiring.length,0);assert.equal(f.state.keys.includes(proxy.key),true);f.valueFree();});
for(const verb of ['ensure','rotate'])test(`L3b ${verb} known revoking record refuses without management call`,async t=>{const f=await lifecycleFixture(t),c=f.create();await c.manager.reconcile();await c.manager.ensure('demo/coder');const r=c.credentials.modelRecord('demo/coder');c.credentials.writeModel('demo/coder',{...r,state:'revoking',revokingAt:'2026-10-25T00:00:00.000Z'});f.state.calls=[];await assert.rejects(c.manager[verb]('demo/coder'),e=>e.code==='proxy-key-revoking');assert.deepEqual(f.state.calls,[]);f.valueFree();});
@@ -0,0 +1,48 @@
// #651 actual runtime + organization.hire publication with orphan private records.
// In-process trusted Broker, real SQLite/organization writer/HTTP; not process IPC.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { startBroker } from '../src/runtime.mjs';
import { busBusiness } from '../src/business.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
import { lifecycleFixture } from './model-lifecycle-fixtures.mjs';
async function hireFixture(t,{revoking=false}={}){
const f=await lifecycleFixture(t),old=f.create(['demo/orphan']);await old.manager.reconcile();const issued=await old.manager.ensure('demo/orphan');
if(revoking){const r=old.credentials.modelRecord('demo/orphan');old.credentials.writeModel('demo/orphan',{...r,state:'revoking',revokingAt:'2026-10-25T00:00:00.000Z'});}old.credentials.close();
const rolesDir=rolesCopy(f.root),system=systemFor(f.root),doc=businessDoc(f.root,'demo');
// Scratch aide: shipped chief hireable:[] and review.verdict is outside HIRE_ALLOWED.
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});
const chiefFile=join(rolesDir,'chief-of-staff.json');writeJson(chiefFile,{...JSON.parse(fs.readFileSync(chiefFile,'utf8')),hireable:['aide']});doc.plugins={...doc.plugins,harness:['pi']};
const file=writeJson(join(f.configDir,'businesses','demo.json'),doc);fs.writeFileSync(join(f.configDir,'secrets','proxy-management.key'),'fx651-manage',{mode:0o600});
const business=loadBusiness('demo',{dir:f.configDir,rolesDir}),resolved={};for(const instance of Object.keys(business.roles))resolved[instance]=resolveInstance({system,business,instance});
const dataRoot=join(f.root,'data');fs.mkdirSync(dataRoot,{mode:0o700});
// Capture only value-free runtime proxy receipts; never print a leaked value.
const log=[],original=process.stderr.write;process.stderr.write=function(chunk,...args){if(String(chunk).startsWith('model-proxy: ')){log.push(String(chunk));const callback=args.find(a=>typeof a==='function');callback?.();return true;}return original.call(this,chunk,...args);};f.own(()=>{process.stderr.write=original;});
const bus=await startBroker({dataRoot,businesses:{demo:busBusiness(business,resolved)},organization:{dir:f.configDir,rolesDir,system},proxy:{port:Number(new URL(f.baseUrl).port),binary:'/fixture-unused'}});
f.own(()=>bus.close());
assert.equal(f.state.keys.includes(issued.key),false,'runtime boot reconciled omitted orphan');assert.equal(bus.credentials.modelRecord('demo/orphan')===null,true,'orphan remains unregistered before hire');
const cap=bus.broker.bindLaunch({business:'demo',role:'chief',run:'orphan-hire-chief',harness:'pi'});bus.broker.request(cap,{verb:'role.claim'});
const listed=()=>JSON.parse(fs.readFileSync(file,'utf8')).launch.instances.includes('orphan'),observed=[];f.state.observePut=()=>observed.push(listed());f.state.calls=[];
const hire=()=>bus.broker.request(cap,{verb:'instance.hire',args:{name:'orphan',definition:'aide',reason:'fixture orphan hire'}});
return {...f,bus,issued,file,log,listed,observed,hire};
}
test('L3b runtime hire publishes orphan only after fresh remote confirmation',async t=>{
const f=await hireFixture(t);const result=await f.hire();assert.equal(typeof result.transaction,'string');const record=f.bus.credentials.modelRecord('demo/orphan');
assert.equal(record.key===f.issued.key,false,'runtime hire must replace removed orphan credential');assert.equal(f.state.keys.includes(record.key),true,'published member has confirmed remote key');assert.equal(f.listed(),true);
assert.ok(f.observed.length>0,'actual fresh issue reached proxy PUT');assert.equal(f.observed.every(published=>published===false),true,'membership absent during remote writes');assert.deepEqual(f.state.calls.slice(0,3),['GET','PUT','GET']);
const proxy=await f.bus.modelPreflight({business:'demo',role:'orphan'});assert.equal(proxy.key===record.key,true);f.valueFree(f.log);f.valueFree();
});
test('L3b runtime orphan fresh PUT failure preserves membership exactly',async t=>{
const f=await hireFixture(t),before=fs.readFileSync(f.file);f.state.failPut=true;
await assert.rejects(f.hire(),e=>e.code==='proxy-key-issue-failed','runtime orphan issue failure refuses before membership publication');
assert.equal(fs.readFileSync(f.file).equals(before),true);assert.equal(f.listed(),false);assert.equal(f.state.keys.includes(f.issued.key),false);assert.ok(f.observed.length>0,'rejected fresh PUT observed');assert.equal(f.observed.every(published=>published===false),true,'membership absent through failed issue and existing cleanup');f.valueFree(f.log);f.valueFree();
});
test('L3b runtime unregistered revoking orphan cannot publish membership',async t=>{
const f=await hireFixture(t,{revoking:true}),before=fs.readFileSync(f.file);
await assert.rejects(f.hire(),e=>e.code==='proxy-key-revoking','runtime post-register revoking hold refuses hire');
assert.equal(fs.readFileSync(f.file).equals(before),true);assert.equal(f.listed(),false);assert.equal(f.state.keys.includes(f.issued.key),false);f.valueFree(f.log);f.valueFree();
});
@@ -0,0 +1,53 @@
// #654 isolated real runtime/writer/controller with owned HTTP request gates.
import fs from 'node:fs';
import http from 'node:http';
import assert from 'node:assert/strict';
import { createHash, randomBytes } from 'node:crypto';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { startBroker } from '../src/runtime.mjs';
import { busBusiness } from '../src/business.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
import { sessionsFile, SESSIONS_VERSION } from '../../seat/src/session.mjs';
const hash = value => createHash('sha256').update(value).digest('hex');
const deferred = () => {let resolve;const promise=new Promise(r=>{resolve=r;});return {promise,resolve};};
export async function bounded(promise,message,ms=5000){let timer;try{return await Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error(message)),ms);})]);}finally{clearTimeout(timer);}}
export async function pendingFixture(t,{twoBusinesses=false}={}){
const root=fs.mkdtempSync(join(tmpdir(),'p654-')),closers=[],active=new Set(),gates=[],history=new Set(['fx654-manage']),receipts=[];
let server,bus,stderr;const state={keys:[],calls:[],failPut:false,nextGate:null};
const valueFree=(entries=receipts)=>{const text=JSON.stringify(entries);for(const value of history)if(text.includes(value))throw Error('fixture credential leaked');};
t.after(async()=>{const failures=[];for(const g of gates)g.release.resolve();try{await bounded(Promise.allSettled([...active]),'owned operations did not settle');}catch{failures.push('owned-operation-close');}for(const close of [...closers].reverse()){try{await close();}catch{failures.push('owned-resource-close');}}try{server?.closeAllConnections();if(server?.listening)await new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));}catch{failures.push('owned-server-close');}try{valueFree();}catch{failures.push('value-free-receipts');}if(stderr)process.stderr.write=stderr;try{fs.rmSync(root,{recursive:true,force:true});}catch{failures.push('owned-root-close');}if(failures.length)throw new AggregateError(failures.map(code=>Error(code)),'owned pending fixture cleanup failed');});
const configDir=join(root,'config'),dataRoot=join(root,'data'),rolesDir=rolesCopy(root),system=systemFor(root);fs.mkdirSync(dataRoot,{mode:0o700});
const doc=businessDoc(root,'demo');doc.plugins={...doc.plugins,harness:['pi']};
// Existing row331 scratch aide pattern only; repository role authority unchanged.
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});const chiefFile=join(rolesDir,'chief-of-staff.json');writeJson(chiefFile,{...JSON.parse(fs.readFileSync(chiefFile,'utf8')),hireable:['aide']});
const ids=twoBusinesses?['demo','other']:['demo'],files={},businesses={};for(const name of ids){const document={...structuredClone(doc),id:name};files[name]=writeJson(join(configDir,'businesses',name+'.json'),document);const business=loadBusiness(name,{dir:configDir,rolesDir}),resolved={};for(const instance of Object.keys(business.roles))resolved[instance]=resolveInstance({system,business,instance});businesses[name]=busBusiness(business,resolved);}
fs.mkdirSync(join(configDir,'secrets'),{recursive:true,mode:0o700});fs.writeFileSync(join(configDir,'secrets','proxy-management.key'),'fx654-manage',{mode:0o600});fs.mkdirSync(join(dataRoot,'bus-host'),{recursive:true,mode:0o700});writeJson(sessionsFile(dataRoot),{sessionsVersion:SESSIONS_VERSION,sessions:[]});
const sentinel=`msk-${hash(configDir).slice(0,12)}-sentinel-${randomBytes(32).toString('base64url')}`;state.keys.push(sentinel);history.add(sentinel);
server=http.createServer(async(req,res)=>{try{
const supplied=req.headers.authorization?.slice(7);if(supplied)history.add(supplied);for(const key of state.keys)history.add(key);
const management=req.url==='/v8/management/config/access/api-keys';state.calls.push(management?req.method:'PROBE');let attempted;
if(management&&req.method==='PUT'){const chunks=[];for await(const chunk of req)chunks.push(chunk);attempted=JSON.parse(Buffer.concat(chunks));for(const key of attempted)history.add(key);}
// Faults/gates are ONLY the owned fake HTTP transport boundary. Existing
// ModelKeys, pending Map, writer locks and publication remain untouched.
const g=state.nextGate;if(g&&management&&req.method===g.method){state.nextGate=null;g.entered.resolve();await g.release.promise;}
if(res.destroyed)return;
if(management){if(supplied!=='fx654-manage'){res.writeHead(401);res.end('{}');return;}if(req.method==='GET'){res.end(JSON.stringify(state.keys));return;}if(state.failPut){res.writeHead(503);res.end('{}');return;}state.keys=attempted;res.end('{"status":"ok"}');return;}
res.writeHead(state.keys.includes(supplied)?200:401);res.end('{}');
}catch{if(!res.destroyed){res.writeHead(500);res.end('{}');}}});await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});assert.notEqual(server.address().port,8317);
stderr=process.stderr.write;process.stderr.write=function(chunk,...args){if(String(chunk).startsWith('model-proxy: ')){receipts.push(String(chunk));const callback=args.find(a=>typeof a==='function');callback?.();return true;}return stderr.call(this,chunk,...args);};
bus=await startBroker({dataRoot,businesses,organization:{dir:configDir,rolesDir,system},proxy:{port:server.address().port,binary:'/fixture-unused'}});closers.push(()=>bus.close());
const chief={};for(const name of ids){chief[name]=bus.broker.bindLaunch({business:name,role:'chief',run:'pending-'+name,harness:'pi'});bus.broker.request(chief[name],{verb:'role.claim'});}
const track=promise=>{active.add(promise);promise.then(()=>active.delete(promise),()=>active.delete(promise));return promise;};
const holdNext=(method='GET')=>{assert.equal(state.nextGate,null,'only one owned HTTP hold armed');const g={method,entered:deferred(),release:deferred()};gates.push(g);state.nextGate=g;return {entered:()=>bounded(g.entered.promise,'owned lifecycle HTTP boundary not observed'),release:()=>g.release.resolve()};};
const listed=(business,name)=>JSON.parse(fs.readFileSync(files[business],'utf8')).launch.instances.includes(name);
const snapshot=business=>{const journal=join(configDir,'businesses','.history',business,'changes.jsonl');return {membership:hash(fs.readFileSync(files[business])),publicationJournal:fs.existsSync(journal)?hash(fs.readFileSync(journal)):null,modelStatus:hash(JSON.stringify(bus.credentials.status())),remote:hash(JSON.stringify(state.keys)),calls:state.calls.length};};
const hire=(business,name,definition='aide',cap=chief[business])=>track(Promise.resolve().then(()=>bus.broker.request(cap,{verb:'instance.hire',args:{name,definition,reason:'fixture pending hire'}})));
const busy=async promise=>{const result=await bounded(promise.then(()=>({code:'unexpected-success'}),e=>({code:e.code??'unexpected-error'})),'same-business refusal not observed');assert.equal(result.code,'organization-busy','second same-business lifecycle call refuses exact pending code');};
return {root,configDir,dataRoot,rolesDir,bus,state,receipts,chief,holdNext,listed,snapshot,hire,busy,valueFree,
healthy:async(business,name)=>{const value=await bus.modelPreflight({business,role:name});const record=bus.credentials.modelRecord(business+'/'+name);assert.equal(value.key===record.key,true);assert.equal(state.keys.includes(record.key),true,'published member retains confirmed model credential');},
wrongActor:()=>{const cap=bus.broker.bindLaunch({business:'demo',role:'coder',run:'pending-coder',harness:'pi'});bus.broker.request(cap,{verb:'role.claim'});return cap;},
assertHistoryGuard:()=>{try{valueFree([{canary:sentinel}]);}catch(e){if(e.message==='fixture credential leaked')return;}throw Error('fixture history guard failed');}};
}
+22
View File
@@ -14,3 +14,25 @@ test('proxy.init not-configured',t=>{const f=fixture(t,null);assert.throws(()=>f
test('proxy.init callback error redacts key and maps outcome to named refusal',async t=>{const f=fixture(t,()=>{throw Error('p1-management');});await assert.rejects(f.b.request(f.human,{verb:'proxy.init',args:{key:'p1-management'}}),e=>e.code==='proxy-init-failed'&&!e.message.includes('p1-management'));assert.equal(JSON.stringify(f.store.all('SELECT body FROM events')).includes('p1-management'),false);}); test('proxy.init callback error redacts key and maps outcome to named refusal',async t=>{const f=fixture(t,()=>{throw Error('p1-management');});await assert.rejects(f.b.request(f.human,{verb:'proxy.init',args:{key:'p1-management'}}),e=>e.code==='proxy-init-failed'&&!e.message.includes('p1-management'));assert.equal(JSON.stringify(f.store.all('SELECT body FROM events')).includes('p1-management'),false);});
test('socket awaits proxy.init and does not return a Promise object or retain human cap',async t=>{const {serve}=await import('../src/server.mjs');const {Client}=await import('../src/client.mjs');const f=fixture(t,async()=>{await new Promise(r=>setTimeout(r,10));return {port:18432,binary:'/fixture'};});const root=mkdtempSync(join(tmpdir(),'p1-sock-'));const socket=join(root,'s');const server=await serve({broker:f.b,path:socket,peerReader:{read:async()=>({pid:process.pid,uid:process.getuid(),gid:process.getgid()}),close(){}},authenticateHuman:()=>f.b.bindHuman({business:'demo',human:'owner',via:'cli',outsideAgent:true})});t.after(()=>server.close());const client=new Client({path:socket,human:{business:'demo',pid:process.pid,startTime:'1',nonce:'fixture'}});assert.deepEqual(await client.call('proxy.init',{key:'p1-management'}),{port:18432,binary:'/fixture'});}); test('socket awaits proxy.init and does not return a Promise object or retain human cap',async t=>{const {serve}=await import('../src/server.mjs');const {Client}=await import('../src/client.mjs');const f=fixture(t,async()=>{await new Promise(r=>setTimeout(r,10));return {port:18432,binary:'/fixture'};});const root=mkdtempSync(join(tmpdir(),'p1-sock-'));const socket=join(root,'s');const server=await serve({broker:f.b,path:socket,peerReader:{read:async()=>({pid:process.pid,uid:process.getuid(),gid:process.getgid()}),close(){}},authenticateHuman:()=>f.b.bindHuman({business:'demo',human:'owner',via:'cli',outsideAgent:true})});t.after(()=>server.close());const client=new Client({path:socket,human:{business:'demo',pid:process.pid,startTime:'1',nonce:'fixture'}});assert.deepEqual(await client.call('proxy.init',{key:'p1-management'}),{port:18432,binary:'/fixture'});});
test('proxy-init-failed: extra callback fields cannot return a management key',async t=>{const f=fixture(t,async()=>({port:18432,binary:'/fixture',key:'p1-management'}));await assert.rejects(f.b.request(f.human,{verb:'proxy.init',args:{key:'p1-management'}}),e=>e.code==='proxy-init-failed');assert.equal(JSON.stringify(f.store.all('SELECT body FROM events')).includes('p1-management'),false);}); test('proxy-init-failed: extra callback fields cannot return a management key',async t=>{const f=fixture(t,async()=>({port:18432,binary:'/fixture',key:'p1-management'}));await assert.rejects(f.b.request(f.human,{verb:'proxy.init',args:{key:'p1-management'}}),e=>e.code==='proxy-init-failed');assert.equal(JSON.stringify(f.store.all('SELECT body FROM events')).includes('p1-management'),false);});
// P2 R2-N1: catches local broker predicates diverging from management-client rules.
for (const [label, key] of [['Latin-1', 'fx331-caf\u00e9'], ['leading space', ' fx331-key'], ['trailing space', 'fx331-key '], ['internal space', 'fx331 key']]) {
test(`proxy-key-invalid: broker rejects ${label} before initializer`, async t => {
let called = false;
const f = fixture(t, () => { called = true; return { port: 18432, binary: '/fixture' }; });
let pending;
try {
assert.throws(() => { pending = f.b.request(f.human, { verb: 'proxy.init', args: { key } }); }, e => e.code === 'proxy-key-invalid');
} finally { await Promise.resolve(pending).catch(() => {}); }
assert.equal(called, false);
assert.equal(JSON.stringify(f.store.all('SELECT body FROM events')).includes(key), false);
});
}
for (const [field, code, value] of [['rotateGraceMinutes', 'proxy-rotate-grace-invalid', 1441], ['firstOutputSeconds', 'proxy-first-output-invalid', 3601]]) {
test(`proxy.init ${code}: human setting validated before initializer`, t => {
let called = false; const f = fixture(t, () => { called = true; });
assert.throws(() => f.b.request(f.human, { verb: 'proxy.init', args: { key: 'fx331-manage', [field]: value } }), e => e.code === code);
assert.equal(called, false);
});
}
+158
View File
@@ -0,0 +1,158 @@
// B1 / 647: real runtime timer, scratch proxy and persisted sessions, never the host instance.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import http from 'node:http';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createHash, randomBytes } from 'node:crypto';
import { setTimeout as wait } from 'node:timers/promises';
import { startBroker } from '../src/runtime.mjs';
import { Credentials } from '../src/credentials.mjs';
import { ModelKeys } from '../src/model-keys.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { busBusiness } from '../src/business.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { businessDoc, rolesCopy, systemFor, systemConfig, writeJson } from '../../business/tests/helpers.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { Registry, SESSIONS_VERSION, sessionsFile } from '../../seat/src/session.mjs';
import { createLauncher } from '../../cli/src/launcher.mjs';
const id = value => createHash('sha256').update(value).digest('hex').slice(0, 12);
async function fixture(t, { overdue = false, exited = false } = {}) {
const root = fs.mkdtempSync(join(tmpdir(), 'b1-'));
let server, credentials, child, childClosed, bus, launcher;
let completed = 0;
const originalSweep = ModelKeys.prototype.sweep;
const exit = async () => {
// Only the ChildProcess this fixture spawned: never use a registry-supplied PID.
if (child?.pid && child.exitCode === null && child.signalCode === null) child.kill('SIGTERM');
if (childClosed) await childClosed;
};
t.after(async () => {
const errors = [];
for (const close of [() => launcher?.close(), () => bus?.close(), () => credentials?.close(), exit,
async () => { server?.closeAllConnections(); if (server?.listening) await new Promise(resolve => server.close(resolve)); }]) {
try { await close(); } catch (error) { errors.push(error); }
}
ModelKeys.prototype.sweep = originalSweep;
fs.rmSync(root, { recursive: true, force: true });
if (errors.length) throw new AggregateError(errors, 'scratch fixture cleanup failed');
});
// Await the ORIGINAL sweep, including its real HTTP/filesystem work. Count both
// fulfilled and rejected completions; the runtime still handles its own refusal.
ModelKeys.prototype.sweep = async function (...args) {
try { return await originalSweep.apply(this, args); }
finally { completed++; }
};
const observeSweep = async () => {
const before = completed, deadline = Date.now() + 5000;
// Same five-second bounded wait budget as bus/tests/host-run.test.mjs:47.
// Production interval remains 1000ms; no clock or scheduler is replaced.
while (completed <= before && Date.now() < deadline) await wait(20);
assert.ok(completed > before, 'original runtime sweep completed within five seconds');
};
const configDir = join(root, 'config'), dataRoot = join(root, 'data');
fs.mkdirSync(dataRoot, { mode: 0o700 });
const config = systemConfig(root), system = systemFor(root), rolesDir = rolesCopy(root);
const doc = businessDoc(root, 'demo'); doc.plugins = { ...doc.plugins, harness: ['pi'] };
writeJson(join(configDir, 'businesses', 'demo.json'), doc);
const business = loadBusiness('demo', { dir: configDir, rolesDir }), resolved = {};
for (const instance of Object.keys(business.roles)) resolved[instance] = resolveInstance({ system, business, instance });
const storeRoot = join(configDir, 'secrets', 'agents'); fs.mkdirSync(storeRoot, { recursive: true, mode: 0o700 });
fs.writeFileSync(join(configDir, 'secrets', 'proxy-management.key'), 'fx647-manage', { mode: 0o600 });
const prefix = `msk-${id(configDir)}-`;
const state = { keys: [prefix + 'sentinel-' + randomBytes(32).toString('base64url')], put: 0, removed: 0 };
server = http.createServer(async (req, res) => {
if (req.url === '/v8/management/config/access/api-keys') {
if (req.headers.authorization !== 'Bearer fx647-manage') { res.writeHead(401); res.end('{}'); return; }
if (req.method === 'GET') { res.end(JSON.stringify(state.keys)); return; }
const chunks = []; for await (const chunk of req) chunks.push(chunk);
const next = JSON.parse(Buffer.concat(chunks));
state.removed += state.keys.filter(key => !next.includes(key)).length;
state.keys = next; state.put++; res.end(JSON.stringify({ status: 'ok' })); return;
}
res.writeHead(state.keys.includes(req.headers.authorization?.slice(7)) ? 200 : 401); res.end('{}');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
credentials = new Credentials({ storeRoot, dataRoot, modelAgents: ['demo/chief'] });
const manager = new ModelKeys({ credentials, configDir, baseUrl, client: new ManagementClient({ baseUrl, key: 'fx647-manage' }) });
await manager.reconcile(); const first = await manager.ensure('demo/chief'); await manager.rotate('demo/chief');
const record = credentials.modelRecord('demo/chief');
credentials.writeModel('demo/chief', { ...record, retiring: record.retiring.map(r => ({ ...r, retireAt: new Date(Date.now() + (overdue ? -60_000 : 3_600_000)).toISOString() })) });
credentials.close(); credentials = null; state.put = 0; state.removed = 0;
// A harmless scratch-owned process supplies real /proc identity. It is deliberately
// not a session runner, so stopSession must refuse rather than signal this stand-in.
child = spawn('/usr/bin/sleep', ['60'], { stdio: 'ignore' });
childClosed = new Promise(resolve => child.once('close', resolve));
await once(child, 'spawn');
const ticks = startTimeOf(child.pid); assert.equal(typeof ticks, 'string');
const entry = { business: 'demo', instance: 'chief', run: 'r647-old', harness: 'pi', provider: 'fake', model: 'fake',
pid: child.pid, startTime: ticks, runnerPid: child.pid, runnerStartTime: ticks, modelKeyId: id(first.key), launchedBy: 'chief' };
const writeSessions = entries => writeJson(sessionsFile(dataRoot), { sessionsVersion: SESSIONS_VERSION, sessions: entries });
writeSessions([entry]);
if (exited) await exit();
const boot = async () => {
bus = await startBroker({ dataRoot, businesses: { demo: busBusiness(business, resolved) }, organization: { dir: configDir, rolesDir, system },
proxy: { port: server.address().port, binary: '/fixture-unused' } });
return bus;
};
const noLaunch = async () => {
const ops = [];
const host = { business: 'demo', beforeClose() {}, bindLaunch: async r => { ops.push('bindLaunch'); return bus.bindLaunch(r); },
op: async r => { ops.push(r.op); throw Error('fixture-unrequested-op'); } };
launcher = createLauncher({ host, system: JSON.parse(fs.readFileSync(config, 'utf8')), launches: false, env: { MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: rolesDir, PATH: '/usr/bin' }, log: () => {} });
await launcher.listen(); return ops;
};
const retained = () => {
assert.equal(state.keys.includes(first.key), true, 'old key stays remotely installed');
assert.equal(bus.credentials.modelRecord('demo/chief').retiring.some(r => r.keyId === entry.modelKeyId), true, 'retiring id stays locally recorded');
assert.equal(state.removed, 0, 'no remote key removal');
};
return { root, dataRoot, state, entry, writeSessions, boot, noLaunch, exit, retained, observeSweep,
alive: () => startTimeOf(child.pid) === ticks,
absent: () => { assert.equal(state.keys.includes(first.key), false); assert.equal(bus.credentials.modelRecord('demo/chief').retiring.length, 0); },
bind: () => bus.bindLaunch({ business: entry.business, role: entry.instance, run: entry.run, harness: entry.harness, pid: entry.pid, startTime: entry.startTime, modelKeyId: entry.modelKeyId }) };
}
test('B1 restart before grace discovers persisted old-key run without bindLaunch', async t => {
const f = await fixture(t); await f.boot(); await f.observeSweep(); f.retained(); assert.equal(f.alive(), true);
});
test('B1 no-launch restart past grace neither signals nor ends a prior session', async t => {
const f = await fixture(t, { overdue: true }); const before = fs.readFileSync(sessionsFile(f.dataRoot), 'utf8');
await f.boot(); const ops = await f.noLaunch(); await f.observeSweep();
f.retained(); assert.equal(f.alive(), true); assert.equal(ops.includes('bindLaunch') || ops.includes('endLaunch'), false);
assert.equal(fs.readFileSync(sessionsFile(f.dataRoot), 'utf8'), before);
});
test('B1 normal recovery gap retains boot inventory when Registry clears before rebinding', async t => {
const f = await fixture(t); await f.boot(); new Registry(f.dataRoot); await f.observeSweep(); f.retained(); assert.equal(f.alive(), true);
});
for (const corruption of ['malformed', 'missing']) test(`B1 ${corruption} persisted discovery never authorizes retirement`, async t => {
const f = await fixture(t, { overdue: true });
if (corruption === 'malformed') fs.writeFileSync(sessionsFile(f.dataRoot), '{'); else fs.unlinkSync(sessionsFile(f.dataRoot));
await f.boot(); await f.observeSweep(); f.retained(); assert.equal(f.alive(), true);
});
for (const field of ['startTime', 'runnerStartTime', 'modelKeyId']) test(`B1 persisted identity disagreement in ${field} retains key and record`, async t => {
const f = await fixture(t, { overdue: true });
f.writeSessions([{ ...f.entry, [field]: field === 'modelKeyId' ? 'fedcba987654' : '0' }]);
await f.boot(); await f.observeSweep(); f.retained(); assert.equal(f.alive(), true);
});
test('B1 discovery failure after boot retains the cached live old-key inventory', async t => {
const f = await fixture(t, { overdue: true }); await f.boot(); fs.writeFileSync(sessionsFile(f.dataRoot), '{');
await f.observeSweep(); f.retained(); assert.equal(f.alive(), true);
});
test('B1 bound run with unauthorized runner stop retains key and retiring record', async t => {
const f = await fixture(t, { overdue: true }); await f.boot(); f.bind(); await f.observeSweep(); f.retained(); assert.equal(f.alive(), true);
});
test('B1 confirmed persisted runner exit permits old-key retirement after grace', async t => {
const f = await fixture(t, { overdue: true, exited: true }); await f.boot(); await f.observeSweep(); f.absent();
});
test('B1 an observed persisted exit after restart permits retirement, never an empty registry alone', async t => {
const f = await fixture(t, { overdue: true }); await f.boot(); await f.observeSweep(); f.retained();
await f.exit(); await f.observeSweep(); f.absent();
});
@@ -0,0 +1,86 @@
// B1/647: discovery changes while real retirement HTTP is in flight.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import http from 'node:http';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createHash, randomBytes } from 'node:crypto';
import { Credentials } from '../src/credentials.mjs';
import { BusError } from '../src/broker.mjs';
import { ModelKeys } from '../src/model-keys.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
const id = value => createHash('sha256').update(value).digest('hex').slice(0, 12);
async function fixture(t, boundary) {
const root = fs.mkdtempSync(join(tmpdir(), 'b1-boundary-'));
let server, credentials;
t.after(async () => {
try { credentials?.close(); }
finally {
server?.closeAllConnections();
if (server?.listening) await new Promise(resolve => server.close(resolve));
fs.rmSync(root, { recursive: true, force: true });
}
});
const configDir = join(root, 'config'), storeRoot = join(configDir, 'secrets', 'agents');
fs.mkdirSync(storeRoot, { recursive: true, mode: 0o700 });
const sentinel = `msk-${id(configDir)}-sentinel-${randomBytes(32).toString('base64url')}`;
const state = { keys: [sentinel], armed: false, discoveryFailed: false, calls: [], faults: 0, refusals: 0 };
const receipts = [];
server = http.createServer(async (req, res) => {
const management = req.url === '/v8/management/config/access/api-keys';
state.calls.push(management ? req.method : 'PROBE');
if (management) {
if (req.headers.authorization !== 'Bearer fx647-manage') { res.writeHead(401); res.end('{}'); return; }
if (req.method === 'GET') {
if (state.armed && boundary === 'management-GET' && !state.discoveryFailed) { state.discoveryFailed = true; state.faults++; }
res.end(JSON.stringify(state.keys)); return;
}
const chunks = []; for await (const chunk of req) chunks.push(chunk);
state.keys = JSON.parse(Buffer.concat(chunks));
res.end(JSON.stringify({ status: 'ok' })); return;
}
const status = state.keys.includes(req.headers.authorization?.slice(7)) ? 200 : 401;
if (state.armed && boundary === 'confirmed-401' && status === 401) { state.discoveryFailed = true; state.faults++; }
res.writeHead(status); res.end('{}');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
credentials = new Credentials({ storeRoot, dataRoot: join(root, 'data'), modelAgents: ['demo/chief'] });
const manager = new ModelKeys({ credentials, configDir, baseUrl,
client: new ManagementClient({ baseUrl, key: 'fx647-manage', receipt: entry => receipts.push(entry) }),
receipt: entry => receipts.push(entry),
// Fault models authoritative discovery becoming uncertain. Do not replace the
// controller, mutation method, HTTP operation or private-record implementation.
runs: () => { if (state.discoveryFailed) { state.refusals++; throw new BusError('proxy-stop-failed'); } return []; } });
await manager.reconcile(); const first = await manager.ensure('demo/chief'); await manager.rotate('demo/chief');
state.calls.length = 0; receipts.length = 0; state.armed = true;
return { state, receipts, credentials, manager, oldKey: first.key, oldId: id(first.key),
retained: () => assert.equal(credentials.modelRecord('demo/chief').retiring.some(r => r.keyId === id(first.key)), true, 'local retiring record retained'),
valueFree: () => {
const output = JSON.stringify(receipts);
assert.equal(output.includes(first.key), false);
assert.equal(output.includes('fx647-manage'), false);
assert.equal(output.includes(credentials.modelRecord('demo/chief').key), false);
} };
}
test('B1 discovery failure during fresh management GET refuses before retirement PUT', async t => {
const f = await fixture(t, 'management-GET');
await assert.rejects(f.manager.sweep(), error => error.code === 'proxy-stop-failed', 'fresh discovery refusal required before remote mutation');
assert.equal(f.state.faults, 1); assert.equal(f.state.refusals, 1);
assert.deepEqual(f.state.calls, ['GET'], 'no removal PUT or extra mutation after discovery failure');
assert.equal(f.state.keys.includes(f.oldKey), true, 'remote old key retained');
f.retained(); f.valueFree();
});
test('B1 discovery failure during confirmed401 refuses before retiring metadata deletion', async t => {
const f = await fixture(t, 'confirmed-401');
await assert.rejects(f.manager.sweep(), error => error.code === 'proxy-stop-failed', 'fresh discovery refusal required before local deletion');
assert.equal(f.state.faults, 1); assert.equal(f.state.refusals, 1);
assert.deepEqual(f.state.calls, ['GET', 'PUT', 'GET', 'PROBE'], 'one remote revoke, no rollback or extra mutation');
assert.equal(f.state.keys.includes(f.oldKey), false, 'remote key already revoked; no rollback claim');
assert.ok(f.receipts.some(entry => entry.call === 'GET /v1/models' && entry.status === 401 && entry.keyIds.includes(f.oldId)), 'observed remote401 receipt');
f.retained(); f.valueFree();
});
@@ -0,0 +1,156 @@
// Test-only review fixtures cloned from B1; old fixture/assertions remain unchanged.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import http from 'node:http';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createHash, randomBytes } from 'node:crypto';
import { setTimeout as wait } from 'node:timers/promises';
import { startBroker } from '../src/runtime.mjs';
import { Credentials } from '../src/credentials.mjs';
import { ModelKeys } from '../src/model-keys.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { busBusiness } from '../src/business.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { businessDoc, rolesCopy, systemFor, systemConfig, writeJson } from '../../business/tests/helpers.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { Registry, SESSIONS_VERSION, sessionsFile } from '../../seat/src/session.mjs';
import { createLauncher } from '../../cli/src/launcher.mjs';
const id = value => createHash('sha256').update(value).digest('hex').slice(0, 12);
export async function reviewFixture(t, { overdue = false, exited = false } = {}) {
const root = fs.mkdtempSync(join(tmpdir(), 'b1-'));
let server, credentials, child, childClosed, runner, runnerClosed, bus, launcher;
let completed = 0, begun = 0, sweptManager = null;
const sweepObservations = [];
const originalSweep = ModelKeys.prototype.sweep;
const exitOne = async (process, closed) => {
if (process?.pid && process.exitCode === null && process.signalCode === null) process.kill('SIGTERM');
if (closed) await closed;
};
const exitOuter = () => exitOne(child, childClosed);
const exitRunner = () => exitOne(runner, runnerClosed);
const exit = async () => { await exitOuter(); await exitRunner(); };
t.after(async () => {
const errors = [];
for (const close of [() => launcher?.close(), () => bus?.close(), () => credentials?.close(), exit,
async () => { server?.closeAllConnections(); if (server?.listening) await new Promise(resolve => server.close(resolve)); }]) {
try { await close(); } catch (error) { errors.push(error); }
}
ModelKeys.prototype.sweep = originalSweep;
fs.rmSync(root, { recursive: true, force: true });
if (errors.length) throw new AggregateError(errors, 'scratch fixture cleanup failed');
});
// Await the ORIGINAL sweep, including its real HTTP/filesystem work. Count both
// fulfilled and rejected completions; the runtime still handles its own refusal.
ModelKeys.prototype.sweep = async function (...args) {
const epoch = ++begun; sweptManager = this;
let outcome = 'fulfilled';
try { return await originalSweep.apply(this, args); }
catch (error) { outcome = error.code === 'proxy-stop-failed' ? 'proxy-stop-failed' : 'other-refusal'; throw error; }
finally { completed++; sweepObservations.push({ epoch, outcome }); }
};
const observeSweep = async () => {
const before = completed, deadline = Date.now() + 5000;
// Same five-second bounded wait budget as bus/tests/host-run.test.mjs:47.
// Production interval remains 1000ms; no clock or scheduler is replaced.
while (completed <= before && Date.now() < deadline) await wait(20);
assert.ok(completed > before, 'original runtime sweep completed within five seconds');
};
// #652 waits for a sweep STARTED after its setup phase, then fully completed.
// It observes the unchanged original method, never calls/replaces its work.
const observeNextSweep = async () => {
const after = begun, deadline = Date.now() + 5000;
while (!sweepObservations.some(s => s.epoch > after) && Date.now() < deadline) await wait(20);
const observation = sweepObservations.find(s => s.epoch > after);
assert.ok(observation, 'phase-specific original runtime sweep completed within five seconds');
return observation;
};
const configDir = join(root, 'config'), dataRoot = join(root, 'data');
fs.mkdirSync(dataRoot, { mode: 0o700 });
const config = systemConfig(root), system = systemFor(root), rolesDir = rolesCopy(root);
const doc = businessDoc(root, 'demo'); doc.plugins = { ...doc.plugins, harness: ['pi'] };
writeJson(join(configDir, 'businesses', 'demo.json'), doc);
const business = loadBusiness('demo', { dir: configDir, rolesDir }), resolved = {};
for (const instance of Object.keys(business.roles)) resolved[instance] = resolveInstance({ system, business, instance });
const storeRoot = join(configDir, 'secrets', 'agents'); fs.mkdirSync(storeRoot, { recursive: true, mode: 0o700 });
fs.writeFileSync(join(configDir, 'secrets', 'proxy-management.key'), 'fx647-manage', { mode: 0o600 });
const prefix = `msk-${id(configDir)}-`;
const state = { failGet: false, failPut: false, hideSentinel: false, keys: [prefix + 'sentinel-' + randomBytes(32).toString('base64url')], put: 0, removed: 0 };
server = http.createServer(async (req, res) => {
if (req.url === '/v8/management/config/access/api-keys') {
if (req.headers.authorization !== 'Bearer fx647-manage') { res.writeHead(401); res.end('{}'); return; }
if ((req.method === 'GET' && state.failGet) || (req.method === 'PUT' && state.failPut)) { res.writeHead(503); res.end('{}'); return; }
if (req.method === 'GET') { res.end(JSON.stringify(state.hideSentinel ? state.keys.filter(k => !k.includes('-sentinel-')) : state.keys)); return; }
const chunks = []; for await (const chunk of req) chunks.push(chunk);
const next = JSON.parse(Buffer.concat(chunks));
state.removed += state.keys.filter(key => !next.includes(key)).length;
state.keys = next; state.put++; res.end(JSON.stringify({ status: 'ok' })); return;
}
res.writeHead(state.keys.includes(req.headers.authorization?.slice(7)) ? 200 : 401); res.end('{}');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
credentials = new Credentials({ storeRoot, dataRoot, modelAgents: ['demo/chief'] });
const manager = new ModelKeys({ credentials, configDir, baseUrl, client: new ManagementClient({ baseUrl, key: 'fx647-manage' }) });
await manager.reconcile(); const first = await manager.ensure('demo/chief'); await manager.rotate('demo/chief');
const record = credentials.modelRecord('demo/chief');
credentials.writeModel('demo/chief', { ...record, retiring: record.retiring.map(r => ({ ...r, retireAt: new Date(Date.now() + (overdue ? -60_000 : 3_600_000)).toISOString() })) });
credentials.close(); credentials = null; state.put = 0; state.removed = 0;
// A harmless scratch-owned process supplies real /proc identity. It is deliberately
// not a session runner, so stopSession must refuse rather than signal this stand-in.
child = spawn('/usr/bin/sleep', ['120'], { stdio: 'ignore' });
childClosed = new Promise(resolve => child.once('close', resolve));
await once(child, 'spawn');
const ticks = startTimeOf(child.pid); assert.equal(typeof ticks, 'string');
runner = spawn('/usr/bin/sleep', ['120'], { stdio: 'ignore' });
runnerClosed = new Promise(resolve => runner.once('close', resolve));
await once(runner, 'spawn');
const runnerTicks = startTimeOf(runner.pid); assert.equal(typeof runnerTicks, 'string');
const entry = { business: 'demo', instance: 'chief', run: 'r647-old', harness: 'pi', provider: 'fake', model: 'fake',
pid: child.pid, startTime: ticks, runnerPid: runner.pid, runnerStartTime: runnerTicks, modelKeyId: id(first.key), launchedBy: 'chief' };
const writeSessions = entries => writeJson(sessionsFile(dataRoot), { sessionsVersion: SESSIONS_VERSION, sessions: entries });
writeSessions([entry]);
if (exited) await exit();
const boot = async ({ proxyMode = true } = {}) => {
bus = await startBroker({ dataRoot, businesses: { demo: busBusiness(business, resolved) }, organization: { dir: configDir, rolesDir, system },
proxy: proxyMode ? { port: server.address().port, binary: '/fixture-unused' } : null });
return bus;
};
const noLaunch = async () => {
const ops = [];
const host = { business: 'demo', beforeClose() {}, bindLaunch: async r => { ops.push('bindLaunch'); return bus.bindLaunch(r); },
op: async r => { ops.push(r.op); throw Error('fixture-unrequested-op'); } };
launcher = createLauncher({ host, system: JSON.parse(fs.readFileSync(config, 'utf8')), launches: false, env: { MOSAIC_CONFIG: config, MOSAIC_ROLES_DIR: rolesDir, PATH: '/usr/bin' }, log: () => {} });
await launcher.listen(); return ops;
};
const retained = () => {
assert.equal(state.keys.includes(first.key), true, 'old key stays remotely installed');
assert.equal(bus.credentials.modelRecord('demo/chief').retiring.some(r => r.keyId === entry.modelKeyId), true, 'retiring id stays locally recorded');
assert.equal(state.removed, 0, 'no remote key removal');
};
return { root, configDir, config, rolesDir, system, dataRoot, state, entry, writeSessions, boot, noLaunch, exit, exitOuter, exitRunner, retained, observeSweep, observeNextSweep,
get sweptManager() { return sweptManager; },
get bus() { return bus; },
attachLauncher: value => { launcher = value; },
closeBus: async () => { await bus?.close(); bus = null; },
setModel: patch => {
const own = new Credentials({ storeRoot, dataRoot, modelAgents: ['demo/chief'] });
try { own.writeModel('demo/chief', { ...own.modelRecord('demo/chief'), ...patch }); } finally { own.close(); }
},
nextGeneration: () => {
const previous = bus.credentials.modelRecord('demo/chief'), key = prefix + randomBytes(32).toString('base64url');
state.keys.push(key);
bus.credentials.writeModel('demo/chief', { ...previous, key, keyId: id(key), issuedAt: new Date().toISOString(),
retiring: [{ key: previous.key, keyId: previous.keyId, issuedAt: previous.issuedAt, retireAt: new Date(Date.now() - 60000).toISOString() }] });
return { removed: () => assert.equal(state.keys.includes(previous.key), false, 'later generation retiring key removed after confirmed terminal pair'),
retained: () => assert.equal(state.keys.includes(previous.key), true, 'later generation retiring key retained on disagreement') };
},
alive: () => startTimeOf(child.pid) === ticks,
absent: () => { assert.equal(state.keys.includes(first.key), false); assert.equal(bus.credentials.modelRecord('demo/chief').retiring.length, 0); },
bind: () => bus.bindLaunch({ business: entry.business, role: entry.instance, run: entry.run, harness: entry.harness, pid: entry.pid, startTime: entry.startTime, modelKeyId: entry.modelKeyId }) };
}
@@ -0,0 +1,18 @@
// Owned bounded child ONLY for #656 FIFO nonblocking control, not a production entry.
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import { getPriority, setPriority } from 'node:os';
import { revocationFixture, outcome } from './model-revocation-fixture.mjs';
setPriority(0,19);
const cleanup=[];let answer;
try {
const f=await revocationFixture({after:close=>cleanup.push(close)},{onRoot:root=>process.send?.({kind:'owned-root',root})});
f.seed();fs.unlinkSync(f.temporary);const made=spawnSync('/usr/bin/mkfifo',['-m','600',f.temporary],{stdio:'ignore',timeout:5000,killSignal:'SIGTERM'});
if(made.status!==0)throw Error('owned fifo setup failed');
await new Promise((resolve,reject)=>process.send({kind:'ready',nice:getPriority(0)},error=>error?reject(error):resolve()));
const began=Date.now(),result=await outcome(async()=>{await f.prepare();f.complete();});
answer={kind:'result',ok:result.ok,code:result.code??null,cachedRevoking:f.record()?.state==='revoking',finalPresent:f.exists(f.file),tmpPresent:f.exists(f.temporary),openModelFds:f.openModelFds(),elapsedMs:Date.now()-began,nice:getPriority(0)};f.valueFree();
}catch{answer={kind:'error',code:'owned-fifo-fixture-failed'};process.exitCode=1;}
let clean=true;for(const close of [...cleanup].reverse()){try{await close();}catch{clean=false;}}
if(!clean){answer={kind:'error',code:'owned-fifo-cleanup-failed'};process.exitCode=1;}
if(process.connected){await new Promise(resolve=>process.send({...answer,ownedCleanup:clean},()=>resolve()));process.disconnect();}
@@ -0,0 +1,86 @@
// #656 STATIC isolated fixtures. No real config/key/proxy; no production seam.
import fs from 'node:fs';
import http from 'node:http';
import assert from 'node:assert/strict';
import { syncBuiltinESMExports } from 'node:module';
import { randomBytes, createHash } from 'node:crypto';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { Credentials } from '../src/credentials.mjs';
import { ModelKeys } from '../src/model-keys.mjs';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { startBroker } from '../src/runtime.mjs';
import { busBusiness } from '../src/business.mjs';
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { organizationWriter } from '../../business/src/organization.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { sessionsFile, SESSIONS_VERSION, Registry } from '../../seat/src/session.mjs';
export const id = value => createHash('sha256').update(value).digest('hex').slice(0,12);
export async function bounded(promise,message,ms=5000){let timer;try{return await Promise.race([promise,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error(message)),ms);})]);}finally{clearTimeout(timer);}}
export async function outcome(operation){try{return {ok:true,value:await operation()};}catch(error){return {ok:false,code:error.code??'unexpected-error'};}}
export async function revocationFixture(t,{runtime=false,onRoot=()=>{},missingBoot=false}={}){
const root=fs.mkdtempSync(join(tmpdir(),'r656-')),configDir=join(root,'config'),dataRoot=join(root,'data'),storeRoot=join(configDir,'secrets','agents'),instance='demo/aide';
const file=join(storeRoot,'demo','aide','model'),temporary=file+'.tmp';
const history=new Set(['fx656-manage']),receipts=[],proofs=[],active=new Set(),closers=[],children=[],fdPaths=new Map();
let server,c,manager,bus,stderr,currentBusClose,restoreFs;
const state={keys:[],calls:[],probes:[],forceProbe:new Map(),failPut:false,failGet:false,onProbe:null,callbackFailed:false,failView:false,liveView:[]};
const faults={unlinkService:false,unlinkTmp:false,unlinkFinal:false,readFinal:false,absenceTmp:false,procRead:null};const counts={procRead:0,readFinal:0,absenceTmp:0,unlinkService:0,unlinkTmp:0,unlinkFinal:0};let removedTmp=false;
const valueFree=()=>{const text=JSON.stringify(receipts);for(const value of history)if(text.includes(value))throw Error('fixture credential leaked');};
t.after(async()=>{const errors=[];try{await bounded(Promise.allSettled([...active]),'owned operation cleanup timeout');}catch{errors.push('owned-operations');}try{restoreFs?.();}catch{errors.push('owned-fs-restore');}for(const child of children){try{if(child.handle.exitCode===null&&child.handle.signalCode===null)child.handle.kill('SIGTERM');await bounded(child.closed,'owned child cleanup timeout');}catch{errors.push('owned-child');}}for(const close of [...closers].reverse()){try{await close();}catch{errors.push('owned-resource');}}try{server?.closeAllConnections();if(server?.listening)await new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));}catch{errors.push('owned-server');}try{valueFree();}catch{errors.push('value-free-receipts');}if(stderr)process.stderr.write=stderr;try{fs.rmSync(root,{recursive:true,force:true});}catch{errors.push('owned-root');}if(errors.length)throw new AggregateError(errors.map(code=>Error(code)),'owned revocation fixture cleanup failed');});
onRoot(root);
fs.mkdirSync(dataRoot,{mode:0o700});fs.mkdirSync(storeRoot,{recursive:true,mode:0o700});fs.mkdirSync(join(dataRoot,'bus-host'),{mode:0o700});assert.equal(fs.existsSync(sessionsFile(dataRoot)),false,'fresh owned data root has no prior sessions');if(!missingBoot){const registry=new Registry(dataRoot);assert.deepEqual(registry.sessions,[]);assert.equal(fs.statSync(sessionsFile(dataRoot)).mode&0o777,0o600);}
const prefix=`msk-${id(configDir)}-`,key=()=>{const value=prefix+randomBytes(32).toString('base64url');history.add(value);return value;};const sentinel=prefix+'sentinel-'+randomBytes(32).toString('base64url');history.add(sentinel);state.keys.push(sentinel);
server=http.createServer(async(req,res)=>{try{
const supplied=req.headers.authorization?.slice(7);if(supplied)history.add(supplied);for(const value of state.keys)history.add(value);
if(req.url==='/v8/management/config/access/api-keys'){
state.calls.push(req.method);if(supplied!=='fx656-manage'){res.writeHead(401);res.end('{}');return;}
let next;if(req.method==='PUT'){const chunks=[];for await(const chunk of req)chunks.push(chunk);next=JSON.parse(Buffer.concat(chunks));for(const value of next)history.add(value);}
if((req.method==='PUT'&&state.failPut)||(req.method==='GET'&&state.failGet)){res.writeHead(503);res.end('{}');return;}
if(req.method==='GET'){res.end(JSON.stringify(state.keys));return;}state.keys=next;res.end('{"status":"ok"}');return;
}
const status=state.forceProbe.get(supplied)??(state.keys.includes(supplied)?200:401);state.calls.push('PROBE');state.probes.push({keyId:id(supplied??''),status});
if(status===401&&state.onProbe){const callback=state.onProbe;state.onProbe=null;try{callback();}catch{state.callbackFailed=true;res.writeHead(500);res.end('{}');return;}}
res.writeHead(status);res.end('{}');
}catch{if(!res.destroyed){res.writeHead(500);res.end('{}');}}});await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});const port=server.address().port;assert.notEqual(port,8317);const baseUrl='http://127.0.0.1:'+port;
fs.writeFileSync(join(configDir,'secrets','proxy-management.key'),'fx656-manage',{mode:0o600});
const track=promise=>{active.add(promise);promise.then(()=>active.delete(promise),()=>active.delete(promise));return promise;};
let boot,writer,chief,human,busBoots=0;
if(runtime){
const rolesDir=rolesCopy(root),system=systemFor(root),doc=businessDoc(root,'demo');doc.plugins={...doc.plugins,harness:['pi']};doc.roles.aide={definition:'aide',vars:{harness:'pi',model:'claude-opus-5-5',thinking:'high'}};doc.launch.instances.push('aide');
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});writeJson(join(configDir,'businesses','demo.json'),doc);
const business=loadBusiness('demo',{dir:configDir,rolesDir}),resolved={};for(const name of Object.keys(business.roles))resolved[name]=resolveInstance({system,business,instance:name});
boot={dataRoot,businesses:{demo:busBusiness(business,resolved)},organization:{dir:configDir,rolesDir,system},proxy:{port,binary:'/fixture-unused'}};writer=organizationWriter({dir:configDir,rolesDir});
stderr=process.stderr.write;process.stderr.write=function(chunk,...args){if(String(chunk).startsWith('model-proxy: ')){receipts.push(String(chunk));args.find(a=>typeof a==='function')?.();return true;}return stderr.call(this,chunk,...args);};
const openBus=async()=>{bus=await startBroker(boot);const ownedBus=bus,run='tmp-chief-'+(++busBoots);let closed=false,bound=false;currentBusClose=async()=>{if(closed)return;closed=true;const errors=[];if(bound){try{ownedBus.endLaunch({business:'demo',run,reason:'host-lost',exitCode:null});}catch{errors.push('owned-chief-end');}}try{await ownedBus.close();}catch{errors.push('owned-bus-close');}if(errors.length)throw new AggregateError(errors.map(code=>Error(code)),'owned bus cleanup failed');};closers.push(currentBusClose);c=bus.credentials;chief=bus.broker.bindLaunch({business:'demo',role:'chief',run,harness:'pi'});bound=true;bus.broker.request(chief,{verb:'role.claim'});human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});};
await openBus();boot.openBus=openBus;
}else{
// Unit proof/bookkeeping tests use the existing constructor callback with no
// processes. Actual identity/live/unbound cases below use REAL runtime inventory.
class ObservedCredentials extends Credentials {captureModelRevocation(...args){const result=super.captureModelRevocation(...args);proofs.push(result);return result;}}
c=new ObservedCredentials({storeRoot,dataRoot,modelAgents:[instance,'demo/other']});closers.push(()=>c.close());
manager=new ModelKeys({credentials:c,configDir,baseUrl,client:new ManagementClient({baseUrl,key:'fx656-manage',receipt:r=>receipts.push(r)}),receipt:r=>receipts.push(r),runs:()=>{if(state.failView)throw Error('owned uncertain view');return state.liveView;}});await manager.reconcile();
}
const original={openSync:fs.openSync,closeSync:fs.closeSync,readSync:fs.readSync,readFileSync:fs.readFileSync,unlinkSync:fs.unlinkSync,lstatSync:fs.lstatSync};
fs.openSync=function(path,...args){const fd=original.openSync(path,...args);if(path===file||path===temporary)fdPaths.set(fd,path);return fd;};
fs.closeSync=function(fd,...args){fdPaths.delete(fd);return original.closeSync(fd,...args);};
fs.readSync=function(fd,...args){if(faults.readFinal&&fdPaths.get(fd)===file){counts.readFinal++;throw Object.assign(Error('owned read refused'),{code:'EACCES'});}return original.readSync(fd,...args);};
fs.readFileSync=function(path,...args){if(faults.procRead&&path==='/proc/'+faults.procRead+'/stat'){counts.procRead++;throw Object.assign(Error('owned proc read refused'),{code:'EACCES'});}return original.readFileSync(path,...args);};
fs.unlinkSync=function(path,...args){if(path===join(storeRoot,'demo','aide','gitea')){counts.unlinkService++;if(faults.unlinkService)throw Object.assign(Error('owned service unlink refused'),{code:'EACCES'});}if(path===file){counts.unlinkFinal++;if(faults.unlinkFinal)throw Object.assign(Error('owned unlink refused'),{code:'EACCES'});}if(path===temporary){counts.unlinkTmp++;if(faults.unlinkTmp)throw Object.assign(Error('owned unlink refused'),{code:'EACCES'});}const result=original.unlinkSync(path,...args);if(path===temporary)removedTmp=true;return result;};
fs.lstatSync=function(path,...args){if(path===temporary&&faults.absenceTmp&&removedTmp){counts.absenceTmp++;throw Object.assign(Error('owned absence read refused'),{code:'EIO'});}return original.lstatSync(path,...args);};
syncBuiltinESMExports();restoreFs=()=>{Object.assign(fs,original);syncBuiltinESMExports();};
const record=()=>c.modelRecord(instance),exists=path=>{try{original.lstatSync(path);return true;}catch(error){if(error.code==='ENOENT')return false;throw error;}};
const seed=({matchingTmp=true,retiring=0,valid=false}={})=>{const current=key(),old=Array.from({length:retiring},()=>({key:key(),issuedAt:'2026-10-23T00:00:00.000Z',retireAt:'2099-01-01T00:00:00.000Z'})).map(r=>({...r,keyId:id(r.key)}));const value={key:current,keyId:id(current),issuedAt:'2026-10-24T00:00:00.000Z',rotateBy:'2099-01-01',state:valid?'valid':'revoking',...(valid?{}:{revokingAt:'2026-10-25T00:00:00.000Z'}),retiring:old};state.keys.push(current,...old.map(r=>r.key));c.writeModel(instance,value);if(matchingTmp){fs.copyFileSync(file,temporary);fs.chmodSync(temporary,0o600);}removedTmp=false;return value;};
const lifecycle=how=>track((async()=>{if(how==='fire')return await bus.broker.request(chief,{verb:'instance.fire',args:{name:'aide',reason:'fixture recognized tmp'}});const revision=writer.read('demo').revision;const request=bus.broker.request(human,{verb:'instances.set',args:{revision,instance:'aide',op:'delete'}});const route={business:'demo',request:request.request},take=bus.lifecycleTake(route);assert.equal(take.go,true,'real trusted delete request is taken');try{const result=await bus.lifecycleDelete(route);bus.lifecycleDone({...route,transaction:result.transaction});return result;}catch(error){bus.lifecycleDone({...route,refusal:error.code??'outcome-unknown'});throw error;}})());
const writeSessions=entries=>writeJson(sessionsFile(dataRoot),{sessionsVersion:SESSIONS_VERSION,sessions:entries});
const ownRun=async({wrongTicks=false,wrongKey=false,publish=true}={})=>{
const spawnOwn=async()=>{const handle=spawn('/usr/bin/sleep',['60'],{stdio:'ignore'}),closed=new Promise(resolve=>handle.once('close',resolve));const owned={handle,closed};children.push(owned);await once(handle,'spawn');const ticks=startTimeOf(handle.pid);assert.equal(typeof ticks,'string');return {...owned,ticks,exit:async()=>{if(handle.exitCode===null&&handle.signalCode===null)handle.kill('SIGTERM');await bounded(closed,'owned stand-in exit timeout');}};};
const outer=await spawnOwn(),runner=await spawnOwn();
const entry={business:'demo',instance:'aide',run:'tmp-unbound',harness:'pi',provider:'fake',model:'fake',pid:outer.handle.pid,startTime:wrongTicks?'1':outer.ticks,runnerPid:runner.handle.pid,runnerStartTime:runner.ticks,modelKeyId:wrongKey?(record().keyId==='ffffffffffff'?'000000000000':'ffffffffffff'):record().keyId,launchedBy:'chief'};if(publish)writeSessions([entry]);
return {entry,alive:()=>startTimeOf(outer.handle.pid)===outer.ticks&&startTimeOf(runner.handle.pid)===runner.ticks,exitOuter:outer.exit,exitRunner:runner.exit,exit:async()=>{await outer.exit();await runner.exit();}};
};
return {root,configDir,dataRoot,storeRoot,file,temporary,instance,remember:value=>history.add(value),state,faults,counts,proofs,valueFree,seed,record,exists,manager,makeKey:key,prepare:()=>manager.prepareRevoke(instance),complete:()=>manager.completeRevoke(instance),get credentials(){return c;},get bus(){return bus;},lifecycle,ownRun,writeSessions,openModelFds:()=>fdPaths.size,reboot:async()=>{await currentBusClose();await boot.openBus();},membershipHash:()=>id(fs.readFileSync(join(configDir,'businesses','demo.json'))),listed:()=>JSON.parse(fs.readFileSync(join(configDir,'businesses','demo.json'),'utf8')).launch.instances.includes('aide'),remoteHas:key=>state.keys.includes(key),assertHistoryGuard:()=>{try{const text=JSON.stringify({canary:sentinel});for(const value of history)if(text.includes(value))throw Error('fixture credential leaked');}catch(error){if(error.message==='fixture credential leaked')return;}throw Error('fixture history guard failed');}};
}
+22
View File
@@ -0,0 +1,22 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { stopModelRuns, MODEL_STOP_TIMEOUT_MS } from '../src/model-keys.mjs';
const run = { business: 'demo', role: 'coder', run: 'r-test', keyId: '0123456789ab', pid: 123, startTime: '456' };
const entry = { business: 'demo', instance: 'coder', run: 'r-test', modelKeyId: '0123456789ab', pid: 123, startTime: '456', live: true };
test('bounded proxy stop is five seconds by default', () => assert.equal(MODEL_STOP_TIMEOUT_MS, 5000));
for (const patch of [{ instance: 'other' }, { modelKeyId: 'fedcba987654' }, { pid: 999 }, { startTime: '999' }]) test(`proxy-stop-failed: mismatched ${Object.keys(patch)[0]} is never signalled`, async () => {
let signals = 0; const receipts = [];
await assert.rejects(stopModelRuns({ runs: [run], sessions: () => [{ ...entry, ...patch }], stop: async () => { signals++; }, receipt: r => receipts.push(r) }), e => e.code === 'proxy-stop-failed');
assert.equal(signals, 0); assert.equal(receipts[0].code, 'proxy-stop-failed');
});
test('proxy-stop-failed: hung SIGTERM path is bounded and error text is not logged', async () => {
const receipts = [], start = performance.now();
await assert.rejects(stopModelRuns({ runs: [run], sessions: () => [entry], stop: () => new Promise(() => {}), receipt: r => receipts.push(r), timeoutMs: 20 }), e => e.code === 'proxy-stop-failed');
assert.ok(performance.now() - start < 1000);
assert.deepEqual(receipts, [{ action: 'proxy-rotation', code: 'proxy-stop-failed', keyIds: ['0123456789ab'], run: 'r-test' }]);
});
test('stop verifies the run exited and logs the proxy reason with ids only', async () => {
let live = true; const receipts = [];
await stopModelRuns({ runs: [run], sessions: () => [{ ...entry, live }], stop: async () => { live = false; return { stopped: true }; }, receipt: r => receipts.push(r), reason: 'proxy-stall' });
assert.deepEqual(receipts, [{ action: 'proxy-stall', keyIds: ['0123456789ab'], business: 'demo', agent: 'coder', run: 'r-test', outcome: 'stopped' }]);
});
@@ -0,0 +1,33 @@
// L1/#651: real independent agent retirements, first failure still reported.
import test from 'node:test';
import assert from 'node:assert/strict';
import { BusError } from '../src/broker.mjs';
import { lifecycleFixture, id } from './model-lifecycle-fixtures.mjs';
async function setup(t){const f=await lifecycleFixture(t),c=f.create();await c.manager.reconcile();const a=await c.manager.ensure('demo/coder'),b=await c.manager.ensure('demo/other');await c.manager.rotate('demo/coder');await c.manager.rotate('demo/other');f.state.runs=[{instance:'demo/coder',run:'blocked-own-run',keyId:id(a.key)}];f.advance(121*60_000);return {...f,...c,a,b};}
test('L1 blocked first agent does not starve independent later retirement',async t=>{
const f=await setup(t);await assert.rejects(f.manager.sweep(),e=>e.code==='proxy-stop-failed');
assert.equal(f.state.keys.includes(f.b.key),false,'later independent key retired despite earlier failure');
assert.equal(f.credentials.modelRecord('demo/other').retiring.length,0);assert.equal(f.state.keys.includes(f.a.key),true);assert.equal(f.credentials.modelRecord('demo/coder').retiring.length,1);
assert.deepEqual(f.state.stopAttempts,['blocked-own-run']);f.valueFree();
});
test('L1 successful retry retains first refusal semantics without deleting blocked metadata',async t=>{
const f=await setup(t);await assert.rejects(f.manager.sweep(),e=>e.code==='proxy-stop-failed');
assert.ok(f.receipts.some(r=>r.action==='proxy.sweep'&&r.instance==='demo/coder'&&r.code==='proxy-stop-failed'),'failed instance has value-free sweep outcome');
f.state.stopFailure=false;await f.manager.sweep();assert.equal(f.state.keys.includes(f.a.key),false);assert.equal(f.state.keys.includes(f.b.key),false);f.valueFree();
});
test('L1 no-error sweep still completes both independent retirements',async t=>{
const f=await setup(t);f.assertHistoryGuard();f.state.runs=[];await f.manager.sweep();assert.equal(f.state.keys.includes(f.a.key),false);assert.equal(f.state.keys.includes(f.b.key),false);f.valueFree();
});
test('L1 preserves exact original first error after visiting later independently failing agent',async t=>{
const f=await setup(t),first=new BusError('proxy-stop-failed'),second=new BusError('proxy-down');
f.state.discoveryFaults.set('demo/coder',first);f.state.discoveryFaults.set('demo/other',second);
await assert.rejects(f.manager.sweep(),error=>error===first,'original first error identity retained');
assert.deepEqual(f.state.discoveries,['demo/coder','demo/other'],'later independent failure also visited');f.valueFree();
});
test('L1 repeated no-op sweeps produce no per-instance complete receipts',async t=>{
const f=await lifecycleFixture(t),c=f.create();await c.manager.reconcile();await c.manager.ensure('demo/coder');f.receipts.length=0;
await c.manager.sweep();await c.manager.sweep();await c.manager.sweep();
assert.equal(f.receipts.some(r=>r.action==='proxy.sweep'&&r.outcome==='complete'),false,'idle sweeps do not emit per-second success churn');assert.deepEqual(f.receipts,[]);f.valueFree();
});
@@ -0,0 +1,62 @@
// M1/#648: deterministic synthetic reuse ONLY after actual owned-process exit.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { syncBuiltinESMExports } from 'node:module';
import { reviewFixture } from './model-review-fixtures.mjs';
function syntheticReuse(t, entries) {
const originalRead = fs.readFileSync, originalKill = process.kill;
const stats = new Map(entries.map(({ pid, startTime }) => {
const fields = ['S', ...Array(18).fill('0'), String(BigInt(startTime) + 1n)];
return [`/proc/${pid}/stat`, `${pid} (synthetic-scratch-reuse) ${fields.join(' ')}\n`];
}));
const protectedPids = new Set(entries.map(e => e.pid));
let signals = 0;
t.after(() => { fs.readFileSync = originalRead; process.kill = originalKill; syncBuiltinESMExports(); });
fs.readFileSync = function (file, ...args) {
if (typeof file === 'string' && stats.has(file)) return args[0] === 'utf8' ? stats.get(file) : Buffer.from(stats.get(file));
return originalRead.call(this, file, ...args);
};
process.kill = function (pid, ...args) {
if (protectedPids.has(pid)) { signals++; throw Error('synthetic identity must never be signalled'); }
return originalKill.call(this, pid, ...args);
};
syncBuiltinESMExports();
return () => assert.equal(signals, 0, 'no signal to synthetic reused identity');
}
test('M1 confirmed outer and runner exits stay terminal through stale registry and synthetic reuse for a later generation', async t => {
const f = await reviewFixture(t, { overdue: true }); await f.boot(); await f.observeSweep(); f.retained();
await f.exitOuter(); await f.observeSweep(); f.retained();
await f.exitRunner(); await f.observeSweep(); f.absent();
// Both actual owned children have exited/reaped and distinct completions observed.
// This synthetic proc view is NOT a claim of actual OS PID wraparound/reuse.
const noSignals = syntheticReuse(t, [{ pid: f.entry.pid, startTime: f.entry.startTime }, { pid: f.entry.runnerPid, startTime: f.entry.runnerStartTime }]);
f.writeSessions([f.entry]); // same stale immutable identity must not erase terminal proof
const removed = f.nextGeneration(); await f.observeSweep(); removed.removed(); noSignals();
});
test('M1 separately confirmed outer exit does not mark a live runner terminal', async t => {
const f = await reviewFixture(t, { overdue: true }); await f.boot(); await f.observeSweep();
await f.exitOuter(); await f.observeSweep(); f.retained();
});
for (const field of ['startTime', 'runnerStartTime', 'modelKeyId']) test(`M1 initial corrupt ${field} does not acquire terminal proof`, async t => {
const f = await reviewFixture(t, { overdue: true });
f.writeSessions([{ ...f.entry, [field]: field === 'modelKeyId' ? 'fedcba987654' : '0' }]);
await f.boot(); await f.observeSweep(); f.retained();
});
test('M1 terminal proof never authorizes a changed persisted key identity', async t => {
const f = await reviewFixture(t, { overdue: true }); await f.boot(); await f.observeSweep();
await f.exitOuter(); await f.observeSweep(); await f.exitRunner(); await f.observeSweep(); f.absent();
const removed = f.nextGeneration();
f.writeSessions([{ ...f.entry, modelKeyId: 'fedcba987654' }]);
await f.observeSweep();
removed.retained();
});
test('M1 an initially corrupt key on an exited pair cannot acquire terminal permission', async t => {
const f = await reviewFixture(t, { overdue: true, exited: true });
f.writeSessions([{ ...f.entry, modelKeyId: 'fedcba987654' }]);
await f.boot(); await f.observeSweep(); f.retained();
});
@@ -0,0 +1,100 @@
// #656 STATIC preparation: real runtime inventory + Broker/writer/controller/HTTP.
// No code/test has run for this packet. Controlled faults are BEFORE verification.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { revocationFixture, outcome, id } from './model-revocation-fixture.mjs';
const timeout=30000;
async function refused(f,how='fire'){const result=await outcome(()=>f.lifecycle(how));assert.equal(result.ok,false,'controlled lifecycle refuses');assert.equal(result.code,'credential-revoke-failed','existing lifecycle refusal code');assert.equal(f.state.callbackFailed,false,'owned transport mutation completed');return result;}
function retained(f){assert.equal(f.record()?.state==='revoking',true,'uncertain matched recovery retains cached revoking intent');assert.equal(f.exists(f.file),true,'existing final path retained');assert.equal(f.exists(f.temporary),true,'uncertain tmp path retained');}
for(const how of ['fire','delete'])test('656 matched durable revoking final and tmp '+how+' complete through real lifecycle',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true}),record=f.seed({retiring:2}),ids=[record.keyId,...record.retiring.map(r=>r.keyId)];
await assert.doesNotReject(()=>f.lifecycle(how),'matched intent '+how+' completes before membership publication');
assert.equal(f.counts.unlinkFinal,1,'composite '+how+' performs ONE final pathname unlink');assert.equal(f.record()===null,true);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);assert.equal(f.listed(),false);assert.equal(ids.every(keyId=>f.state.probes.some(p=>p.keyId===keyId&&p.status===401)),true,'EVERY current and retiring key has confirmed401');assert.equal(f.openModelFds(),0);f.assertHistoryGuard();f.valueFree();
});
for(const how of ['fire','delete'])test('656 matched tmp unlink failure keeps durable cache and '+how+' retry completes',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();const before=f.membershipHash();f.faults.unlinkTmp=true;await refused(f,how);retained(f);assert.equal(f.membershipHash(),before);await assert.rejects(f.bus.modelPreflight({business:'demo',role:'aide'}),e=>e.code==='proxy-key-revoking');
f.faults.unlinkTmp=false;await assert.doesNotReject(()=>f.lifecycle(how),'recognized tmp unlink retry completes');assert.equal(f.record()===null,true);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);assert.equal(f.listed(),false);f.valueFree();
});
test('656 failed remote confirmation preserves matching intent and membership without rollback claim',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true}),record=f.seed({retiring:2}),before=f.membershipHash();f.state.failPut=true;await refused(f);retained(f);assert.equal(f.membershipHash(),before);assert.equal([record.key,...record.retiring.map(r=>r.key)].every(key=>f.remoteHas(key)),true);f.valueFree();
});
test('656 non401 last retiring key never authorizes local deletion',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true}),record=f.seed({retiring:2}),old=record.retiring.at(-1),before=f.membershipHash();f.state.forceProbe.set(old.key,200);await refused(f);retained(f);assert.equal(f.membershipHash(),before);assert.equal(f.state.probes.some(p=>p.keyId===old.keyId&&p.status===401),false);assert.equal([record.keyId,...record.retiring.map(r=>r.keyId)].every(keyId=>f.state.probes.some(p=>p.keyId===keyId)),true);f.valueFree();
});
for(const mode of ['live-unbound','partial-exit','wrong-ticks','wrong-key','proc-read-error'])test('656 '+mode+' real inventory refuses matching recovery without signalling',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();const run=await f.ownRun({wrongTicks:mode==='wrong-ticks',wrongKey:mode==='wrong-key'});if(mode==='partial-exit')await run.exitOuter();if(mode==='proc-read-error')f.faults.procRead=run.entry.runnerPid;
const before=f.membershipHash(),puts=f.state.calls.filter(m=>m==='PUT').length;await refused(f);f.faults.procRead=null;retained(f);if(mode==='proc-read-error')assert.equal(f.counts.procRead>0,true,'owned proc read fault was reached');assert.equal(f.membershipHash(),before);assert.equal(f.state.calls.filter(m=>m==='PUT').length,puts,'known live or uncertain view causes no removal PUT');
if(mode==='partial-exit')assert.equal(startTimeOf(run.entry.runnerPid),run.entry.runnerStartTime,'live counterpart is retained');else assert.equal(run.alive(),true,'only owned stand-ins remain alive; discovery grants no stop authority');f.valueFree();
});
test('656 confirmed outer and runner exits permit matching intent recovery without rebind',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();const run=await f.ownRun();await run.exit();await assert.doesNotReject(()=>f.lifecycle('fire'),'confirmed exited pair permits matched intent completion');assert.equal(f.record()===null,true);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);assert.equal(f.listed(),false);f.valueFree();
});
test('656 malformed actual persisted registry holds matching intent',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();f.writeSessions([]);fs.writeFileSync(join(f.dataRoot,'bus-host','sessions.json'),'not-json\n');const before=f.membershipHash();await refused(f);retained(f);assert.equal(f.membershipHash(),before);f.valueFree();
});
const negatives=[
['tmp symlink',f=>{fs.unlinkSync(f.temporary);const target=join(f.root,'unrelated');fs.writeFileSync(target,'owned unrelated\n',{mode:0o600});fs.symlinkSync(target,f.temporary);return {path:target,hash:id('owned unrelated\n')};}],
['tmp directory',f=>{fs.unlinkSync(f.temporary);fs.mkdirSync(f.temporary,{mode:0o700});}],
['tmp wrong mode',f=>fs.chmodSync(f.temporary,0o644)],
['tmp oversize',f=>fs.writeFileSync(f.temporary,'x'.repeat(16385),{mode:0o600})],
['tmp partial JSON',f=>fs.writeFileSync(f.temporary,'{',{mode:0o600})],
['tmp full metadata mismatch',f=>{const record=f.record();fs.writeFileSync(f.temporary,JSON.stringify({...record,rotateBy:'2098-01-01'})+'\n',{mode:0o600});}],
['tmp hardlink alias',f=>{fs.unlinkSync(f.temporary);fs.linkSync(f.file,f.temporary);}],
['final symlink',f=>{fs.unlinkSync(f.file);const target=join(f.root,'unrelated-final');fs.writeFileSync(target,'owned unrelated\n',{mode:0o600});fs.symlinkSync(target,f.file);return {path:target,hash:id('owned unrelated\n')};}],
['final wrong mode',f=>fs.chmodSync(f.file,0o644)],
['final malformed record',f=>fs.writeFileSync(f.file,'{',{mode:0o600})],
['unknown extra entry',f=>fs.writeFileSync(join(f.root,'config','secrets','agents','demo','aide','unknown'),'owned unrelated\n',{mode:0o600})],
];
for(const [name,mutate] of negatives)test('656 '+name+' is not recognized matching recovery authority',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();const unrelated=mutate(f);const before=f.membershipHash(),puts=f.state.calls.filter(m=>m==='PUT').length;await refused(f);retained(f);assert.equal(f.membershipHash(),before);assert.equal(f.state.calls.filter(m=>m==='PUT').length,puts);assert.equal(f.openModelFds(),0);if(unrelated)assert.equal(id(fs.readFileSync(unrelated.path)),unrelated.hash,'owned unrelated target is never followed or deleted');f.valueFree();
});
for(const target of ['tmp replacement','tmp content mutation','final replacement','final content mutation'])test('656 controlled '+target+' BEFORE final verification retains local intent',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed({retiring:1});let reached=false;
f.state.onProbe=()=>{reached=true;const path=target.startsWith('tmp')?f.temporary:f.file;if(target.includes('replacement')){const replacement=join(f.root,'owned-replacement');fs.writeFileSync(replacement,fs.readFileSync(path),{mode:0o600});fs.renameSync(replacement,path);}else{const value=f.record();fs.writeFileSync(path,JSON.stringify({...value,rotateBy:'2098-01-01'})+'\n',{mode:0o600});}};
const before=f.membershipHash();await refused(f);assert.equal(reached,true,'controlled fault actually ran before response401');retained(f);assert.equal(f.membershipHash(),before);assert.equal(f.openModelFds(),0);f.valueFree();
});
test('656 late actual registry publication after remote401 retains local intent without remote rollback',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true}),record=f.seed(),run=await f.ownRun({publish:false});let reached=false;f.state.onProbe=()=>{reached=true;f.writeSessions([run.entry]);};const before=f.membershipHash();await refused(f);assert.equal(reached,true);retained(f);assert.equal(f.membershipHash(),before);assert.equal(f.remoteHas(record.key),false,'late refusal does not pretend to roll back the remote operation');assert.equal(run.alive(),true);f.valueFree();
});
test('656 boot reconcile also removes strictly matched intent tmp only after every401',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true}),record=f.seed({retiring:2});await f.reboot();assert.equal(f.record()===null&&!f.exists(f.file)&&!f.exists(f.temporary),true,'boot reconcile completes recognized matching tmp recovery');assert.equal([record.keyId,...record.retiring.map(r=>r.keyId)].every(keyId=>f.state.probes.some(p=>p.keyId===keyId&&p.status===401)),true);f.valueFree();
});
test('656 active final rewrite self-repair remains a separate existing control',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed({valid:true});await f.lifecycle('fire');assert.equal(f.record()===null,true);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);assert.equal(f.listed(),false);f.valueFree();
});
for(const how of ['fire','delete'])test('656 ordinary no-model no-tmp '+how+' stays ordinary idempotence',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});await f.lifecycle(how);assert.equal(f.record()===null,true);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);assert.equal(f.listed(),false);f.valueFree();
});
test('656 unrelated service fence and bytes remain unchanged after model work',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();const other=join(f.storeRoot,'demo','aide','gitea'),body='owned unrelated\n';fs.writeFileSync(other,body,{mode:0o600});const before=f.membershipHash();await refused(f);assert.equal(f.membershipHash(),before);assert.equal(id(fs.readFileSync(other)),id(body));f.valueFree();
});
test('656 ordinary present no-tmp intent with missing boot registry refuses before removal',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true,missingBoot:true}),record=f.seed({matchingTmp:false}),before=f.membershipHash(),puts=f.state.calls.filter(m=>m==='PUT').length;
const failure=await outcome(()=>f.lifecycle('fire'));assert.equal(!failure.ok&&failure.code==='credential-revoke-failed',true,'ordinary present intent with missing boot view refuses before removal');assert.equal(f.record()?.state,'revoking');assert.equal(f.exists(f.file),true);assert.equal(f.state.calls.filter(m=>m==='PUT').length,puts);assert.equal(f.remoteHas(record.key),true);assert.equal(f.membershipHash(),before);
f.writeSessions([]);await refused(f);assert.equal(f.state.calls.filter(m=>m==='PUT').length,puts,'later Registry empty does not repair failed boot proof');assert.equal(f.membershipHash(),before);f.valueFree();
});
test('656 authoritative owned empty boot permits ordinary present no-tmp lifecycle completion',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed({matchingTmp:false});await f.lifecycle('fire');assert.equal(f.record()===null,true);assert.equal(f.exists(f.file)||f.exists(f.temporary),false);assert.equal(f.listed(),false);f.valueFree();
});
for(const late of ['final unlink failure','tmp absence read failure'])test('656 '+late+' after tmp removal stays revoking then authoritative restart completes',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();const before=f.membershipHash();if(late==='final unlink failure')f.faults.unlinkFinal=true;else f.faults.absenceTmp=true;
const failure=await outcome(()=>f.lifecycle('fire'));assert.equal(f.record()?.state==='revoking',true,'late recovery failure retains cached revoking fence');assert.equal(failure.ok,false);assert.equal(failure.code,'credential-revoke-failed');assert.equal(f.exists(f.file),true);assert.equal(f.exists(f.temporary),false,'tmp removal succeeded before this late refusal');assert.equal(f.membershipHash(),before);f.faults.unlinkFinal=false;f.faults.absenceTmp=false;
await f.reboot();assert.equal(f.record()===null,true);assert.equal(f.exists(f.file)||f.exists(f.temporary),false,'new authoritative boot reacquires strict proof, not vanished token');f.valueFree();
});
test('656 missing boot after tmp removed never turns remaining final into exit proof',{timeout},async t=>{
const f=await revocationFixture(t,{runtime:true});f.seed();f.faults.unlinkFinal=true;await outcome(()=>f.lifecycle('fire'));assert.equal(f.record()?.state==='revoking',true,'late recovery failure retains cached revoking fence');assert.equal(f.exists(f.temporary),false);f.faults.unlinkFinal=false;fs.unlinkSync(join(f.dataRoot,'bus-host','sessions.json'));const puts=f.state.calls.filter(m=>m==='PUT').length,before=f.membershipHash();await f.reboot();
assert.equal(f.record()?.state,'revoking');assert.equal(f.exists(f.file),true);assert.equal(f.state.calls.filter(m=>m==='PUT').length,puts,'failed boot refuses before new removal PUT');assert.equal(f.membershipHash(),before);await assert.rejects(f.bus.modelPreflight({business:'demo',role:'aide'}),e=>e.code==='proxy-key-revoking');f.writeSessions([]);await refused(f);assert.equal(f.state.calls.filter(m=>m==='PUT').length,puts);f.valueFree();
});
@@ -12,8 +12,12 @@ import { fixture } from './spec-fixtures.mjs';
test('Broker keeps the sandbox client import closure: only the listed imports, no lazy project import', () => { 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'); const source = readFileSync(new URL('../src/broker.mjs', import.meta.url), 'utf8');
assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), [ assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), [
// Rows330/331: pure verb/refusal vocabulary, never the host management implementation.
"import { PROXY_PAGE_VERBS, PROXY_PAGE_CODES } from './proxy-page-vocabulary.mjs';",
// Row 210 (#1707): createHash gives host.run events the argv, stdin and output sha256 values. // Row 210 (#1707): createHash gives host.run events the argv, stdin and output sha256 values.
"import { randomUUID, randomBytes, createHash } from 'node:crypto';", "import { randomUUID, randomBytes, createHash } from 'node:crypto';",
// Row 331 r5: parsing-only validation, individually mounted; no PIN or host state.
"import { validManagementKey, proxyTiming } from '../../model-proxy/src/validation.mjs';",
// Row 265 (#1770): pure room address parsing, mounted in RUNNER_FILES beside the broker. // Row 265 (#1770): pure room address parsing, mounted in RUNNER_FILES beside the broker.
"import { parseAddress } from './address.mjs';", "import { parseAddress } from './address.mjs';",
// Row 65 (#1558): bindLaunch checks a run's transcript path is absolute. // Row 65 (#1558): bindLaunch checks a run's transcript path is absolute.
@@ -38,6 +42,7 @@ test('Broker keeps the sandbox client import closure: only the listed imports, n
"import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers, grantScopeOf, grantInScope } from '../../business/src/vocabulary.mjs';", "import { ID_PATTERN, THREAD_DEFINITIONS, END_PROTECTED, forkProblem, forkUrl, credentialKind, validArgv, commandAllowed, HOST_ID, CREATIVE_DEFINITIONS, unhireableAction, giteaName, parseGiteaUrl, covers, grantScopeOf, grantInScope } from '../../business/src/vocabulary.mjs';",
]); ]);
assert.doesNotMatch(source, /\bimport\s*\(/); assert.doesNotMatch(source, /\bimport\s*\(/);
assert.doesNotMatch(readFileSync(new URL('../src/proxy-page-vocabulary.mjs', import.meta.url), 'utf8'), /\bimport\b/);
assert.doesNotMatch(readFileSync(new URL('../src/landing.mjs', import.meta.url), 'utf8'), /\bimport\b/); 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('../../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/); assert.doesNotMatch(readFileSync(new URL('../../plugins/src/kinds.mjs', import.meta.url), 'utf8'), /\bimport\b/);
@@ -0,0 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {revocationFixture} from './model-revocation-fixture.mjs';
// Join control: the real runtime supplies P2's cached metadata to P4. This
// fixture's seeded record is read evidence only, not issuance/ownership proof.
test('Console key view uses the same runtime model record without exposing its value', {timeout:30000}, async t=>{
const f=await revocationFixture(t,{runtime:true});
const record=f.seed({valid:true,matchingTmp:false});
const human=f.bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const result=await f.bus.broker.request(human,{verb:'proxy.keys',args:{}});
assert.deepEqual(result.keys.find(row=>row.keyId===record.keyId),{
keyId:record.keyId,management:'stack-managed',agent:'aide',issuedAt:record.issuedAt,state:'valid',
});
assert.equal(JSON.stringify(result).includes(record.key),false,'no model value enters the Console result');
f.valueFree();
});
@@ -0,0 +1,81 @@
// P4 named mutant: returning any unfiltered credential field. Every projection
// and write hits the real management client against a local stand-in server.
import test from 'node:test';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { fakeManagement } from './proxy-page-fixture.mjs';
const id = value => createHash('sha256').update(value).digest('hex').slice(0, 12);
const oauth = extra => ({ id: 'oauth-one.json', name: 'oauth-one.json', provider: 'claude', label: 'p4-private-label',
email: 'p4-private-email', token: 'p4-private-token', status: 'ready', priority: 3, ...extra });
test('accounts have exactly the allowlisted public fields, id from credential identifier', async t => {
const s = await fakeManagement(t, { files: [oauth()] });
const result = await s.call('proxy.accounts');
assert.deepEqual(result, { accounts: [{ accountId: id('oauth-one.json'), provider: 'claude',
displayName: `Claude account ${id('oauth-one.json')}`, status: 'ready', priority: 3 }] });
for (const value of ['p4-private-label', 'p4-private-email', 'p4-private-token']) {
assert.equal(JSON.stringify(result).includes(value), false);
assert.notEqual(result.accounts[0].accountId, id(value));
assert.equal(JSON.stringify(s.state.receipts).includes(value), false);
}
});
test('token-like data in every upstream field never reaches public accounts', async t => {
const toxic = 'p4-private-token';
const s = await fakeManagement(t, { files: [oauth({ label: toxic, email: toxic, token: toxic, status: toxic,
provider: toxic, account: toxic, account_type: toxic, auth_index: toxic, path: toxic, size: toxic, priority: toxic })] });
const result = await s.call('proxy.accounts');
assert.equal(JSON.stringify(result).includes(toxic), false);
assert.equal(JSON.stringify(s.state.receipts).includes(toxic), false);
assert.deepEqual(Object.keys(result.accounts[0]).sort(), ['accountId', 'displayName', 'priority', 'provider', 'status']);
});
test('API key account is derived from proxy auth_index, never api-key, and can set priority', async t => {
const s = await fakeManagement(t);
await s.call('proxy.apikey.add', { provider: 'z.ai', key: 'p4-canary-key' });
const first = (await s.call('proxy.accounts')).accounts[0];
assert.equal(first.accountId, id('upstream-1')); assert.notEqual(first.accountId, id('p4-canary-key'));
assert.equal(first.provider, 'z.ai'); assert.equal(first.priority, 0);
// Fake preserves derived identifiers across writes, just as the pinned proxy does.
// The next write is expected to strip derived auth_index before sending config.
await s.call('proxy.priority.set', { accountId: first.accountId, priority: 7 });
assert.equal((await s.call('proxy.accounts')).accounts[0].priority, 7);
assert.equal(JSON.stringify(s.state.receipts).includes('p4-canary-key'), false);
});
test('OAuth priority uses top-level name/priority and confirms it through list', async t => {
const s = await fakeManagement(t, { files: [oauth()] });
assert.deepEqual(await s.call('proxy.priority.set', { accountId: id('oauth-one.json'), priority: 8 }),
{ accountId: id('oauth-one.json'), priority: 8 });
assert.equal(s.state.files[0].priority, 8);
assert.equal(s.state.calls.filter(c => c === 'PATCH /v8/management/credentials/fields').length, 1);
});
test('priority-unconfirmed: accepted API-key write with unchanged read-back refuses', async t => {
const s = await fakeManagement(t, { priorityNoop: true,
groups: [{ name: 'z.ai', 'base-url': 'https://api.z.ai/api/anthropic', priority: 0,
keys: [{ 'api-key': 'p4-upstream', auth_index: 'upstream-1' }] }] });
await assert.rejects(s.call('proxy.priority.set', { accountId: id('upstream-1'), priority: 8 }), { code: 'priority-unconfirmed' });
});
for (const status of [201, 400]) test(`priority-write-refused: OAuth PATCH status ${status}`, async t => {
const s = await fakeManagement(t, { files: [oauth()], patchStatus: status });
await assert.rejects(s.call('proxy.priority.set', { accountId: id('oauth-one.json'), priority: 8 }), { code: 'priority-write-refused' });
assert.equal(s.state.calls.filter(c => c.startsWith('PATCH ')).length, 1);
});
test('priority-invalid: unknown account refuses before any write', async t => {
const s = await fakeManagement(t);
await assert.rejects(s.call('proxy.priority.set', { accountId: 'a'.repeat(12), priority: 8 }), { code: 'priority-invalid' });
assert.equal(s.state.calls.some(c => c.startsWith('PUT ') || c.startsWith('PATCH ')), false);
});
test('proxy-down: a closed management listener refuses once', async t => {
const s = await fakeManagement(t);
await new Promise(r => s.server.close(r));
await assert.rejects(s.call('proxy.accounts'), { code: 'proxy-down' });
assert.equal(s.state.receipts.length, 1);
});
test('client keys return ids, sentinel/foreign labels and only scoped record metadata', async t => {
const s = await fakeManagement(t, { keys: ['msk-fixture-sentinel-x', 'msk-fixture-agent', 'foreign-key'],
records: [{ keyId: id('msk-fixture-agent'), agent: 'coder', issuedAt: '2026-10-24T00:00:00Z', state: 'valid' }] });
const result = await s.call('proxy.keys');
assert.deepEqual(result, { keys: [
{ keyId: id('msk-fixture-sentinel-x'), management: 'sentinel', agent: null, issuedAt: null, state: null },
{ keyId: id('msk-fixture-agent'), management: 'stack-managed', agent: 'coder', issuedAt: '2026-10-24T00:00:00Z', state: 'valid' },
{ keyId: id('foreign-key'), management: 'not stack-managed', agent: null, issuedAt: null, state: null },
] });
for (const value of s.state.keys) assert.equal(JSON.stringify(result).includes(value), false);
});
@@ -0,0 +1,33 @@
// P4 real client/page regression: fresh pinned proxy404 must allow the first key.
// Malformed successful bodies still fail closed with the existing refusal code.
import test from 'node:test';
import assert from 'node:assert/strict';
import { fakeManagement } from './proxy-page-fixture.mjs';
const route = '/v8/management/config/api-keys/claude';
test('fresh404 yields no accounts, then adds the first key once with no value leakage', async t => {
const canary = 'p4-first-key';
const s = await fakeManagement(t, { emptyProviderResponse: { status: 404, body: { error: 'p4-body-canary' } } });
const empty = await s.call('proxy.accounts');
assert.equal(Array.isArray(empty.accounts), true); assert.equal(empty.accounts.length, 0);
const added = await s.call('proxy.apikey.add', { provider: 'z.ai', key: canary });
assert.equal(added.added, true);
const populated = await s.call('proxy.accounts');
assert.equal(populated.accounts.length, 1); assert.equal(populated.accounts[0].provider, 'z.ai');
assert.equal(s.state.receives, 1, 'one PUT, no retry');
assert.equal(s.state.groups[0].keys[0]['api-key'] === canary, true);
assert.deepEqual(s.state.calls, ['GET /v8/management/credentials', `GET ${route}`, `GET ${route}`, `PUT ${route}`,
'GET /v8/management/credentials', `GET ${route}`]);
assert.deepEqual(s.state.receipts.filter(r => r.call === `GET ${route}`).map(r => r.status), [404, 404, 200]);
for (const value of [canary, 'p4-body-canary', 'p4-management']) {
assert.equal(JSON.stringify([empty, added, populated]).includes(value), false);
assert.equal(JSON.stringify(s.state.receipts).includes(value), false);
}
});
for (const [name, body] of [['null', null], ['object', { groups: [] }]])
test(`malformed200 ${name} remains proxy-down, no retry or write`, async t => {
const s = await fakeManagement(t, { emptyProviderResponse: { status: 200, body } });
await assert.rejects(s.call('proxy.accounts'), { code: 'proxy-down' });
assert.deepEqual(s.state.calls, ['GET /v8/management/credentials', `GET ${route}`]);
assert.equal(s.state.receives, 0);
assert.deepEqual(s.state.receipts.map(r => r.status), [200, 200]);
});
+49
View File
@@ -0,0 +1,49 @@
// Local management stand-in. All credentials made up, kept only in this test's
// memory; logs collect endpoint names, never query strings, headers or bodies.
import http from 'node:http';
import { ManagementClient } from '../../model-proxy/src/client.mjs';
import { ProxyPage } from '../src/proxy-page.mjs';
export async function fakeManagement(t, options = {}) {
const state = { files: [], groups: [], keys: ['msk-fixture-sentinel-x', 'foreign-key'], receives: 0,
calls: [], receipts: [], priorityNoop: false, patchStatus: 200, ...options };
let nextId = 1;
const server = http.createServer(async (req, res) => {
const u = new URL(req.url, 'http://localhost');
const chunks = []; for await (const c of req) chunks.push(c);
const value = chunks.length ? JSON.parse(Buffer.concat(chunks)) : null;
state.calls.push(req.method + ' ' + u.pathname);
const reply = (status, body) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); };
if (req.headers.authorization !== 'Bearer p4-management') return reply(401, { error: 'refused' });
if (u.pathname === '/v8/management/credentials') return reply(200, { files: state.files, observed_at: '2026-10-24T00:00:00Z' });
if (u.pathname === '/v8/management/config/api-keys/claude') {
if (req.method === 'GET') {
if (state.groups.length === 0 && state.emptyProviderResponse)
return reply(state.emptyProviderResponse.status, state.emptyProviderResponse.body);
return reply(200, state.groups);
}
// Stand-in treats auth_index as derived/read-only metadata.
if (value.some(g => (g.keys ?? []).some(k => Object.hasOwn(k, 'auth_index')))) return reply(422, { error: 'derived field' });
state.receives++;
if (!state.priorityNoop) state.groups = value.map(g => ({ ...g, keys: g.keys.map(k => ({ ...k, auth_index: state.groups.flatMap(old => old.keys ?? []).find(old => old['api-key'] === k['api-key'])?.auth_index ?? `upstream-${nextId++}` })) }));
return reply(200, { status: 'ok' });
}
if (u.pathname === '/v8/management/credentials/fields') {
if (state.patchStatus !== 200) return reply(state.patchStatus, { error: 'p4-private-detail' });
const row = state.files.find(f => f.name === value.name);
if (!row) return reply(404, { error: 'not found' });
if (!state.priorityNoop) row.priority = value.priority;
return reply(200, { status: 'ok' });
}
if (u.pathname === '/v8/management/config/access/api-keys') return reply(200, state.keys);
if (u.pathname === '/v8/management/oauth/auth-url') return reply(200, { status: 'ok', state: 'p4-state', url: 'https://claude.ai/oauth/authorize?state=p4-state' });
if (u.pathname === '/v8/management/oauth/status') return reply(200, { status: 'wait' });
if (u.pathname === '/v8/management/oauth/callback' || u.pathname === '/v8/management/oauth/session') return reply(200, { status: 'ok' });
return reply(404, {});
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
t.after(() => new Promise(r => server.close(r)));
const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${server.address().port}`, key: 'p4-management', receipt: entry => state.receipts.push(entry) });
const page = new ProxyPage({ client, instanceTag: 'fixture', keyRecords: () => options.records ?? [] });
const call = (verb, args = {}) => page.request(verb, args, { business: 'demo', human: 'owner' });
return { state, page, call, client, server };
}
@@ -0,0 +1,80 @@
// P4: callback must match an unfinished, unexpired login owned by this human/business.
// Named mutant: skip pending-state check. Canary errors must never reflect upstream input.
import test from 'node:test';
import assert from 'node:assert/strict';
const moduleURL = new URL('../src/proxy-page.mjs', import.meta.url);
const owner = { business: 'demo', human: 'owner' };
async function setup(options = {}) {
const { ProxyPage } = await import(moduleURL);
let now = 1000;
const calls = [];
const client = Object.fromEntries(['getCredentials', 'getKeys', 'oauthStart', 'oauthCallback', 'oauthStatus', 'oauthCancel',
'getUpstreamGroups', 'putUpstreamGroups', 'patchCredential'].map(name => [name, async (...args) => {
calls.push([name, ...args]);
if (name === 'oauthStart') return { status: 'ok', state: 'p4-state', url: 'https://claude.ai/oauth/authorize?state=p4-state' };
if (name === 'oauthStatus') return { status: 'wait' };
if (name === 'getCredentials') return { files: [] };
if (name === 'getUpstreamGroups') return [];
if (name === 'getKeys') return [];
return { status: 'ok' };
}]));
const p = new ProxyPage({ client, instanceTag: 'fixture', now: () => now, ...options });
return { client, calls, p, time: value => { now = value; }, call: (verb, args = {}, who = owner) => p.request(verb, args, who) };
}
for (const [verb, args] of [
['proxy.login.start', { provider: 'other' }], ['proxy.apikey.add', { provider: 'other', key: 'fixture-key' }],
]) test(`${verb}: provider-not-allowed`, async () => {
const s = await setup(); await assert.rejects(s.call(verb, args), { code: 'provider-not-allowed' }); assert.equal(s.calls.length, 0);
});
for (const [name, key] of [['empty', ''], ['too long', 'x'.repeat(4097)], ['charset', 'bad\nkey']])
test(`apikey-invalid ${name}`, async () => {
const s = await setup(); await assert.rejects(s.call('proxy.apikey.add', { provider: 'z.ai', key }), { code: 'apikey-invalid' }); assert.equal(s.calls.length, 0);
});
for (const [name, url] of [['not URL', 'not a url'], ['oversize', 'http://localhost/?' + 'a'.repeat(8193)],
['repeated state', 'http://localhost/?state=p4-state&state=p4-state']])
test(`callback-url-invalid ${name}`, async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
await assert.rejects(s.call('proxy.login.submit', { redirect_url: url }), { code: 'callback-url-invalid' });
assert.equal(s.calls.filter(c => c[0] === 'oauthCallback').length, 0);
});
test('login-state-unknown callback never reaches proxy', async () => {
const s = await setup();
await assert.rejects(s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=unknown&code=p4-code' }), { code: 'login-state-unknown' });
assert.equal(s.calls.length, 0);
});
test('login state is scoped to the human and business', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
for (const who of [{ business: 'other', human: 'owner' }, { business: 'demo', human: 'other' }])
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }, who), { code: 'login-state-unknown' });
assert.equal(s.calls.length, 1);
});
test('accepted callback wait is pending; only a status ok completes and retires state', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
assert.deepEqual(await s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=p4-state&code=p4-code' }), { status: 'wait' });
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'wait' });
s.client.oauthStatus = async () => ({ status: 'ok', token: 'never-return' });
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'ok' });
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' });
await assert.rejects(s.call('proxy.login.submit', { redirect_url: 'http://localhost/?state=p4-state&code=p4-code' }), { code: 'login-state-unknown' });
});
test('expiry reports login-timeout once with no management call and retires state', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' }); s.time(301000);
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'error', reason: 'login-timeout' });
assert.equal(s.calls.length, 1);
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' });
});
test('cancel calls DELETE and retires state', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
assert.deepEqual(await s.call('proxy.login.cancel', { state: 'p4-state' }), { status: 'cancelled' });
assert.deepEqual(s.calls.at(-1), ['oauthCancel', 'p4-state']);
await assert.rejects(s.call('proxy.login.status', { state: 'p4-state' }), { code: 'login-state-unknown' });
});
test('upstream OAuth error body never crosses bus response', async () => {
const s = await setup(); await s.call('proxy.login.start', { provider: 'claude' });
s.client.oauthStatus = async () => ({ status: 'error', error: 'p4-private-code', state: 'p4-state' });
assert.deepEqual(await s.call('proxy.login.status', { state: 'p4-state' }), { status: 'error', reason: 'login-failed' });
});
for (const args of [{ accountId: 'bad', priority: 1 }, { accountId: 'a'.repeat(12), priority: 1.5 },
{ accountId: 'a'.repeat(12), priority: 1001 }]) test('priority-invalid ' + JSON.stringify(args), async () => {
const s = await setup(); await assert.rejects(s.call('proxy.priority.set', args), { code: 'priority-invalid' });
});
+55
View File
@@ -0,0 +1,55 @@
// P4: broker security boundary. Mutants: granted user accepted; secret in error/log.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Broker } from '../src/broker.mjs';
import { Store } from '../src/store.mjs';
const verbs = ['proxy.accounts', 'proxy.login.start', 'proxy.login.submit', 'proxy.login.status',
'proxy.login.cancel', 'proxy.apikey.add', 'proxy.priority.set', 'proxy.keys'];
function setup(t, proxyPage = async () => ({ ok: true })) {
const root = mkdtempSync(join(tmpdir(), 'p4-bus-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const calls = [];
const broker = new Broker({ store, proxyPage: proxyPage && (async (...args) => { calls.push(args); return proxyPage(...args); }),
access: () => ({ accessVersion: 1, revision: 1, users: [], grants: [
{ id: 'g-1', user: 'alice', business: 'demo', projects: ['*'], instances: ['coder'], level: 'collaborator' },
] }),
businesses: { demo: { id: 'demo', human: 'owner', arbiters: { technical: 'coder', delivery: 'coder' },
vars: { 'bus.tester': true }, roles: { coder: { definition: 'coder', authority: { withinRole: ['message.send'], crossRole: [] } } } } },
});
const caps = {
human: broker.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true }),
tester: broker.bindTester({ business: 'demo' }),
agent: broker.bindLaunch({ business: 'demo', role: 'coder', run: 'p4-run', harness: 'pi' }),
user: broker.bindUser({ business: 'demo', user: 'alice' }),
};
return { calls, store, call: (who, verb, args = {}) => broker.request(caps[who], { verb, args }) };
}
for (const verb of verbs) {
test(`${verb}: owner calls page once with business/human scope`, async t => {
const s = setup(t);
assert.deepEqual(await s.call('human', verb), { ok: true });
assert.deepEqual(s.calls, [[verb, {}, { business: 'demo', human: 'owner' }]]);
});
for (const who of ['tester', 'agent', 'user']) test(`${verb}: ${who} human-required`, async t => {
const s = setup(t);
await assert.rejects(async () => s.call(who, verb), { code: 'human-required' });
assert.equal(s.calls.length, 0);
});
test(`${verb}: not-configured`, async t => {
const s = setup(t, null);
await assert.rejects(async () => s.call('human', verb), { code: 'not-configured' });
});
}
test('API-key canary never enters broker events or unknown error output', async t => {
const canary = 'p4-canary-key';
const s = setup(t, async () => { throw new Error(canary); });
await assert.rejects(async () => s.call('human', 'proxy.apikey.add', { provider: 'z.ai', key: canary }),
e => e.code === 'proxy-down' && !e.message.includes(canary));
assert.equal(s.calls.length, 1);
assert.equal(JSON.stringify(s.store.all('SELECT body FROM events')).includes(canary), false);
});
+10 -2
View File
@@ -351,7 +351,7 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
// hireable checks. args { name, definition, vars? }. A new name is added and listed. A fired name // hireable checks. args { name, definition, vars? }. A new name is added and listed. A fired name
// (declared, not listed) is listed again only with its own definition and no vars. The writer // (declared, not listed) is listed again only with its own definition and no vars. The writer
// reads the file as it stands, so a veto or retire written since the broker's copy still counts. // reads the file as it stands, so a veto or retire written since the broker's copy still counts.
function hire(business, args, { actor, context }) { function prepareHire(business, args) {
const { document: old, revision } = writer.read(business); const { document: old, revision } = writer.read(business);
if (!old.launch) throw new OrganizationError("organization-launch-not-configured"); if (!old.launch) throw new OrganizationError("organization-launch-not-configured");
const { name, definition } = args; const { name, definition } = args;
@@ -380,6 +380,14 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
// mos-1585: duplicates refuse before definition eligibility, with no file or credential effect. // mos-1585: duplicates refuse before definition eligibility, with no file or credential effect.
definitionFor(rolesDir, definition); definitionFor(rolesDir, definition);
next.launch.instances.push(name); next.launch.instances.push(name);
return { old, next, revision };
}
// Trusted read-only lifecycle preparation; the writer uses the same checks again.
function checkHire(business, args) {
return { revision: prepareHire(business, args).revision };
}
function hire(business, args, { actor, context }) {
const { old, next, revision } = prepareHire(business, args);
return commit(business, old, next, { revision, actor, context }); return commit(business, old, next, { revision, actor, context });
} }
@@ -465,5 +473,5 @@ export function instanceSettings({ dir, rolesDir, system, levels = () => null, r
return { ...read(business, actor), transaction: receipt.transaction, restart: false }; return { ...read(business, actor), transaction: receipt.transaction, restart: false };
} }
return { writer, read, set, checkDelete, checkFire, veto, hire, fire, grantScope, grantScopeRead }; return { writer, read, set, checkDelete, checkFire, checkHire, veto, hire, fire, grantScope, grantScopeRead };
} }
+1 -1
View File
@@ -45,7 +45,7 @@ export const USAGE = `usage:
${LIMITS_USAGE} ${LIMITS_USAGE}
${PLUGINS_USAGE} ${PLUGINS_USAGE}
${CREDENTIALS_USAGE} ${CREDENTIALS_USAGE}
mosaic proxy fetch | init [--port N] [--business ID] mosaic proxy fetch | init [--port N] [--business ID] [--rotate-grace-minutes N] [--first-output-seconds N]
${HOSTS_USAGE} ${HOSTS_USAGE}
${GRANTS_USAGE} ${GRANTS_USAGE}
${INSTANCES_USAGE} ${INSTANCES_USAGE}
+22 -5
View File
@@ -139,7 +139,9 @@
// transaction; a later delete retries it. // transaction; a later delete retries it.
import { createServer } from "node:net"; import { createServer } from "node:net";
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs"; import { createHash } from 'node:crypto';
import { watchModelOutput } from './model-stall.mjs';
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, relative } from "node:path"; import { dirname, join, relative } from "node:path";
import { configDir, loadBusiness, resolveInstance, systemVars } from "../../business/src/index.mjs"; import { configDir, loadBusiness, resolveInstance, systemVars } from "../../business/src/index.mjs";
import { THREAD_DEFINITIONS } from "../../business/src/vocabulary.mjs"; import { THREAD_DEFINITIONS } from "../../business/src/vocabulary.mjs";
@@ -507,6 +509,16 @@ export function createLauncher({
({resolved,model,account,harness,route,routeReason}=await check(instance,{pm:instance===b.launch.by,thread,session,human})); ({resolved,model,account,harness,route,routeReason}=await check(instance,{pm:instance===b.launch.by,thread,session,human}));
admission(admissionGeneration); admission(admissionGeneration);
const { run, runDir } = newRun(dataRoot, b.id); const { run, runDir } = newRun(dataRoot, b.id);
let proxy = null, modelKeyId;
if (system.proxy) {
try { proxy = await host.op({ op: 'modelPreflight', business: b.id, role: instance }); }
catch (error) { throw new Refusal(error.code ?? 'proxy-down', error.code ?? 'proxy-down'); }
if (!proxy || typeof proxy.key !== 'string' || proxy.baseUrl !== `http://127.0.0.1:${system.proxy.port}`) throw new Refusal('proxy-down', 'proxy-down');
modelKeyId = createHash('sha256').update(proxy.key).digest('hex').slice(0, 12);
// Internal opt-in only: no session.json, user environment or config field.
try { writeFileSync(join(runDir, 'proxy-first-turn-enabled'), 'enabled\n', { flag: 'wx', mode: 0o600 }); }
catch { throw new Refusal('launch-failed', 'first-turn protocol setup failed'); }
}
const sessionDir = store ? store.session : join(runDir, "session"); const sessionDir = store ? store.session : join(runDir, "session");
if (!store) mkdirSync(sessionDir, { mode: 0o700 }); if (!store) mkdirSync(sessionDir, { mode: 0o700 });
if (store) compactions(store, model.harness, harness, { instance, thread, session, run }); if (store) compactions(store, model.harness, harness, { instance, thread, session, run });
@@ -522,6 +534,7 @@ export function createLauncher({
const holding = holders?.filter((h) => h.instance === instance) ?? null; const holding = holders?.filter((h) => h.instance === instance) ?? null;
const land = thread ? await host.op({ op: "landHeld", record: { business: b.id, role: instance, thread } }) : holding.length > 0; const land = thread ? await host.op({ op: "landHeld", record: { business: b.id, role: instance, thread } }) : holding.length > 0;
const bundle = buildBundle({ const bundle = buildBundle({
...(proxy ? { proxy } : {}),
dir: join(runDir, "bundle"), dir: join(runDir, "bundle"),
resolved, resolved,
models: b.launch.models, models: b.launch.models,
@@ -611,12 +624,13 @@ export function createLauncher({
admission(admissionGeneration); admission(admissionGeneration);
if (!outerStart) throw new Error("the session process exited at once"); if (!outerStart) throw new Error("the session process exited at once");
lock?.update({ run, runPid: child.pid, runStartTime: outerStart }); lock?.update({ run, runPid: child.pid, runStartTime: outerStart });
const launch = await host.bindLaunch({ business: b.id, role: instance, run, harness: model.harness, provider: model.provider, model: model.model, account, transcript, pid: child.pid, startTime: outerStart, ...(thread ? { thread, session } : {}) }); const launch = await host.bindLaunch({ business: b.id, role: instance, run, harness: model.harness, provider: model.provider, model: model.model, account, transcript, pid: child.pid, startTime: outerStart, ...(thread ? { thread, session } : {}), ...(proxy ? { modelKeyId } : {}) });
bound = true; bound = true;
admission(admissionGeneration); admission(admissionGeneration);
entry = { entry = {
business: b.id, instance, ...(thread ? { thread, session, workspace } : {}), run, launchedBy, harness: model.harness, model: model.model, provider: model.provider, modelKey: model.modelKey, business: b.id, instance, ...(thread ? { thread, session, workspace } : {}), run, launchedBy, harness: model.harness, model: model.model, provider: model.provider, modelKey: model.modelKey,
pid: child.pid, startTime: outerStart, runnerPid: runner.pid, runnerStartTime: runner.startTime, runDir, startedAt: new Date().toISOString(), pid: child.pid, startTime: outerStart, runnerPid: runner.pid, runnerStartTime: runner.startTime, runDir, startedAt: new Date().toISOString(),
...(proxy ? { modelKeyId } : {}),
}; };
registry.add(entry); registry.add(entry);
// Row 156: one start per decision, whichever launch wrote it into landing. // Row 156: one start per decision, whichever launch wrote it into landing.
@@ -630,10 +644,14 @@ export function createLauncher({
spawnedAt.delete(run); spawnedAt.delete(run);
unregisterRuntime(dataRoot, managed.id); unregisterRuntime(dataRoot, managed.id);
if (e instanceof Refusal) throw e; if (e instanceof Refusal) throw e;
if (e.code?.startsWith('proxy-')) throw new Refusal(e.code, e.code);
throw new Refusal("launch-failed", e.message); throw new Refusal("launch-failed", e.message);
} }
children.set(run, exited); children.set(run, exited);
const outputWatch = proxy ? watchModelOutput({ runDir, armAtFirstTurn: true, keyId: modelKeyId, seconds: system.proxy.firstOutputSeconds ?? 120,
stop: () => host.op({ op: 'modelStall', business: b.id, role: instance, run, keyId: modelKeyId }), receipt: record }) : null;
exited.then(async ({ code, signal }) => { exited.then(async ({ code, signal }) => {
outputWatch?.cancel();
let receipt; let receipt;
try { receipt = runtime(dataRoot, managed.id); } try { receipt = runtime(dataRoot, managed.id); }
catch { log(`run ${run}: processing receipt unavailable at exit`); } catch { log(`run ${run}: processing receipt unavailable at exit`); }
@@ -1191,14 +1209,13 @@ export function createLauncher({
log(`run ${s.run} (${s.instance}) from an earlier host still runs (pid ${s.pid}, runner ${s.runnerPid}): no session of ${s.instance} starts until it exits`); log(`run ${s.run} (${s.instance}) from an earlier host still runs (pid ${s.pid}, runner ${s.runnerPid}): no session of ${s.instance} starts until it exits`);
} }
try { try {
await host.bindLaunch({ business: b.id, role: s.instance, run: s.run, harness: s.harness, pid: s.pid, startTime: s.startTime, ...(s.thread ? { thread: s.thread } : {}), ...(s.session ? { session: s.session } : {}) }); await host.op({ op: 'recoverLaunch', record: { business: b.id, role: s.instance, run: s.run, harness: s.harness, pid: s.pid, startTime: s.startTime, ...(s.thread ? { thread: s.thread } : {}), ...(s.session ? { session: s.session } : {}) } });
} catch (e) { } catch (e) {
// A host that died after ending the run but before updating sessions.json. // A host that died after ending the run but before updating sessions.json.
if (e.code === "run-ended") log(`run ${s.run} (${s.instance}) from an earlier host was already ended`); if (e.code === "run-ended") log(`run ${s.run} (${s.instance}) from an earlier host was already ended`);
else log(`run ${s.run} (${s.instance}) from an earlier host: not ended, bindLaunch refused: ${e.code ?? e.message}`); else log(`run ${s.run} (${s.instance}) from an earlier host: not ended, historical cleanup refused: ${e.code ?? e.message}`);
continue; continue;
} }
await endRun(s.run, "host-lost", null);
record({ event: "end", instance: s.instance, ...(s.thread ? { thread: s.thread } : {}), ...(s.session ? { session: s.session } : {}), run: s.run, reason: "host-lost", exitCode: null, signal: null }); record({ event: "end", instance: s.instance, ...(s.thread ? { thread: s.thread } : {}), ...(s.session ? { session: s.session } : {}), run: s.run, reason: "host-lost", exitCode: null, signal: null });
log(`run ${s.run} (${s.instance}) from an earlier host ended: host-lost`); log(`run ${s.run} (${s.instance}) from an earlier host ended: host-lost`);
} }
+9 -4
View File
@@ -1,6 +1,7 @@
import { CliError } from './errors.mjs'; import { CliError } from './errors.mjs';
import { refuseInsideAgent } from './transport.mjs'; import { refuseInsideAgent } from './transport.mjs';
import { fetchPinned } from '../../model-proxy/src/fetch.mjs'; import { fetchPinned } from '../../model-proxy/src/fetch.mjs';
import { validManagementKey, proxyTiming } from '../../model-proxy/src/common.mjs';
const failure = (code, exitCode = 2) => Object.assign(new CliError(code, exitCode), { code }); const failure = (code, exitCode = 2) => Object.assign(new CliError(code, exitCode), { code });
// A raw terminal suppresses echo; input is never argv, env, readline history or stdout. // A raw terminal suppresses echo; input is never argv, env, readline history or stdout.
export function promptProxyKey(io, label) { export function promptProxyKey(io, label) {
@@ -31,14 +32,14 @@ export function promptProxyKey(io, label) {
export async function proxyCommand(args, io, { system, callFor, prompt = promptProxyKey, fetch = fetchPinned }) { export async function proxyCommand(args, io, { system, callFor, prompt = promptProxyKey, fetch = fetchPinned }) {
const [verb, ...rest] = args; const [verb, ...rest] = args;
if (args.includes('--tester')) throw failure('human-required', 3); if (args.includes('--tester')) throw failure('human-required', 3);
if (verb === 'init') { if (verb === 'init' || verb === 'fetch') {
try { refuseInsideAgent(io.env); } catch { throw failure('human-required', 3); } try { refuseInsideAgent(io.env); } catch { throw failure('human-required', 3); }
} }
if (!['fetch', 'init'].includes(verb)) throw failure('proxy-usage', 4); if (!['fetch', 'init'].includes(verb)) throw failure('proxy-usage', 4);
const parsed = { flags: new Set(), values: {} }; let port; const parsed = { flags: new Set(), values: {} }; let port;
for (let i = 0; i < rest.length; i++) { for (let i = 0; i < rest.length; i++) {
const flag = rest[i]; const flag = rest[i];
if (verb !== 'init' || !['--business', '--port'].includes(flag) || parsed.values[flag] !== undefined || !rest[i + 1]) throw failure('proxy-usage', 4); if (verb !== 'init' || !['--business', '--port', '--rotate-grace-minutes', '--first-output-seconds'].includes(flag) || parsed.values[flag] !== undefined || !rest[i + 1]) throw failure('proxy-usage', 4);
parsed.values[flag] = rest[++i]; parsed.values[flag] = rest[++i];
} }
if (parsed.values['--port'] !== undefined) { if (parsed.values['--port'] !== undefined) {
@@ -46,6 +47,10 @@ export async function proxyCommand(args, io, { system, callFor, prompt = promptP
port = Number(parsed.values['--port']); port = Number(parsed.values['--port']);
if (port < 1024 || port > 65535 || port === 8317) throw failure('proxy-config-invalid'); if (port < 1024 || port > 65535 || port === 8317) throw failure('proxy-config-invalid');
} }
const timing = {};
for (const [flag, field] of [['--rotate-grace-minutes', 'rotateGraceMinutes'], ['--first-output-seconds', 'firstOutputSeconds']])
if (parsed.values[flag] !== undefined) timing[field] = /^[0-9]+$/.test(parsed.values[flag]) ? Number(parsed.values[flag]) : NaN;
try { proxyTiming(timing); } catch (error) { throw failure(error.code); }
try { try {
const config = typeof system === 'function' ? system() : system; const config = typeof system === 'function' ? system() : system;
if (verb === 'fetch') { const result = await fetch({ dataRoot: config.dataRoot }); io.stdout.write(JSON.stringify({ binary: result.binary, panel: result.panel }) + '\n'); return; } if (verb === 'fetch') { const result = await fetch({ dataRoot: config.dataRoot }); io.stdout.write(JSON.stringify({ binary: result.binary, panel: result.panel }) + '\n'); return; }
@@ -54,8 +59,8 @@ export async function proxyCommand(args, io, { system, callFor, prompt = promptP
const call = callFor(parsed); const call = callFor(parsed);
const first = await prompt(io, 'Management key: '), second = await prompt(io, 'Repeat management key: '); const first = await prompt(io, 'Management key: '), second = await prompt(io, 'Repeat management key: ');
if (first !== second) throw failure('proxy-key-mismatch'); if (first !== second) throw failure('proxy-key-mismatch');
if (!first.trim() || first.length > 4096 || /[\x00-\x1f\x7f]/.test(first)) throw failure('proxy-key-invalid'); if (!validManagementKey(first)) throw failure('proxy-key-invalid');
const result = await call('proxy.init', { key: first, ...(port === undefined ? {} : { port }) }); const result = await call('proxy.init', { key: first, ...(port === undefined ? {} : { port }), ...timing });
io.stdout.write(JSON.stringify(result) + '\n'); io.stdout.write(JSON.stringify(result) + '\n');
} catch (e) { } catch (e) {
if (e instanceof CliError) throw e; if (e instanceof CliError) throw e;
+45
View File
@@ -0,0 +1,45 @@
import { existsSync, openSync, fstatSync, readSync, closeSync, constants } from 'node:fs';
import { join } from 'node:path';
// A first-turn timestamp is protocol metadata only, never output or a credential.
function firstTurn(runDir) {
let fd;
try {
fd = openSync(join(runDir, 'proxy-first-turn'), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
const stat = fstatSync(fd);
if (!stat.isFile() || (stat.mode & 0o777) !== 0o600 || stat.uid !== process.getuid() || stat.size > 64) return 0n;
const bytes = Buffer.alloc(64), size = readSync(fd, bytes, 0, bytes.length, 0);
if (bytes.subarray(0, size).some(byte => byte > 127)) return 0n;
const match = /^turn ([1-9][0-9]{0,23})\n$/.exec(bytes.subarray(0, size).toString('ascii'));
if (!match || match[0].length !== size || size !== stat.size) return 0n;
const stamp = BigInt(match[1]);
return stamp > 0n && stamp <= process.hrtime.bigint() ? stamp : 0n;
} catch (error) { return error.code === 'ENOENT' ? null : 0n; }
finally { if (fd !== undefined) { try { closeSync(fd); } catch { return 0n; } } }
}
// Legacy utility callers retain their explicit construction-time budget. Managed
// proxy sessions opt into first-actual-turn arming in the launcher.
export function watchModelOutput({ runDir, seconds = 120, keyId, stop, receipt, armAtFirstTurn = false }) {
let timer, settled = false, finish;
const done = new Promise(resolve => { finish = resolve; });
const cancel = () => { if (settled) return; settled = true; clearTimeout(timer); finish(); };
const budget = BigInt(Math.ceil(seconds * 1e9));
let deadline = armAtFirstTurn ? null : process.hrtime.bigint() + budget;
const tick = async () => {
if (settled) return;
if (existsSync(join(runDir, 'proxy-first-output'))) { cancel(); return; }
if (deadline === null) {
const first = firstTurn(runDir);
if (first === null) { timer = setTimeout(tick, 1000); return; }
// Invalid protocol metadata fails closed through the same trusted stop;
// it cannot grant an idle session an indefinite or extended budget.
deadline = first === 0n ? 0n : first + budget;
}
const left = Number(deadline - process.hrtime.bigint()) / 1e6;
if (left > 0) { timer = setTimeout(tick, Math.min(1000, left)); return; }
try { await stop(); receipt({ action: 'proxy-stall', keyIds: [keyId] }); }
catch { receipt({ action: 'proxy-stall', code: 'proxy-stop-failed', keyIds: [keyId] }); }
finally { cancel(); }
};
timer = setTimeout(tick, 0);
return { cancel, done };
}
+13
View File
@@ -0,0 +1,13 @@
// Isolated owned FIFO probe: parent bounds/kills this exact child if I/O blocks.
import fs from 'node:fs';
import { join } from 'node:path';
import { watchModelOutput } from '../../src/model-stall.mjs';
import { getPriority, setPriority } from 'node:os';
// Enforce the gate's existing nice19 contract on this owned probe itself.
setPriority(0, 19);
let stops=0;const receipts=[];
const watch=watchModelOutput({runDir:process.argv[2],armAtFirstTurn:true,seconds:60,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:r=>receipts.push(r)});
const clock=setTimeout(watch.cancel,100);
await watch.done;clearTimeout(clock);
const marker=join(process.argv[2],'proxy-first-turn'),openMarkerFds=fs.readdirSync('/proc/self/fd').filter(n=>{try{return fs.readlinkSync('/proc/self/fd/'+n)===marker;}catch{return false;}}).length;
process.stdout.write(JSON.stringify({stops,receipts,nice:getPriority(0),openMarkerFds})+'\n');
@@ -0,0 +1,94 @@
// #653 isolated owned fixture: real launcher/runtime/runner and fake loopback management HTTP.
import fs from 'node:fs';
import http from 'node:http';
import assert from 'node:assert/strict';
import { createHash, randomBytes } from 'node:crypto';
import { spawnSync } from 'node:child_process';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { fixture, fakeHarnesses } from './helpers.mjs';
import { loadSystem, bootConfig } from '../src/config.mjs';
import { createLauncher } from '../src/launcher.mjs';
import { startBroker } from '../../bus/src/runtime.mjs';
import { gitEnv } from '../../seat/src/checkout.mjs';
import { readSessions, runsDir, sessionsFile, SESSIONS_VERSION, launchLogFile } from '../../seat/src/session.mjs';
export const waitFor = async (read, message, ms=15000) => { const end=Date.now()+ms;while(Date.now()<end){const value=await read();if(value)return value;await new Promise(r=>setTimeout(r,20));}throw Error(message); };
export const pause = ms => new Promise(r=>setTimeout(r,ms));
const id = value => createHash('sha256').update(value).digest('hex').slice(0,12);
export async function managedLaunchFixture(t,{proxy=true,launches=true,refuseProbe=false}={}){
const root=fs.mkdtempSync(join(tmpdir(),'ml653-')),closers=[],history=new Set(['fx653-manage']),proxyReceipts=[],logs=[];
let server,bus,launcher,stderr;const trace={preflightAttempts:0,successfulPreflights:0,enableBeforeSpawn:[],bindings:[],stalls:[]};
const checkHistory=entries=>{const text=JSON.stringify(entries);for(const value of history)if(text.includes(value))throw Error('fixture credential leaked');};
t.after(async()=>{const failures=[];for(const close of [...closers].reverse()){try{await close();}catch{failures.push('owned-close');}}try{server?.closeAllConnections();if(server?.listening)await new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));}catch{failures.push('owned-server-close');}try{checkHistory({logs,proxyReceipts,trace});}catch{failures.push('value-free-receipts');}if(stderr)process.stderr.write=stderr;try{fs.rmSync(root,{recursive:true,force:true});}catch{failures.push('owned-root-close');}if(failures.length)throw new AggregateError(failures.map(code=>Error(code)),'owned managed fixture cleanup failed');});
const f=fixture(root,'acme',doc=>{doc.plugins={...doc.plugins,harness:['pi']};for(const role of Object.values(doc.roles))role.vars={...role.vars,harness:'pi'};doc.roles.chief.vars.model='claude-opus-5-5';return doc;});
const env={PATH:process.env.PATH,HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,MOSAIC_CONFIG:f.config,MOSAIC_ROLES_DIR:f.roles};
const configDir=join(root,'config'),prefix=`msk-${id(configDir)}-`,sentinel=prefix+'sentinel-'+randomBytes(32).toString('base64url');history.add(sentinel);
const state={keys:[sentinel],calls:[],refuseProbe};
server=http.createServer(async(req,res)=>{try{
const supplied=req.headers.authorization?.slice(7);if(supplied)history.add(supplied);for(const key of state.keys)history.add(key);
if(req.url==='/v8/management/config/access/api-keys'){
state.calls.push(req.method);if(supplied!=='fx653-manage'){res.writeHead(401);res.end('{}');return;}
if(req.method==='GET'){res.end(JSON.stringify(state.keys));return;}
const parts=[];for await(const part of req)parts.push(part);const values=JSON.parse(Buffer.concat(parts));for(const value of values)history.add(value);state.keys=values;res.end('{"status":"ok"}');return;
}
state.calls.push('PROBE');res.writeHead(!state.refuseProbe&&state.keys.includes(supplied)?200:401);res.end('{}');
}catch{if(!res.destroyed){res.writeHead(500);res.end('{}');}}});
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});const port=server.address().port;assert.notEqual(port,8317);
fs.mkdirSync(join(configDir,'secrets'),{recursive:true,mode:0o700});fs.writeFileSync(join(configDir,'secrets','proxy-management.key'),'fx653-manage',{mode:0o600});
fs.mkdirSync(join(f.dataRoot,'bus-host'),{recursive:true,mode:0o700});fs.writeFileSync(sessionsFile(f.dataRoot),JSON.stringify({sessionsVersion:SESSIONS_VERSION,sessions:[]}),{mode:0o600});
const git=(...args)=>{const result=spawnSync('git',['-c','user.name=test','-c','[email protected]','-C',f.doc.projects.stack.root,...args],{env:gitEnv(env),encoding:'utf8'});assert.equal(result.status,0,'owned local git fixture succeeds');};
git('init','-q','-b','main');fs.writeFileSync(join(f.doc.projects.stack.root,'file.txt'),'one\n');git('add','file.txt');git('commit','-q','-m','one');
const adapter=join(root,'adapter.sh');fs.writeFileSync(adapter,'case "$MOSAIC_REQUEST" in *SILENT*) exec /usr/bin/sleep 30;; *) printf %s fx653-private-output; printf "done\\n" > "$MOSAIC_TURN_MARKER";; esac\n',{mode:0o644});
const system={...loadSystem({env}),...(proxy?{proxy:{port,binary:'/fixture-unused',firstOutputSeconds:1}}:{})};
const {trackers,...boot}=bootConfig({system,businessId:'acme',env});assert.equal(trackers,undefined);
stderr=process.stderr.write;process.stderr.write=function(chunk,...args){if(String(chunk).startsWith('model-proxy: ')){proxyReceipts.push(String(chunk));const callback=args.find(a=>typeof a==='function');callback?.();return true;}return stderr.call(this,chunk,...args);};
bus=await startBroker(boot);closers.push(()=>bus.close());
const hooks=[];let expectedKeyId;
const actualOp=async m=>{
switch(m.op){
case 'modelPreflight':return bus.modelPreflight(m);
case 'modelStall':return bus.modelStall(m);
case 'identity':return bus.identity(m.cap);
case 'authorizeLaunch':return bus.authorizeLaunch(m.cap,m.instance);
case 'refuse':return bus.refuse(m.cap,m.code,m.subject);
case 'endLaunch':return bus.endLaunch(m.record);
case 'recoverLaunch':return bus.recoverLaunch(m.record);
case 'credentialStatus':return bus.credentialStatus(m.business,m.role);
case 'mayLaunch':return bus.mayLaunch(m);
case 'landHolders':return bus.landHolders(m.business);
case 'landHeld':return bus.landHeld(m.record);
case 'landRelease':return bus.landRelease(m.record);
case 'threadsState':return bus.threadsState(m.business);
case 'threadIdle':return bus.threadIdle(m.record);
case 'lifecycleRequests':return bus.lifecycleRequests(m.business);
case 'sessionClears':return bus.sessionClears(m.business);
default:throw Error('fixture unrequested trusted operation');
}
};
const host={business:'acme',path:bus.path,dataRoot:f.dataRoot,beforeClose:fn=>hooks.push(fn),
op:async m=>{
if(m.op==='modelPreflight'){trace.preflightAttempts++;const value=await actualOp(m);if(value){trace.successfulPreflights++;expectedKeyId=id(value.key);}return value;}
if(m.op==='landHolders'&&trace.successfulPreflights){
// Existing trusted host call occurs after enable publication and BEFORE bundle,
// registerRuntime/spawnSession/bind in the production startLocked sequence.
const dirs=fs.readdirSync(runsDir(f.dataRoot,'acme'));assert.equal(dirs.length,1);const file=join(runsDir(f.dataRoot,'acme'),dirs[0],'proxy-first-turn-enabled');
assert.equal(fs.readFileSync(file,'utf8'),'enabled\n','managed caller publishes enable before spawn');assert.equal(fs.statSync(file).mode&0o777,0o600);assert.equal(trace.bindings.length,0);assert.equal(readSessions(f.dataRoot).length,0);trace.enableBeforeSpawn.push(true);
}
if(m.op==='modelStall'){const result=await actualOp(m);trace.stalls.push({run:m.run,keyId:m.keyId,stopped:result.stopped});return result;}
return actualOp(m);
},
bindLaunch:async record=>{const result=bus.bindLaunch(record);trace.bindings.push({run:record.run,pid:record.pid,startTime:record.startTime,modelKeyId:record.modelKeyId});return result;}};
const makeLoader=await fakeHarnesses({after:close=>closers.push(close)},{pi:adapter});
launcher=createLauncher({host,system,env,launches,makeLoader,sessionDefaults:{pollInterval:50},harnessVersion:'0.85.1',log:line=>logs.push(line)});
// The existing trusted host hook owns all launcher-created children. No registry
// PID is signalled by fixture teardown; normal bounded launcher close is used.
closers.push(async()=>{for(const close of hooks)await close(0);});await launcher.listen();
const human=bus.broker.bindHuman({business:'acme',human:'jason',via:'cli',outsideAgent:true});
const launchLog=()=>fs.existsSync(launchLogFile(f.dataRoot,'acme'))?fs.readFileSync(launchLogFile(f.dataRoot,'acme'),'utf8').trim().split('\n').filter(Boolean).map(l=>JSON.parse(l)):[];
const valueFree=()=>checkHistory({logs,proxyReceipts,launch:launchLog(),trace});
return {root,f,state,trace,proxyReceipts,logs,launcher,bus,launchLog,expectedKeyId:()=>expectedKeyId,valueFree,
runDirectories:()=>fs.existsSync(runsDir(f.dataRoot,'acme'))?fs.readdirSync(runsDir(f.dataRoot,'acme')).map(name=>join(runsDir(f.dataRoot,'acme'),name)):[],
sessions:()=>readSessions(f.dataRoot),send:body=>bus.broker.request(human,{verb:'message.send',args:{to:'chief',body,class:'REQUEST'}}),
reply:sent=>waitFor(()=>bus.broker.request(human,{verb:'message.receive'}).find(m=>m.in_reply_to===sent.id),'real managed reply observed'),
assertHistoryGuard:()=>{try{checkHistory({probe:sentinel});}catch(e){if(e.message==='fixture credential leaked')return;}throw Error('fixture history guard failed');}};
}
@@ -0,0 +1,29 @@
// #653 STATIC test-first preparation. No provider/proxy service or manual enable marker.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { sandboxProblem } from '../../seat/src/sandbox.mjs';
import { startTimeOf } from '../../seat/src/proc.mjs';
import { managedLaunchFixture, pause, waitFor } from './model-managed-launch-fixture.mjs';
const skip=sandboxProblem()??false; // Any unavailable confinement is UNVERIFIED, not a green result.
async function launched(t,options){const f=await managedLaunchFixture(t,options);let answer;await assert.doesNotReject(async()=>{try{answer=await f.launcher.launchPm();}finally{if(options?.proxy!==false)assert.equal(f.trace.successfulPreflights,1,'own real model preflight succeeded before launch outcome');}},'actual managed proxy launch succeeds after successful preflight');const s=f.sessions().find(s=>s.run===answer.run);assert.ok(s?.live,'real confined runner is registered and live');return {f,s};}
test('653 actual managed proxy launch stays idle unarmed then stops first silent turn through trusted runtime',{skip,timeout:30000},async t=>{
const {f,s}=await launched(t);assert.equal(f.trace.successfulPreflights,1);assert.deepEqual(f.trace.enableBeforeSpawn,[true]);assert.equal(f.trace.bindings[0].modelKeyId,f.expectedKeyId());assert.equal(s.modelKeyId,f.expectedKeyId());assert.notEqual(s.pid,s.runnerPid);
await pause(1250);assert.equal(f.trace.stalls.length,0,'idle beyond configured initial budget does not stop');assert.equal(fs.existsSync(join(s.runDir,'proxy-first-turn')),false);assert.equal(startTimeOf(s.runnerPid),s.runnerStartTime);
const before=process.hrtime.bigint();f.send('SILENT');await waitFor(()=>fs.existsSync(join(s.runDir,'proxy-first-turn')),'actual managed adapter turn publishes marker');const stamp=BigInt(fs.readFileSync(join(s.runDir,'proxy-first-turn'),'utf8').trim().split(' ')[1]);assert.ok(stamp>=before&&stamp<=process.hrtime.bigint());assert.equal(fs.existsSync(join(s.runDir,'proxy-first-output')),false);
await waitFor(()=>f.trace.stalls.length===1,'actual caller watcher reaches trusted modelStall');assert.deepEqual(f.trace.stalls,[{run:s.run,keyId:s.modelKeyId,stopped:true}]);await waitFor(()=>f.sessions().length===0,'normal managed exit unregisters session');assert.equal(f.launchLog().some(r=>r.action==='proxy-stall'&&r.keyIds?.includes(s.modelKeyId)&&!r.code),true);f.assertHistoryGuard();f.valueFree();
});
test('653 actual managed proxy output cancels caller watcher and first timestamp never resets',{skip,timeout:30000},async t=>{
const {f,s}=await launched(t);const first=f.send('OUTPUT');const answer=await f.reply(first);assert.equal(answer.body,'fx653-private-output');const file=join(s.runDir,'proxy-first-turn'),stamp=fs.readFileSync(file,'utf8');assert.equal(fs.statSync(file).mode&0o777,0o600);assert.equal(fs.readFileSync(join(s.runDir,'proxy-first-output'),'utf8'),'output\n');
const second=f.send('OUTPUT');await f.reply(second);assert.equal(fs.readFileSync(file,'utf8'),stamp);await pause(1250);assert.equal(f.trace.stalls.length,0,'actual first output cancels managed watcher');assert.equal(f.sessions()[0].live,true);assert.equal(fs.readdirSync(s.runDir).some(n=>n.startsWith('.proxy-first-turn-')),false);f.valueFree();
});
test('653 actual nonproxy launch writes output without proxy enable or first-turn protocol',{skip,timeout:30000},async t=>{
const {f,s}=await launched(t,{proxy:false});assert.equal(f.trace.preflightAttempts,0);assert.equal(fs.existsSync(join(s.runDir,'proxy-first-turn-enabled')),false);const sent=f.send('OUTPUT');assert.equal((await f.reply(sent)).body,'fx653-private-output');assert.equal(fs.existsSync(join(s.runDir,'proxy-first-turn')),false);assert.equal(f.trace.stalls.length,0);f.valueFree();
});
test('653 actual refused proxy preflight never enables binds or spawns a managed session',{skip,timeout:30000},async t=>{
const f=await managedLaunchFixture(t,{refuseProbe:true});await assert.rejects(f.launcher.launchPm(),e=>e.code==='proxy-key-refused');assert.equal(f.trace.successfulPreflights,0);assert.equal(f.trace.preflightAttempts,1);assert.equal(f.trace.bindings.length,0);assert.equal(f.sessions().length,0);assert.equal(f.trace.enableBeforeSpawn.length,0);assert.equal(f.runDirectories().some(dir=>fs.existsSync(join(dir,'proxy-first-turn-enabled'))||fs.existsSync(join(dir,'session.json'))),false,'refused preflight has no enable/session publication');f.valueFree();
});
test('653 actual no-launch host refuses before proxy preflight and session side effects',{skip,timeout:30000},async t=>{
const f=await managedLaunchFixture(t,{launches:false});await assert.rejects(f.launcher.launchPm(),e=>e.code==='launches-disabled');assert.equal(f.trace.preflightAttempts,0);assert.equal(f.trace.bindings.length,0);assert.equal(f.sessions().length,0);assert.equal(f.runDirectories().length,0,'no-launch never allocates a run');f.valueFree();
});
@@ -0,0 +1,52 @@
// P2 RED tests: these catch fetch before eligibility and divergent key predicates.
import test from 'node:test';
import assert from 'node:assert/strict';
import { proxyCommand } from '../src/model-proxy.mjs';
import { io } from './helpers.mjs';
for (const env of [{ CODEX_THREAD_ID: 'fixture' }, { MOSAIC_AGENT_RUN: 'fixture' }]) {
test(`human-required: proxy fetch refuses ${Object.keys(env)[0]} before config or fetch`, async () => {
let touched = false;
const forbidden = () => { touched = true; throw new Error('eligibility must run first'); };
await assert.rejects(proxyCommand(['fetch'], io(env), { system: forbidden, fetch: forbidden }), e => e.code === 'human-required');
assert.equal(touched, false);
});
}
test('human-required: proxy fetch tester refuses before config or fetch', async () => {
let touched = false;
const forbidden = () => { touched = true; throw new Error('eligibility must run first'); };
await assert.rejects(proxyCommand(['fetch', '--tester'], io(), { system: forbidden, fetch: forbidden }), e => e.code === 'human-required');
assert.equal(touched, false);
});
for (const key of ['fx331-caf\u00e9', ' fx331-key', 'fx331-key ', 'fx331 key']) {
test(`proxy-key-invalid: CLI rejects non-management-key input (${JSON.stringify(key).length})`, async () => {
const x = io(); x.stdin.isTTY = true; x.stdout.isTTY = true;
let calls = 0;
await assert.rejects(proxyCommand(['init'], x, {
system: { dataRoot: '/unused-fixture' }, prompt: async () => key,
callFor: () => async () => { calls++; return { port: 18432, binary: '/fixture' }; },
}), e => e.code === 'proxy-key-invalid');
assert.equal(calls, 0);
assert.equal(x.out.text.includes(key), false);
assert.equal(x.err.text.includes(key), false);
});
}
for (const [flag, code, value] of [['--rotate-grace-minutes', 'proxy-rotate-grace-invalid', '1441'], ['--first-output-seconds', 'proxy-first-output-invalid', '3601']]) {
test(`${code}: init flag refuses before config or prompt`, async () => {
let touched = false;
const forbidden = () => { touched = true; throw new Error('must not run'); };
await assert.rejects(proxyCommand(['init', flag, value], io(), { system: forbidden, prompt: forbidden }), e => e.code === code);
assert.equal(touched, false);
});
}
test('human proxy init passes optional timing flags only through authenticated call', async () => {
const x = io(); x.stdin.isTTY = true; x.stdout.isTTY = true;
const calls = [];
await proxyCommand(['init', '--rotate-grace-minutes', '20', '--first-output-seconds', '10'], x, {
system: { dataRoot: '/unused-fixture' }, prompt: async () => 'fx331-manage',
callFor: () => async (verb, args) => { calls.push({ verb, args }); return { port: 18432, binary: '/fixture' }; },
});
assert.equal(calls.length, 1); assert.equal(calls[0].args.rotateGraceMinutes, 20); assert.equal(calls[0].args.firstOutputSeconds, 10);
assert.equal(x.out.text.includes('fx331-manage'), false);
});
+30
View File
@@ -0,0 +1,30 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { watchModelOutput } from '../src/model-stall.mjs';
function fixture(t) { const root = fs.mkdtempSync(join(tmpdir(), 'p2-stall-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); return root; }
test('proxy-stall: silent run stops and logs only ids, never output or key values', async t => {
const root = fixture(t), receipts = []; let stops = 0;
const watch = watchModelOutput({ runDir: root, seconds: 0.02, keyId: '0123456789ab', stop: async () => { stops++; }, receipt: r => receipts.push(r) });
t.after(watch.cancel); await watch.done;
assert.equal(stops, 1); assert.deepEqual(receipts, [{ action: 'proxy-stall', keyIds: ['0123456789ab'] }]);
});
test('first stdout marker cancels stall without reading or logging marker text', async t => {
const root = fixture(t), receipts = []; let stops = 0;
fs.writeFileSync(join(root, 'proxy-first-output'), 'fx331-private-output');
const watch = watchModelOutput({ runDir: root, seconds: 0.02, keyId: '0123456789ab', stop: async () => { stops++; }, receipt: r => receipts.push(r) });
t.after(watch.cancel); await watch.done; assert.equal(stops, 0); assert.deepEqual(receipts, []);
});
test('proxy-stop-failed: failed stall stop is logged by code and never leaks error text', async t => {
const root = fixture(t), receipts = [];
const watch = watchModelOutput({ runDir: root, seconds: 0.02, keyId: '0123456789ab', stop: async () => { throw Error('fx331-private-output'); }, receipt: r => receipts.push(r) });
t.after(watch.cancel); await watch.done;
assert.deepEqual(receipts, [{ action: 'proxy-stall', code: 'proxy-stop-failed', keyIds: ['0123456789ab'] }]);
});
test('run exit cancellation settles watcher without stopping the run', async t => {
const root = fixture(t); let stops = 0;
const watch = watchModelOutput({ runDir: root, seconds: 0.02, keyId: '0123456789ab', stop: async () => { stops++; }, receipt: () => {} });
watch.cancel(); await watch.done; assert.equal(stops, 0);
});
@@ -0,0 +1,15 @@
// #650 first actual turn, one initial budget; prepared only, not executed.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { watchModelOutput } from '../src/model-stall.mjs';
const delay=ms=>new Promise(r=>setTimeout(r,ms));
async function doneWithin(watch){let timer;try{await Promise.race([watch.done,new Promise((_,reject)=>{timer=setTimeout(()=>reject(Error('watcher completion not observed')),5000);})]);}finally{clearTimeout(timer);}}
function fixture(t){const runDir=fs.mkdtempSync(join(tmpdir(),'clock650-'));let stops=0;const receipts=[];t.after(()=>fs.rmSync(runDir,{recursive:true,force:true}));const watch=watchModelOutput({runDir,armAtFirstTurn:true,seconds:0.1,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:r=>receipts.push(r)});t.after(watch.cancel);return {runDir,watch,receipts,stops:()=>stops,arm:()=>fs.writeFileSync(join(runDir,'proxy-first-turn'),`turn ${process.hrtime.bigint()}\n`,{mode:0o600})};}
test('650 idle longer than initial budget remains unarmed',async t=>{const f=fixture(t);await delay(250);assert.equal(f.stops(),0,'idle time must not spend first-output budget');f.watch.cancel();await f.watch.done;assert.deepEqual(f.receipts,[]);});
test('650 first silent turn spends one initial budget then invokes existing stop callback',async t=>{const f=fixture(t);await delay(250);assert.equal(f.stops(),0,'silent turn clock is unarmed before first turn');f.arm();await doneWithin(f.watch);assert.equal(f.stops(),1);assert.deepEqual(f.receipts,[{action:'proxy-stall',keyIds:['0123456789ab']}]);});
test('650 first output cancels turn watcher without output text in receipt',async t=>{const f=fixture(t);f.arm();fs.writeFileSync(join(f.runDir,'proxy-first-output'),'output\n',{mode:0o600});await doneWithin(f.watch);assert.equal(f.stops(),0);assert.deepEqual(f.receipts,[]);});
test('650 later marker cannot extend already armed initial budget',async t=>{const f=fixture(t);f.arm();await delay(30);fs.writeFileSync(join(f.runDir,'proxy-first-turn'),`turn ${process.hrtime.bigint()+60_000_000_000n}\n`);await doneWithin(f.watch);assert.equal(f.stops(),1);});
test('650 idle run exit cancellation stops no process',async t=>{const f=fixture(t);f.watch.cancel();await f.watch.done;assert.equal(f.stops(),0);assert.deepEqual(f.receipts,[]);});
@@ -0,0 +1,39 @@
// #650 strict reader negatives, real owned files only; prepared, not executed.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { spawn, execFileSync } from 'node:child_process';
import { syncBuiltinESMExports } from 'node:module';
import { watchModelOutput } from '../src/model-stall.mjs';
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
function scratch(t){const root=fs.mkdtempSync(join(tmpdir(),'reader650-'));t.after(()=>fs.rmSync(root,{recursive:true,force:true}));return root;}
const canonical=()=>Buffer.from(`turn ${process.hrtime.bigint()}\n`);
async function invalid(t,prepare){const root=scratch(t),file=join(root,'proxy-first-turn');prepare(file,root);let stops=0;const receipts=[];const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:60,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:r=>receipts.push(r)});t.after(watch.cancel);await delay(30);assert.equal(stops,1,'invalid first-turn metadata triggers single trusted stop');await delay(30);assert.equal(stops,1);assert.deepEqual(receipts,[{action:'proxy-stall',keyIds:['0123456789ab']}]);const targets=fs.readdirSync('/proc/self/fd').flatMap(n=>{try{return [fs.readlinkSync('/proc/self/fd/'+n)];}catch{return [];}});assert.equal(targets.includes(file),false,'invalid marker descriptor closed');return {root,file};}
const malformed=[
['oversized',()=>Buffer.alloc(65,120)],['empty',()=>Buffer.alloc(0)],['wrong prefix',()=>Buffer.from(`turns ${process.hrtime.bigint()}\n`)],
['zero',()=>Buffer.from('turn 0\n')],['future',()=>Buffer.from(`turn ${process.hrtime.bigint()+60_000_000_000n}\n`)],['leading zero',()=>Buffer.from(`turn 0${process.hrtime.bigint()}\n`)],
['extra trailing newline',()=>Buffer.concat([canonical(),Buffer.from('\n')])],['extra token',()=>Buffer.from(`turn ${process.hrtime.bigint()} extra\n`)],
['CRLF',()=>Buffer.from(`turn ${process.hrtime.bigint()}\r\n`)],['malformed digit',()=>Buffer.from('turn abc\n')],
...[['highbit prefix',0],['highbit digit',5],['highbit newline',-1]].map(([name,index])=>[name,()=>{const b=canonical();b[index<0?b.length-1:index]|=128;return b;}]),
];
for(const [name,bytes] of malformed)test(`650 reader rejects ${name} without exposing metadata`,async t=>{await invalid(t,file=>fs.writeFileSync(file,bytes(),{mode:0o600}));});
test('650 reader rejects wrong mode',async t=>{await invalid(t,file=>fs.writeFileSync(file,canonical(),{mode:0o644}));});
test('650 reader never follows owned symlink to a valid long-budget timestamp',async t=>{await invalid(t,(file,root)=>{const target=join(root,'owned-target');fs.writeFileSync(target,canonical(),{mode:0o600});fs.symlinkSync(target,file);});});
test('650 reader FIFO cannot block the watcher process',async t=>{
const root=scratch(t);execFileSync('mkfifo',['-m','600',join(root,'proxy-first-turn')]);
const probe=new URL('./fixtures/model-clock-probe.mjs',import.meta.url).pathname;
const child=spawn(process.execPath,[probe,root],{env:{PATH:process.env.PATH,HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,MOSAIC_CONFIG:process.env.MOSAIC_CONFIG},stdio:['ignore','pipe','ignore']});
let output='',timedOut=false;child.stdout.on('data',b=>{output+=b;});const closed=new Promise((resolve,reject)=>{child.once('error',reject);child.once('close',(code,signal)=>resolve({code,signal}));});const timer=setTimeout(()=>{timedOut=true;child.kill('SIGKILL');},5000);t.after(async()=>{clearTimeout(timer);if(child.exitCode===null&&child.signalCode===null)child.kill('SIGKILL');await closed;});
const result=await closed;clearTimeout(timer);assert.equal(timedOut,false,'FIFO probe completes without blocking');assert.equal(result.code,0);const answer=JSON.parse(output);assert.equal(answer.stops,1,'FIFO metadata is refused through existing stop');assert.equal(answer.nice,19);assert.equal(answer.openMarkerFds,0,'FIFO descriptor closed before child exit');assert.deepEqual(answer.receipts,[{action:'proxy-stall',keyIds:['0123456789ab']}]);
});
test('650 reader late detection uses already-spent embedded budget',async t=>{const root=scratch(t);fs.writeFileSync(join(root,'proxy-first-turn'),`turn ${process.hrtime.bigint()-120_000_000_000n}\n`,{mode:0o600});let stops=0;const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:60,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:()=>{}});t.after(watch.cancel);await delay(30);assert.equal(stops,1,'embedded initial timestamp already spent the budget');});
test('650 reader closes owned marker descriptors and never rereads armed timestamp',async t=>{
const root=scratch(t),file=join(root,'proxy-first-turn');fs.writeFileSync(file,canonical(),{mode:0o600});const original=fs.openSync;let reads=0;fs.openSync=function(p,flags,...args){if(p===file&&typeof flags==='number')reads++;return original.call(this,p,flags,...args);};syncBuiltinESMExports();t.after(()=>{fs.openSync=original;syncBuiltinESMExports();});
const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:0.1,keyId:'0123456789ab',stop:async()=>{},receipt:()=>{}});t.after(watch.cancel);await delay(30);assert.equal(reads,1,'first marker read observed');fs.writeFileSync(file,canonical());await watch.done;assert.equal(reads,1,'armed timestamp never reread');
const targets=fs.readdirSync('/proc/self/fd').flatMap(n=>{try{return [fs.readlinkSync('/proc/self/fd/'+n)];}catch{return [];}});assert.equal(targets.includes(file),false,'all owned marker descriptors closed');
});
// Wrong-owner and independently unreadable valid-0600 fixtures require changing
// another UID/ACL. Not forged here; those branches remain explicitly UNVERIFIED.
test('650 first output cancels before malformed metadata can request a stop',async t=>{const root=scratch(t);fs.writeFileSync(join(root,'proxy-first-turn'),'not-a-timestamp',{mode:0o644});fs.writeFileSync(join(root,'proxy-first-output'),'output\n',{mode:0o600});let stops=0;const receipts=[];const watch=watchModelOutput({runDir:root,armAtFirstTurn:true,seconds:60,keyId:'0123456789ab',stop:async()=>{stops++;},receipt:r=>receipts.push(r)});t.after(watch.cancel);await watch.done;assert.equal(stops,0);assert.deepEqual(receipts,[]);});
@@ -86,6 +86,7 @@ async function start(t, { f, adapter }, { tickMs = 100, stopTimeoutMs } = {}) {
authorizeLaunch: (m) => runtime.authorizeLaunch(m.cap, m.instance), authorizeLaunch: (m) => runtime.authorizeLaunch(m.cap, m.instance),
refuse: (m) => runtime.refuse(m.cap, m.code, m.subject), refuse: (m) => runtime.refuse(m.cap, m.code, m.subject),
endLaunch: (m) => runtime.endLaunch(m.record), endLaunch: (m) => runtime.endLaunch(m.record),
recoverLaunch: (m) => runtime.recoverLaunch(m.record),
credentialStatus: (m) => runtime.credentialStatus(m.business, m.role), credentialStatus: (m) => runtime.credentialStatus(m.business, m.role),
threadsState: (m) => runtime.threadsState(m.business), threadsState: (m) => runtime.threadsState(m.business),
threadIdle: (m) => runtime.threadIdle(m.record), threadIdle: (m) => runtime.threadIdle(m.record),
@@ -91,6 +91,7 @@ async function start(t, { f, adapter, access }) {
authorizeLaunch: (m) => runtime.authorizeLaunch(m.cap, m.instance), authorizeLaunch: (m) => runtime.authorizeLaunch(m.cap, m.instance),
refuse: (m) => runtime.refuse(m.cap, m.code, m.subject), refuse: (m) => runtime.refuse(m.cap, m.code, m.subject),
endLaunch: (m) => runtime.endLaunch(m.record), endLaunch: (m) => runtime.endLaunch(m.record),
recoverLaunch: (m) => runtime.recoverLaunch(m.record),
credentialStatus: (m) => runtime.credentialStatus(m.business, m.role), credentialStatus: (m) => runtime.credentialStatus(m.business, m.role),
threadsState: (m) => runtime.threadsState(m.business), threadsState: (m) => runtime.threadsState(m.business),
threadIdle: (m) => runtime.threadIdle(m.record), threadIdle: (m) => runtime.threadIdle(m.record),
+32
View File
@@ -0,0 +1,32 @@
import { writeFileSync, openSync, closeSync, linkSync, unlinkSync } from 'node:fs';
import { join } from 'node:path';
// The host needs an output-exists signal, never the output or a model credential.
export function firstOutputMarker(runDir) {
let seen = false;
return chunk => {
if (seen || !chunk.length) return;
try { writeFileSync(join(runDir, 'proxy-first-output'), 'output\n', { flag: 'wx', mode: 0o600 }); }
catch (error) { if (error.code !== 'EEXIST') throw error; }
seen = true;
};
}
// Publish only a fixed time record inside the existing writable run directory.
export function firstTurnMarker(runDir) {
return () => {
const stamp = process.hrtime.bigint();
const temporary = join(runDir, `.proxy-first-turn-${process.pid}-${stamp}`);
let fd, owned = false, failed = false;
try {
fd = openSync(temporary, 'wx', 0o600); owned = true;
writeFileSync(fd, `turn ${stamp}\n`);
try { linkSync(temporary, join(runDir, 'proxy-first-turn')); }
catch (error) { if (error.code !== 'EEXIST') throw error; }
} catch { failed = true; }
finally {
if (fd !== undefined) { try { closeSync(fd); } catch { failed = true; } }
if (owned) { try { unlinkSync(temporary); } catch { failed = true; } }
}
if (failed) throw new Error('first-turn marker publication failed');
};
}
+30 -4
View File
@@ -38,6 +38,7 @@ import { createServer, connect } from "node:net";
import { join } from "node:path"; import { join } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { Client } from "../../bus/src/client.mjs"; import { Client } from "../../bus/src/client.mjs";
import { firstOutputMarker, firstTurnMarker } from './first-output.mjs';
import { GUARD_CODES, FIXED_TEXTS } from "../../library/src/reply-protocol.mjs"; import { GUARD_CODES, FIXED_TEXTS } from "../../library/src/reply-protocol.mjs";
import { fetchReview, landWork, pushWorking } from "./git-tools.mjs"; import { fetchReview, landWork, pushWorking } from "./git-tools.mjs";
@@ -344,12 +345,14 @@ function runTurn({ session, request, n, env, onChild }) {
closeSync(errFd); closeSync(errFd);
onChild(child); onChild(child);
let out = ""; let out = "";
const markOutput = firstOutputMarker(session.runDir);
child.stdout.setEncoding("utf8"); child.stdout.setEncoding("utf8");
child.stdout.on("data", (s) => { child.stdout.on("data", (s) => {
markOutput(s);
if (out.length < 4 * BODY_MAX) out += s; if (out.length < 4 * BODY_MAX) out += s;
}); });
return new Promise((resolve) => { return new Promise((resolve) => {
let timedOut = false; let timedOut = false, markerFailed = false, markerKill;
const kill = (signal) => { const kill = (signal) => {
try { try {
process.kill(-child.pid, signal); process.kill(-child.pid, signal);
@@ -360,15 +363,28 @@ function runTurn({ session, request, n, env, onChild }) {
kill("SIGTERM"); kill("SIGTERM");
setTimeout(() => kill("SIGKILL"), 5000).unref(); setTimeout(() => kill("SIGKILL"), 5000).unref();
}, session.turnTimeout * 1000); }, session.turnTimeout * 1000);
if (session.proxyFirstTurn) child.once("spawn", () => {
try { firstTurnMarker(session.runDir)(); }
catch {
markerFailed = true;
clearTimeout(clock);
kill("SIGTERM");
markerKill = setTimeout(() => kill("SIGKILL"), 5000).unref();
}
});
child.on("error", (e) => { child.on("error", (e) => {
if (markerFailed) return; // wait for close/reap, never resolve early
clearTimeout(clock); clearTimeout(clock);
onChild(null); onChild(null);
resolve({ ok: false, reason: `adapter didn't start: ${e.code ?? e.message}` }); resolve({ ok: false, reason: `adapter didn't start: ${e.code ?? e.message}` });
}); });
child.on("close", (code, signal) => { child.on("close", (code, signal) => {
clearTimeout(clock); clearTimeout(clock);
clearTimeout(markerKill);
kill("SIGKILL"); kill("SIGKILL");
onChild(null); onChild(null);
if (markerFailed) return resolve({ ok: false, markerFailed: true, reason: "first-turn marker publication failed" });
if (timedOut) return resolve({ ok: false, reason: `turn passed its ${session.turnTimeout} s wall clock` }); if (timedOut) return resolve({ ok: false, reason: `turn passed its ${session.turnTimeout} s wall clock` });
if (code !== 0) return resolve({ ok: false, reason: signal ? `adapter killed by ${signal}` : `adapter exited ${code}` }); if (code !== 0) return resolve({ ok: false, reason: signal ? `adapter killed by ${signal}` : `adapter exited ${code}` });
// A "marker" harness (row 65: the module's turnEnd()) writes the marker when its turn ends. // A "marker" harness (row 65: the module's turnEnd()) writes the marker when its turn ends.
@@ -431,7 +447,8 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
let session, cap, policy; let session, cap, policy;
try { try {
session = { ...DEFAULTS, ...JSON.parse(readFileSync(join(runDir, "session.json"), "utf8")), runDir }; session = { ...DEFAULTS, ...JSON.parse(readFileSync(join(runDir, "session.json"), "utf8")), runDir,
proxyFirstTurn: existsSync(join(runDir, "proxy-first-turn-enabled")) };
cap = JSON.parse(await readLine(stdin)).cap; cap = JSON.parse(await readLine(stdin)).cap;
if (typeof cap !== "string" || !/^[0-9a-f]{64}$/.test(cap)) throw new Error("no capability on stdin"); if (typeof cap !== "string" || !/^[0-9a-f]{64}$/.test(cap)) throw new Error("no capability on stdin");
policy = JSON.parse(readFileSync(session.bundle.policy, "utf8")); policy = JSON.parse(readFileSync(session.bundle.policy, "utf8"));
@@ -516,7 +533,13 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
// Row 156 (#1651): the landing turns, before any message. Each reads the state as a message turn // Row 156 (#1651): the landing turns, before any message. Each reads the state as a message turn
// does and is bracketed by usage with the decision id as its turn; it sends nothing and marks // does and is bracketed by usage with the decision id as its turn; it sends nothing and marks
// nothing read. The turn counter is shared with message turns. // nothing read. The turn counter is shared with message turns.
let gone = false; let gone = false, markerFailure = false;
const failedMarker = turn => {
if (!turn.markerFailed) return false;
markerFailure = true; code = EXIT.usage;
log("runner: first-turn marker publication failed");
return true;
};
for (const e of session.landing ?? []) { for (const e of session.landing ?? []) {
if (stopping) break; if (stopping) break;
let block; let block;
@@ -538,6 +561,7 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
unslop.read(); unslop.read();
try { await client.call("usage.end", { turn: e.decision }); } try { await client.call("usage.end", { turn: e.decision }); }
catch (err) { log(`runner: usage outcome unavailable: ${err.code ?? "failed"}`); } catch (err) { log(`runner: usage outcome unavailable: ${err.code ?? "failed"}`); }
if (failedMarker(turn)) { gone = true; break; }
log(`runner: landing turn ${n} for decision ${e.decision} (${e.task_ref}): ${turn.ok ? "ok" : `failed: ${turn.reason}`}`); log(`runner: landing turn ${n} for decision ${e.decision} (${e.task_ref}): ${turn.ok ? "ok" : `failed: ${turn.reason}`}`);
} }
loop: while (!stopping && !gone) { loop: while (!stopping && !gone) {
@@ -580,6 +604,7 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
// A failed/stopped harness can still have used tokens. Missing data remains unknown. // A failed/stopped harness can still have used tokens. Missing data remains unknown.
try { await client.call("usage.end", { turn: m.id }); } try { await client.call("usage.end", { turn: m.id }); }
catch (e) { log(`runner: usage outcome unavailable: ${e.code ?? "failed"}`); } catch (e) { log(`runner: usage outcome unavailable: ${e.code ?? "failed"}`); }
if (failedMarker(turn)) break loop;
if (stopping) break loop; if (stopping) break loop;
if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`); if (!turn.ok) log(`runner: turn ${n} for message ${m.id} failed: ${turn.reason}`);
try { try {
@@ -628,6 +653,7 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
unslop.read(); unslop.read();
try { await client.call("usage.end", { turn: `${m.id}:retry` }); } try { await client.call("usage.end", { turn: `${m.id}:retry` }); }
catch (e) { log(`runner: usage outcome unavailable: ${e.code ?? "failed"}`); } catch (e) { log(`runner: usage outcome unavailable: ${e.code ?? "failed"}`); }
if (failedMarker(retry)) break loop;
if (stopping) break loop; if (stopping) break loop;
if (!retry.ok) log(`runner: retry turn ${n} for message ${m.id} failed: ${retry.reason}`); if (!retry.ok) log(`runner: retry turn ${n} for message ${m.id} failed: ${retry.reason}`);
try { await send(retry); } try { await send(retry); }
@@ -650,7 +676,7 @@ export async function main(runDir, { stdin = process.stdin, env = process.env, l
} }
server.close(); server.close();
rmSync(session.toolSocket, { force: true }); rmSync(session.toolSocket, { force: true });
if (code === EXIT.stopped && !released) { if ((code === EXIT.stopped || markerFailure) && !released) {
try { try {
await client.call("role.release"); await client.call("role.release");
} catch (e) { } catch (e) {
@@ -0,0 +1,16 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { firstOutputMarker } from '../src/first-output.mjs';
test('stdout marker contains a fixed token only, never output text; emitted once after nonempty output', t => {
const root = fs.mkdtempSync(join(tmpdir(), 'p2-output-')); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const observe = firstOutputMarker(root), file = join(root, 'proxy-first-output');
observe(''); assert.equal(fs.existsSync(file), false);
observe('fx331-private-output');
assert.equal(fs.readFileSync(file, 'utf8'), 'output\n'); assert.equal(fs.statSync(file).mode & 0o777, 0o600);
const before = fs.statSync(file).mtimeMs;
observe('other output'); assert.equal(fs.statSync(file).mtimeMs, before);
assert.equal(fs.readFileSync(file, 'utf8').includes('fx331-private-output'), false);
});
@@ -0,0 +1,38 @@
// #650 timestamp producer's one-write invariant; prepared only.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { syncBuiltinESMExports } from 'node:module';
import * as markers from '../src/first-output.mjs';
function root(t){const p=fs.mkdtempSync(join(tmpdir(),'turn650-'));t.after(()=>fs.rmSync(p,{recursive:true,force:true}));return p;}
test('650 first-turn producer publishes time metadata only and keeps output content separate',t=>{const p=root(t);assert.equal(typeof markers.firstTurnMarker,'function','first-turn protocol producer exists');const before=process.hrtime.bigint();markers.firstTurnMarker(p)();const value=fs.readFileSync(join(p,'proxy-first-turn'),'utf8');assert.match(value,/^turn [0-9]+\n$/);const n=BigInt(value.trim().split(' ')[1]);assert.ok(n>=before&&n<=process.hrtime.bigint());assert.equal(fs.statSync(join(p,'proxy-first-turn')).mode&0o777,0o600);assert.deepEqual(fs.readdirSync(p),['proxy-first-turn']);markers.firstOutputMarker(p)('fx650-private-output');assert.equal(fs.readFileSync(join(p,'proxy-first-output'),'utf8'),'output\n');assert.equal(value.includes('fx650-private-output'),false);});
test('650 subsequent actual turn cannot reset or extend first timestamp',t=>{const p=root(t);assert.equal(typeof markers.firstTurnMarker,'function','first-turn one-shot producer exists');markers.firstTurnMarker(p)();const first=fs.readFileSync(join(p,'proxy-first-turn')),stat=fs.statSync(join(p,'proxy-first-turn'));markers.firstTurnMarker(p)();assert.equal(fs.readFileSync(join(p,'proxy-first-turn')).equals(first),true);assert.equal(fs.statSync(join(p,'proxy-first-turn')).mtimeMs,stat.mtimeMs);assert.deepEqual(fs.readdirSync(p),['proxy-first-turn']);});
// Narrow test-only filesystem interception, only this fixture's private prefix.
// Product has no injectable seam; every other file operation passes through.
for(const fault of ['partial write','exclusive collision','link','close','unlink'])test(`650 producer ${fault} failure keeps ownership and fixed diagnostic`,t=>{
const p=fs.mkdtempSync(join(tmpdir(),'turn650-fault-'));let restore=()=>{};t.after(()=>{restore();fs.rmSync(p,{recursive:true,force:true});});assert.equal(typeof markers.firstTurnMarker,'function','guarded first-turn producer exists');
const original=Object.fromEntries(['openSync','writeFileSync','linkSync','closeSync','unlinkSync'].map(k=>[k,fs[k]]));let fd,temporary,closeCalls=0,unlinkCalls=0,collisionCreated=false;
const own=value=>typeof value==='string'&&value.startsWith(join(p,'.proxy-first-turn-'));
restore=()=>{for(const [k,v] of Object.entries(original))fs[k]=v;syncBuiltinESMExports();};
fs.openSync=function(file,...args){
if(!own(file))return original.openSync.call(this,file,...args);
temporary=file;
if(fault==='exclusive collision'){
const collisionFd=original.openSync.call(this,file,'wx',0o600);
try{original.writeFileSync.call(this,collisionFd,'owned-collision');}
finally{original.closeSync.call(this,collisionFd);}
collisionCreated=true;
}
fd=original.openSync.call(this,file,...args);return fd;
};
fs.writeFileSync=function(file,data,...args){if(file===fd&&fault==='partial write'){original.writeFileSync(file,'t');throw Error('fx650-write-failed');}return original.writeFileSync.call(this,file,data,...args);};
fs.linkSync=function(from,...args){if(own(from)&&fault==='link')throw Error('fx650-link-failed');return original.linkSync.call(this,from,...args);};
fs.closeSync=function(value){if(value===fd){closeCalls++;const result=original.closeSync.call(this,value);if(fault==='close')throw Error('fx650-close-failed');return result;}return original.closeSync.call(this,value);};
fs.unlinkSync=function(file){if(own(file)){unlinkCalls++;if(fault==='unlink')throw Error('fx650-unlink-failed');}return original.unlinkSync.call(this,file);};syncBuiltinESMExports();
assert.throws(()=>markers.firstTurnMarker(p)(),error=>error.message==='first-turn marker publication failed','publication errors expose fixed diagnostic only');
if(fault==='exclusive collision'){assert.equal(collisionCreated,true,'collision fixture created before product open');assert.equal(unlinkCalls,0,'collision does not confer temp ownership');assert.equal(fs.existsSync(temporary),true,'pre-existing collision not removed');assert.equal(closeCalls,0);}
else {assert.equal(closeCalls,1,'owned descriptor close always attempted');assert.equal(unlinkCalls,1,'owned temporary unlink always attempted');if(fault!=='unlink')assert.equal(fs.existsSync(temporary),false);}
});
@@ -0,0 +1,41 @@
// #650 controlled producer failure through REAL runner process and real broker.
// Unconfined fault-injection tests; direct confined production marker proof is separate.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { join } from 'node:path';
import { spawn } from 'node:child_process';
import { citationWorld } from '../../bus/tests/citation-helpers.mjs';
import { serve } from '../../bus/src/server.mjs';
const RUNNER=new URL('../src/runner.mjs',import.meta.url).pathname;
const ADAPTER=new URL('./fixtures/citation-adapter.mjs',import.meta.url).pathname;
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function setup(t,mode){
const closers=[];let child,closed,server;
t.after(async()=>{const errors=[];if(child){try{if(child.exitCode===null&&child.signalCode===null)child.kill('SIGTERM');let timer;try{await Promise.race([closed,new Promise(resolve=>{timer=setTimeout(()=>{child.kill('SIGKILL');resolve();},5000);})]);await closed;}finally{clearTimeout(timer);}}catch{errors.push('own-runner-close');}}try{await server?.close();}catch{errors.push('server-close');}for(const close of [...closers].reverse()){try{await close();}catch{errors.push('owned-world-close');}}if(errors.length)throw new AggregateError(errors.map(code=>Error(code)),'owned producer fixture cleanup failed');});
const f=citationWorld({after:close=>closers.push(close)},{library:mode==='retry'}),runDir=join(f.root,'run'),bundle=join(runDir,'bundle');fs.mkdirSync(bundle,{recursive:true,mode:0o700});fs.mkdirSync(join(f.root,'ws'));
fs.writeFileSync(f.settings,JSON.stringify({replyLimits:{requireCitation:true}}));const brokerSocket=join(f.root,'broker.sock');server=await serve({broker:f.broker,path:brokerSocket});
fs.writeFileSync(join(runDir,'fixture-steps.json'),JSON.stringify([{text:'uncited'},{text:'still uncited'},{text:'unreachable'}]));
const adapter=join(runDir,'adapter.sh');fs.writeFileSync(adapter,`exec '${process.execPath}' '${ADAPTER}'\n`);fs.writeFileSync(join(bundle,'prompt.md'),'# fixture\n');fs.writeFileSync(join(bundle,'tools.json'),'[]');fs.writeFileSync(join(bundle,'policy.json'),JSON.stringify({needs:[],credentials:[]}));
fs.writeFileSync(join(runDir,'proxy-first-turn-enabled'),'enabled\n',{mode:0o600});
fs.writeFileSync(join(runDir,'session.json'),JSON.stringify({business:'acme',instance:'coder',run:'coder-run',harness:'pi',provider:'fixture',model:'fixture',brokerSocket,workspace:join(f.root,'ws'),sessionDir:join(runDir,'session'),adapter,bundle:{prompt:join(bundle,'prompt.md'),tools:join(bundle,'tools.json'),policy:join(bundle,'policy.json')},toolSocket:join(runDir,'tools.sock'),turnMarker:join(runDir,'turn.done'),turnEnd:'marker',harnessEnv:{},pollInterval:20,turnTimeout:10,...(mode==='landing'?{landing:[{decision:'d1',task_ref:'vikunja:3/7'},{decision:'d2',task_ref:'vikunja:3/8'}]}:{})}));
const trace=join(f.root,'owned-spawn-trace.json'),attempt=join(f.root,'owned-marker-attempts'),preload=join(f.root,'fault-preload.mjs'),targetAttempt=mode==='retry'?2:1;
// Test-only preload: all actual spawn/kill/fs operations pass through. Intercept
// only own adapter spawn, owned returned PID group and own temporary marker path.
fs.writeFileSync(preload,`import fs from 'node:fs';import cp from 'node:child_process';import {syncBuiltinESMExports} from 'node:module';
const runDir=${JSON.stringify(runDir)},adapter=${JSON.stringify(adapter)},trace=${JSON.stringify(trace)},attempt=${JSON.stringify(attempt)},target=${targetAttempt};
const link=fs.linkSync,spawn=cp.spawn,kill=process.kill,write=fs.writeFileSync;let tries=0;const state={groups:[],signals:[],unownedSignals:0};const save=()=>write(trace,JSON.stringify(state));
cp.spawn=function(command,args,...rest){const child=spawn.call(this,command,args,...rest);if(command==='/bin/sh'&&args?.[0]===adapter){state.groups.push(child.pid);save();}return child;};
process.kill=function(pid,signal){if(pid<0){if(!state.groups.includes(-pid)){state.unownedSignals++;save();throw Error('unowned signal refused');}state.signals.push({pid,signal});save();}return kill.call(this,pid,signal);};
fs.linkSync=function(from,to,...rest){if(typeof from==='string'&&from.startsWith(runDir+'/.proxy-first-turn-')&&to===runDir+'/proxy-first-turn'){write(attempt,String(++tries));if(tries===target)throw Error('fx650-marker-fault');}return link.call(this,from,to,...rest);};syncBuiltinESMExports();\n`);
child=spawn(process.execPath,['--import',preload,RUNNER,runDir],{stdio:['pipe','ignore','pipe'],env:{PATH:process.env.PATH,HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,MOSAIC_CONFIG:process.env.MOSAIC_CONFIG}});let log='';child.stderr.on('data',chunk=>{log+=chunk;});child.stdin.on('error',()=>{});closed=new Promise((resolve,reject)=>{child.once('error',reject);child.once('close',(code,signal)=>resolve({code,signal}));});child.stdin.end(JSON.stringify({cap:f.agent})+'\n');
if(mode!=='landing'){const ready=Date.now()+5000;while(!fs.existsSync(join(runDir,'tools.sock'))&&Date.now()<ready)await delay(20);assert.equal(fs.existsSync(join(runDir,'tools.sock')),true,'real runner tool server ready');await f.call(f.human,'message.send',{to:'coder',body:'first fixture request',class:'REQUEST'});await f.call(f.human,'message.send',{to:'coder',body:'second fixture request',class:'REQUEST'});}
return {...f,runDir,closed,log:()=>log,attempt,targetAttempt,trace};
}
for(const mode of ['landing','message','retry'])test(`650 ${mode} publication failure reaps owned adapter then releases and exits2 without later turn`,{timeout:60000},async t=>{
const f=await setup(t,mode),deadline=Date.now()+15000;while(!f.log().includes('runner: first-turn marker publication failed')&&Date.now()<deadline)await delay(20);
assert.equal(f.log().includes('runner: first-turn marker publication failed'),true,'controlled publication failure observed at real caller');
const outcome=await f.closed;assert.equal(outcome.code,2);assert.equal(outcome.signal,null);assert.equal(f.log().includes('fx650-marker-fault'),false,'raw publication error never logged');assert.equal(fs.existsSync(join(f.runDir,'tools.sock')),false);
assert.equal(f.store.get("SELECT op FROM role_claims WHERE role='coder' ORDER BY seq DESC LIMIT 1").op,'release','existing role release reached');assert.equal(Number(fs.readFileSync(f.attempt,'utf8')),f.targetAttempt,'no later timestamp attempt after failure');
const traced=JSON.parse(fs.readFileSync(f.trace,'utf8'));assert.equal(traced.groups.length,f.targetAttempt,'no later adapter turn after publication failure');assert.ok(traced.signals.some(s=>s.signal==='SIGTERM'),'owned adapter TERM observed');assert.equal(traced.signals.every(s=>traced.groups.includes(-s.pid)),true,'no registry or unrelated PID signalled');assert.equal(traced.unownedSignals,0,'no unowned group signal attempted');
});
+51 -3
View File
@@ -1,4 +1,4 @@
# Model proxy (P1, #1827) # Model proxy (P1 #1827, P2 #1829)
The bus initializes and manages one pinned CLIProxyAPI per instance. PIN records The bus initializes and manages one pinned CLIProxyAPI per instance. PIN records
release URLs and independently reviewable SHA256s of the archive, executable and release URLs and independently reviewable SHA256s of the archive, executable and
@@ -20,8 +20,8 @@ contains `proxy.port` and `proxy.binary` only when proxy.json exists.
The management client is imported by the bus; it only GETs/PUTs the v8 list on The management client is imported by the bus; it only GETs/PUTs the v8 list on
127.0.0.1, never follows redirects, and emits receipts with SHA256 key IDs only. 127.0.0.1, never follows redirects, and emits receipts with SHA256 key IDs only.
Client-key values are held in memory, never returned by a bus verb. P2 adds Client-key values are never returned by an agent-facing bus verb. The bus manages
per-agent lifecycle integration; this row creates only the sentinel. per-agent lifecycle below; initialization creates only the sentinel.
The unit starts through a hash-checking runner with the pinned panel directory The unit starts through a hash-checking runner with the pinned panel directory
and `-local-model`, so catalogs require no network. Render/install is explicit; and `-local-model`, so catalogs require no network. Render/install is explicit;
@@ -47,3 +47,51 @@ Then start the bus normally and retry init from the human terminal. If the bus
or initialization state is uncertain, leave the lock in place. A refused fetch or initialization state is uncertain, leave the lock in place. A refused fetch
may leave empty `opt/cliproxyapi/` parent directories; they are not an installed may leave empty `opt/cliproxyapi/` parent directories; they are not an installed
version and need not be removed before retrying. version and need not be removed before retrying.
PIN upgrades must carry the native Console page's offline upgrade receipt.
Run `bash agents/poe/work/queue-330/acceptance.sh <verified-fetched-executable>`
under the test-slot rules, and include its output for the new PIN version in
that upgrade row's BUILD.md. Sage's landing gate checks the receipt. The script
uses a new temp root and private network namespace, with made-up credentials,
and calls the page's bus verbs. If the page cannot pass against the new PIN,
the upgrade stops; Sage takes Q46's fallback to Jason through Mos.
The native `/proxy` page uses human-only bus verbs; only the bus holds the
management client. On PIN 8.0.42, OAuth credentials are listed under
`/v8/management/credentials` and priority is patched as `{name, priority}`.
API-key groups are absent from that list: z.ai keys use the Claude-compatible
provider configuration at `/v8/management/config/api-keys/claude`, whose keys
carry proxy-generated `auth_index` identifiers. Their priority updates use
provider configuration and a read-back check. No upstream API-key value is
returned by the bus.
P2 adds an implicit `model` credential for every agent in proxy mode. The bus
confirms the v8 key-list write before storing a key at
`<configDir>/secrets/agents/<business>/<agent>/model` (0600, directories 0700).
The private file holds the key and its lifecycle metadata. Status and receipts
contain key IDs, issue time, rotation date and state; they never contain values.
No role or business `credentials.model` reference is needed or accepted.
Rotation defaults to 30 days. New launches use the replacement key. The old key
stays until its runs end or its grace expires; a failed bounded session stop
keeps both the old key and its retirement metadata for the next check. Revoke
requires a confirming management GET and polling `/v1/models` to 401 before local
cleanup. Polling has a five-second total deadline and 25ms intervals. A durable
`revoking` state blocks launches before preflight, and boot reconciliation
finishes it without restoring that key. Reconciliation preserves foreign keys
and refuses launches when this instance's sentinel is absent.
For a new instance only, human `proxy init` also accepts
`--rotate-grace-minutes N` (1–1440, default 120) and
`--first-output-seconds N` (1–3600, default 120). These are optional fields in
proxy.json; existing two-field sidecars remain valid. There is no per-agent
rotation-period setter in P2. Fetch also refuses agent-marked and tester calls
before reading config or touching the network/disk.
The trusted launcher requests `modelPreflight` with business/role and receives
exactly `{ baseUrl, key }`. That answer never passes through an agent socket or
status API. A 401 or other non-success HTTP status refuses `proxy-key-refused`;
a failed connection refuses `proxy-down`. No stdout by the configured deadline
requests a bounded stop and records `proxy-stall` with key IDs. The runner writes
only a fixed token to `<run>/proxy-first-output`, never stdout contents. A stop
failure records/refuses `proxy-stop-failed` and retains the key.
+55 -5
View File
@@ -15,14 +15,14 @@ export class ManagementClient {
|| typeof receipt !== 'function' || !Number.isSafeInteger(timeout) || timeout <= 0) refuse('proxy-client-invalid'); || typeof receipt !== 'function' || !Number.isSafeInteger(timeout) || timeout <= 0) refuse('proxy-client-invalid');
this.#base = u.origin; this.#key = key; this.#receipt = receipt; this.#timeout = timeout; this.#base = u.origin; this.#key = key; this.#receipt = receipt; this.#timeout = timeout;
} }
#request(method, keys, onStatus) { #request(method, endpoint, payload, onStatus) {
// node:http addresses the fixed numeric loopback directly. An ambient fetch // node:http addresses the fixed numeric loopback directly. An ambient fetch
// dispatcher or HTTP_PROXY cannot send the Authorization header elsewhere. // dispatcher or HTTP_PROXY cannot send the Authorization header elsewhere.
let deadline; let deadline;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let received = false; let received = false;
const req = http.request(this.#base + KEY_PATH, { const req = http.request(this.#base + endpoint, {
method, headers: { Authorization: `Bearer ${this.#key}`, ...(method === 'PUT' ? { 'Content-Type': 'application/json' } : {}) }, method, headers: { Authorization: `Bearer ${this.#key}`, ...(payload !== undefined ? { 'Content-Type': 'application/json' } : {}) },
}, response => { }, response => {
received = true; onStatus(response.statusCode); received = true; onStatus(response.statusCode);
const error = response.statusCode >= 300 && response.statusCode < 400 ? 'proxy-client-redirect' const error = response.statusCode >= 300 && response.statusCode < 400 ? 'proxy-client-redirect'
@@ -43,13 +43,13 @@ export class ManagementClient {
}); });
req.on('error', () => reject(new ProxyError(received ? 'proxy-client-response' : 'proxy-client-down'))); req.on('error', () => reject(new ProxyError(received ? 'proxy-client-response' : 'proxy-client-down')));
deadline = setTimeout(() => { req.destroy(); reject(new ProxyError('proxy-client-down')); }, this.#timeout); deadline = setTimeout(() => { req.destroy(); reject(new ProxyError('proxy-client-down')); }, this.#timeout);
req.end(method === 'PUT' ? JSON.stringify(keys) : undefined); req.end(payload === undefined ? undefined : JSON.stringify(payload));
}).finally(() => clearTimeout(deadline)); }).finally(() => clearTimeout(deadline));
} }
async #call(method, keys) { async #call(method, keys) {
let status = 0, ids = []; let status = 0, ids = [];
try { try {
const body = await this.#request(method, keys, value => { status = value; }); const body = await this.#request(method, KEY_PATH, keys, value => { status = value; });
if (method === 'GET') { if (method === 'GET') {
if (!validKeys(body)) refuse('proxy-client-response'); if (!validKeys(body)) refuse('proxy-client-response');
ids = body.map(sha256).map(h => h.slice(0, 12)); ids = body.map(sha256).map(h => h.slice(0, 12));
@@ -62,6 +62,56 @@ export class ManagementClient {
} }
getKeys() { return this.#call('GET'); } getKeys() { return this.#call('GET'); }
putKeys(keys) { if (!validKeys(keys)) refuse('proxy-client-invalid'); return this.#call('PUT', keys); } putKeys(keys) { if (!validKeys(keys)) refuse('proxy-client-invalid'); return this.#call('PUT', keys); }
// Fixed management operations for the bus's native page. Queries contain login
// states; neither queries nor request/response bodies belong in a receipt.
async #page(method, endpoint, payload, expectedStatus = null) {
let status = 0;
try {
const answer = await this.#request(method, endpoint, payload, value => { status = value; });
if (expectedStatus !== null && status !== expectedStatus) refuse('proxy-client-refused');
return answer;
} catch (error) {
if (method === 'GET' && endpoint === '/v8/management/config/api-keys/claude' && status === 404) return [];
throw error;
}
finally { this.#receipt({ call: `${method} ${endpoint.split('?')[0]}`, status, keyIds: [] }); }
}
getCredentials() { return this.#page('GET', '/v8/management/credentials'); }
getUpstreamGroups() { return this.#page('GET', '/v8/management/config/api-keys/claude'); }
putUpstreamGroups(groups) {
// auth_index is derived response metadata, not writable provider config.
const payload = groups.map(group => ({ ...group, keys: group.keys.map(({ auth_index, ...key }) => key) }));
return this.#page('PUT', '/v8/management/config/api-keys/claude', payload, 200);
}
patchCredential(name, priority) { return this.#page('PATCH', '/v8/management/credentials/fields', { name, priority }, 200); }
oauthStart(provider) {
if (!['claude', 'codex'].includes(provider)) refuse('proxy-client-invalid');
return this.#page('GET', `/v8/management/oauth/auth-url?provider=${provider}`);
}
oauthCallback(redirect_url) { return this.#page('POST', '/v8/management/oauth/callback', { redirect_url }); }
oauthStatus(state) { return this.#page('GET', `/v8/management/oauth/status?state=${encodeURIComponent(state)}`); }
oauthCancel(state) { return this.#page('DELETE', `/v8/management/oauth/session?state=${encodeURIComponent(state)}`); }
// Client-key authentication only. Never send the management key to the model route,
// follow a Location, consume/log the response body or trust an ambient dispatcher.
async probeKey(key, { timeout = this.#timeout } = {}) {
if (!validManagementKey(key) || !Number.isFinite(timeout) || timeout <= 0) refuse('proxy-client-invalid');
let timer, status = 0;
try {
return await new Promise((resolve, reject) => {
const req = http.request(this.#base + '/v1/models', { method: 'GET', headers: { Authorization: `Bearer ${key}` } }, response => {
status = response.statusCode;
response.destroy();
resolve(status);
});
req.on('error', () => reject(new ProxyError('proxy-down')));
timer = setTimeout(() => { req.destroy(); reject(new ProxyError('proxy-down')); }, timeout);
req.end();
});
} finally {
clearTimeout(timer);
this.#receipt({ call: 'GET /v1/models', status, keyIds: [sha256(key).slice(0, 12)] });
}
}
} }
// Trusted bus boot only: no CLI/Console/session exports this value or client. // Trusted bus boot only: no CLI/Console/session exports this value or client.
+2 -11
View File
@@ -1,11 +1,8 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { validateHeaderValue } from 'node:http';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
export class ProxyError extends Error { import { ProxyError, refuse, validManagementKey, proxyTiming } from './validation.mjs';
constructor(code) { super(code); this.name = 'ProxyError'; this.code = code; } export { ProxyError, refuse, validManagementKey, proxyTiming };
}
export const refuse = code => { throw new ProxyError(code); };
export const sha256 = value => createHash('sha256').update(value).digest('hex'); export const sha256 = value => createHash('sha256').update(value).digest('hex');
export const PIN = Object.freeze(JSON.parse(fs.readFileSync(new URL('../PIN', import.meta.url), 'utf8'))); export const PIN = Object.freeze(JSON.parse(fs.readFileSync(new URL('../PIN', import.meta.url), 'utf8')));
// Refuse symlink components before handling any state. Callers only create owned new leaves. // Refuse symlink components before handling any state. Callers only create owned new leaves.
@@ -28,9 +25,3 @@ export function regularBytes(file, code) {
} catch { refuse(code); } } catch { refuse(code); }
finally { if (fd !== undefined) fs.closeSync(fd); } finally { if (fd !== undefined) fs.closeSync(fd); }
} }
export function validManagementKey(key) {
if (typeof key !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(key)) return false;
try { validateHeaderValue('Authorization', `Bearer ${key}`); return true; }
catch { return false; }
}
+10 -6
View File
@@ -2,23 +2,26 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import net from 'node:net'; import net from 'node:net';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { PIN, refuse, safePath, regularBytes, sha256, validManagementKey } from './common.mjs'; import { PIN, refuse, safePath, regularBytes, sha256, validManagementKey, proxyTiming } from './common.mjs';
export function installPaths(dataRoot, pin = PIN) { export function installPaths(dataRoot, pin = PIN) {
const dir = path.join(dataRoot, 'opt', 'cliproxyapi', pin.version); const dir = path.join(dataRoot, 'opt', 'cliproxyapi', pin.version);
return { dir, binary: path.join(dir, 'cli-proxy-api'), panel: path.join(dir, 'static', 'management.html') }; return { dir, binary: path.join(dir, 'cli-proxy-api'), panel: path.join(dir, 'static', 'management.html') };
} }
export function validateProxy(value, dataRoot, pin = PIN) { export function validateProxy(value, dataRoot, pin = PIN) {
if (!value || typeof value !== 'object' || Array.isArray(value) if (!value || typeof value !== 'object' || Array.isArray(value)
|| Object.keys(value).sort().join(',') !== 'binary,port' || Object.keys(value).some(k => !['binary', 'port', 'rotateGraceMinutes', 'firstOutputSeconds'].includes(k))
|| !Number.isInteger(value.port) || value.port < 1024 || value.port > 65535 || value.port === 8317 || !Number.isInteger(value.port) || value.port < 1024 || value.port > 65535 || value.port === 8317
|| value.binary !== installPaths(dataRoot, pin).binary) refuse('proxy-config-invalid'); || value.binary !== installPaths(dataRoot, pin).binary) refuse('proxy-config-invalid');
return { port: value.port, binary: value.binary }; return { port: value.port, binary: value.binary, ...proxyTiming(value) };
} }
export function loadProxy(configDir, dataRoot) { export function loadProxy(configDir, dataRoot) {
const file = path.join(configDir, 'proxy.json'); const file = path.join(configDir, 'proxy.json');
try { fs.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return undefined; refuse('proxy-config-invalid'); } try { fs.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return undefined; refuse('proxy-config-invalid'); }
try { return validateProxy(JSON.parse(regularBytes(file, 'proxy-config-invalid')), dataRoot); } try { return validateProxy(JSON.parse(regularBytes(file, 'proxy-config-invalid')), dataRoot); }
catch { refuse('proxy-config-invalid'); } catch (error) {
if (['proxy-rotate-grace-invalid', 'proxy-first-output-invalid'].includes(error.code)) throw error;
refuse('proxy-config-invalid');
}
} }
export function verifyInstall(dataRoot, pin = PIN) { export function verifyInstall(dataRoot, pin = PIN) {
const paths = installPaths(dataRoot, pin); const paths = installPaths(dataRoot, pin);
@@ -50,8 +53,9 @@ export function initialConfig({ dataRoot, port, key, sentinel }) {
}; };
} }
// Called only by the bus's path-bound human verb, never by an agent or the tester. // Called only by the bus's path-bound human verb, never by an agent or the tester.
export async function initializeProxy({ configDir, dataRoot, key, port, pin = PIN }) { export async function initializeProxy({ configDir, dataRoot, key, port, pin = PIN, ...options }) {
if (!validManagementKey(key)) refuse('proxy-key-invalid'); if (!validManagementKey(key)) refuse('proxy-key-invalid');
const timing = proxyTiming(options);
safePath(configDir, 'proxy-init-failed'); safePath(dataRoot, 'proxy-init-failed'); safePath(configDir, 'proxy-init-failed'); safePath(dataRoot, 'proxy-init-failed');
const paths = verifyInstall(dataRoot, pin); const paths = verifyInstall(dataRoot, pin);
const settingsFile = path.join(configDir, 'proxy.json'), proxyDir = path.join(dataRoot, 'proxy'); const settingsFile = path.join(configDir, 'proxy.json'), proxyDir = path.join(dataRoot, 'proxy');
@@ -77,7 +81,7 @@ export async function initializeProxy({ configDir, dataRoot, key, port, pin = PI
const tag = sha256(configDir).slice(0, 12); const tag = sha256(configDir).slice(0, 12);
const sentinel = `msk-${tag}-sentinel-${randomBytes(32).toString('base64url')}`; const sentinel = `msk-${tag}-sentinel-${randomBytes(32).toString('base64url')}`;
fs.writeFileSync(path.join(proxyDir, 'config.yaml'), JSON.stringify(initialConfig({ dataRoot, port: reservation.port, key, sentinel }), null, 2) + '\n', { flag: 'wx', mode: 0o600 }); fs.writeFileSync(path.join(proxyDir, 'config.yaml'), JSON.stringify(initialConfig({ dataRoot, port: reservation.port, key, sentinel }), null, 2) + '\n', { flag: 'wx', mode: 0o600 });
const settings = { port: reservation.port, binary: paths.binary }; const settings = { port: reservation.port, binary: paths.binary, ...timing };
const settingsFd = fs.openSync(settingsFile, 'wx', 0o600); ownsSettings = true; const settingsFd = fs.openSync(settingsFile, 'wx', 0o600); ownsSettings = true;
try { fs.writeFileSync(settingsFd, JSON.stringify(settings, null, 2) + '\n'); } finally { fs.closeSync(settingsFd); } try { fs.writeFileSync(settingsFd, JSON.stringify(settings, null, 2) + '\n'); } finally { fs.closeSync(settingsFd); }
return settings; return settings;
+21
View File
@@ -0,0 +1,21 @@
import { validateHeaderValue } from 'node:http';
export class ProxyError extends Error {
constructor(code) { super(code); this.name = 'ProxyError'; this.code = code; }
}
export const refuse = code => { throw new ProxyError(code); };
export function validManagementKey(key) {
if (typeof key !== 'string' || !/^[\x21-\x7e]{1,4096}$/.test(key)) return false;
try { validateHeaderValue('Authorization', `Bearer ${key}`); return true; }
catch { return false; }
}
export function proxyTiming(value) {
const result = {};
for (const [field, max, code] of [['rotateGraceMinutes', 1440, 'proxy-rotate-grace-invalid'], ['firstOutputSeconds', 3600, 'proxy-first-output-invalid']]) {
if (!Object.hasOwn(value, field)) continue;
if (!Number.isInteger(value[field]) || value[field] < 1 || value[field] > max) refuse(code);
result[field] = value[field];
}
return result;
}
@@ -0,0 +1,49 @@
// P2 RED: catch wrong-key preflight, 401 success, redirect following and unbounded probes.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { ManagementClient } from '../src/client.mjs';
async function fixture(t, handler) {
const server = http.createServer(handler);
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
t.after(() => new Promise(resolve => { server.closeAllConnections(); server.close(resolve); }));
const baseUrl = `http://127.0.0.1:${server.address().port}`;
const receipts = [];
return { server, receipts, client: new ManagementClient({ baseUrl, key: 'fx331-manage', receipt: r => receipts.push(r) }) };
}
for (const status of [200, 401, 503]) {
test(`probeKey observes ${status} with one agent-authenticated GET /v1/models`, async t => {
const calls = [];
const f = await fixture(t, (req, res) => {
calls.push({ path: req.url, method: req.method, ownKey: req.headers.authorization === 'Bearer fx331-agent' });
res.writeHead(status); res.end('fixture-body');
});
assert.equal(typeof f.client.probeKey, 'function', 'client must support bounded agent-key probe');
assert.equal(await f.client.probeKey('fx331-agent', { timeout: 100 }), status);
assert.deepEqual(calls, [{ path: '/v1/models', method: 'GET', ownKey: true }]);
const output = JSON.stringify(f.receipts);
assert.equal(output.includes('fx331-agent'), false);
assert.equal(output.includes('fx331-manage'), false);
});
}
test('proxy-down: agent-key probe connection failure is sanitized', async t => {
const f = await fixture(t, (_req, res) => res.end());
await new Promise(resolve => f.server.close(resolve));
assert.equal(typeof f.client.probeKey, 'function');
await assert.rejects(f.client.probeKey('fx331-agent', { timeout: 100 }), e => e.code === 'proxy-down' && !e.message.includes('fx331-agent'));
});
test('proxy-down: silent listener cannot extend probe request deadline', async t => {
const f = await fixture(t, () => {});
assert.equal(typeof f.client.probeKey, 'function');
const started = performance.now();
await assert.rejects(f.client.probeKey('fx331-agent', { timeout: 50 }), e => e.code === 'proxy-down');
assert.ok(performance.now() - started < 1000, 'bounded HTTP probe must settle');
});
test('agent-key probe returns redirect status without following Location or exposing key', async t => {
let calls = 0;
const f = await fixture(t, (_req, res) => { calls++; res.writeHead(302, { Location: '/wrong' }); res.end(); });
assert.equal(typeof f.client.probeKey, 'function');
assert.equal(await f.client.probeKey('fx331-agent', { timeout: 100 }), 302);
assert.equal(calls, 1);
});
@@ -0,0 +1,66 @@
// P4 pinned8.0.42 regression: absent Claude provider config is HTTP404.
// Normalize only that fixed GET; all other methods/endpoints/statuses refuse.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { ManagementClient } from '../src/client.mjs';
const endpoint = '/v8/management/config/api-keys/claude';
const bodyCanary = 'p4-body-canary';
async function fixture(t, status, body = { error: bodyCanary, 'api-key': bodyCanary }) {
const requests = [], receipts = [];
const server = http.createServer(async (req, res) => {
const chunks = []; for await (const chunk of req) chunks.push(chunk);
requests.push({ method: req.method, path: req.url, body: Buffer.concat(chunks).toString('utf8') });
res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body));
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
t.after(() => new Promise(r => server.close(r)));
const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${server.address().port}`, key: 'p4-management', receipt: r => receipts.push(r) });
return { client, requests, receipts };
}
function evidence(s, method, path, status) {
assert.equal(s.requests.length, 1, 'one call, no retry');
assert.deepEqual(s.requests.map(r => [r.method, r.path]), [[method, path]]);
assert.deepEqual(s.receipts, [{ call: `${method} ${path}`, status, keyIds: [] }]);
assert.equal(JSON.stringify(s.receipts).includes(bodyCanary), false);
assert.equal(JSON.stringify(s.receipts).includes('p4-management'), false);
}
async function refused(operation) {
let failure;
try { await operation(); } catch (error) { failure = error; }
assert.equal(failure?.code, 'proxy-client-refused');
assert.equal(failure.message.includes(bodyCanary), false, 'no error-body value escapes');
assert.equal(JSON.stringify(failure).includes(bodyCanary), false);
}
test('empty provider GET404 yields empty array, discards body and receipts actual404 once', async t => {
const s = await fixture(t, 404);
const result = await s.client.getUpstreamGroups();
assert.equal(Array.isArray(result), true); assert.equal(result.length, 0);
assert.equal(JSON.stringify(result).includes(bodyCanary), false);
evidence(s, 'GET', endpoint, 404);
});
for (const status of [401, 403, 500]) test(`provider GET${status} remains proxy-client-refused, no retry`, async t => {
const s = await fixture(t, status);
await refused(() => s.client.getUpstreamGroups());
evidence(s, 'GET', endpoint, status);
});
for (const [method, path] of [['getCredentials', '/v8/management/credentials'], ['getKeys', '/v8/management/config/access/api-keys']])
test(`${method}404 remains proxy-client-refused, no retry`, async t => {
const s = await fixture(t, 404);
await refused(() => s.client[method]());
evidence(s, 'GET', path, 404);
});
test('provider PUT404 remains proxy-client-refused, never treated as empty GET', async t => {
const s = await fixture(t, 404);
await refused(() => s.client.putUpstreamGroups([{ name: 'fixture', keys: [{ 'api-key': 'p4-first-key' }] }]));
evidence(s, 'PUT', endpoint, 404);
assert.equal(s.requests[0].body.includes('p4-first-key'), true);
assert.equal(JSON.stringify(s.receipts).includes('p4-first-key'), false);
});
test('provider GET200 array passes through, with receipt and no retry', async t => {
const s = await fixture(t, 200, [{ name: 'fixture', keys: [] }]);
const result = await s.client.getUpstreamGroups();
assert.equal(Array.isArray(result), true); assert.equal(result.length, 1);
assert.equal(result[0].name === 'fixture' && Array.isArray(result[0].keys), true);
evidence(s, 'GET', endpoint, 200);
});
@@ -0,0 +1,15 @@
// P2 named settings refuse independently; legacy sidecars retain their shape.
import test from 'node:test';
import assert from 'node:assert/strict';
import { validateProxy, installPaths } from '../src/settings.mjs';
const dataRoot = '/fixture-data';
const base = { port: 18432, binary: installPaths(dataRoot).binary };
test('proxy settings without optional timing fields remain valid', () => assert.deepEqual(validateProxy(base, dataRoot), base));
test('proxy settings accept each positive upper bound', () => {
assert.deepEqual(validateProxy({ ...base, rotateGraceMinutes: 1440, firstOutputSeconds: 3600 }, dataRoot), { ...base, rotateGraceMinutes: 1440, firstOutputSeconds: 3600 });
});
for (const [field, maximum, code] of [['rotateGraceMinutes', 1440, 'proxy-rotate-grace-invalid'], ['firstOutputSeconds', 3600, 'proxy-first-output-invalid']]) {
for (const value of [0, -1, maximum + 1, 1.5, '120', null]) test(`${code}: ${JSON.stringify(value)}`, () => {
assert.throws(() => validateProxy({ ...base, [field]: value }, dataRoot), e => e.code === code);
});
}
@@ -0,0 +1,49 @@
// P4 client contract; endpoint/payload cases are probe-pending on PIN8.0.42.
// Catches management key/body/query values leaking into receipts, redirects/retries.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { ManagementClient } from '../src/client.mjs';
async function setup(t, handler) {
const calls = [], receipts = [];
const server = http.createServer(async (req, res) => {
let body = ''; for await (const c of req) body += c;
calls.push({ method: req.method, path: req.url, auth: req.headers.authorization, body });
const answer = handler?.(req, body) ?? { status: 200, body: { status: 'ok' } };
res.writeHead(answer.status, { 'content-type': 'application/json', ...answer.headers });
res.end(JSON.stringify(answer.body));
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
t.after(() => new Promise(r => server.close(r)));
const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${server.address().port}`, key: 'p4-management', receipt: r => receipts.push(r) });
return { client, calls, receipts };
}
test('OAuth management calls keep URL and state out of receipts', async t => {
const s = await setup(t);
assert.equal(typeof s.client.oauthStart, 'function');
await s.client.oauthStart('claude');
await s.client.oauthCallback('http://localhost/callback?state=p4-state&code=p4-code');
await s.client.oauthStatus('p4-state');
await s.client.oauthCancel('p4-state');
assert.deepEqual(s.calls.map(({ method, path }) => [method, path]), [
['GET', '/v8/management/oauth/auth-url?provider=claude'], ['POST', '/v8/management/oauth/callback'],
['GET', '/v8/management/oauth/status?state=p4-state'], ['DELETE', '/v8/management/oauth/session?state=p4-state'],
]);
assert.ok(s.calls.every(c => c.auth === 'Bearer p4-management'));
for (const value of ['p4-state', 'p4-code', 'localhost', 'p4-management']) assert.equal(JSON.stringify(s.receipts).includes(value), false);
});
test('upstream key goes only in request body and upstream refusal body is discarded', async t => {
const s = await setup(t, () => ({ status: 400, body: { error: 'p4-canary-key' } }));
assert.equal(typeof s.client.putUpstreamGroups, 'function');
await assert.rejects(s.client.putUpstreamGroups([{ name: 'z.ai', keys: [{ 'api-key': 'p4-canary-key' }] }]),
e => !e.message.includes('p4-canary-key'));
assert.equal(s.calls.length, 1);
assert.ok(s.calls[0].body.includes('p4-canary-key'));
assert.equal(JSON.stringify(s.receipts).includes('p4-canary-key'), false);
});
test('redirected management calls are not followed or retried', async t => {
const s = await setup(t, () => ({ status: 302, headers: { location: 'http://127.0.0.1:8317/' }, body: {} }));
assert.equal(typeof s.client.getCredentials, 'function');
await assert.rejects(s.client.getCredentials(), { code: 'proxy-client-redirect' });
assert.equal(s.calls.length, 1);
});
@@ -0,0 +1,35 @@
// R5: the sandbox may load parsing helpers, never proxy PIN or host state.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import * as common from '../src/common.mjs';
test('validation has only the exact parsing import and no state or I/O capability', () => {
const source = readFileSync(new URL('../src/validation.mjs', import.meta.url), 'utf8');
assert.deepEqual([...source.matchAll(/^import .*;$/gm)].map(m => m[0]), ["import { validateHeaderValue } from 'node:http';"]);
assert.equal((source.match(/\bimport\b/g) ?? []).length, 1);
assert.doesNotMatch(source, /\b(?:require|process|globalThis|global|fetch|eval|Function|WebSocket|XMLHttpRequest|Buffer|URL|fs|path|PIN|config|state|env)\b/);
assert.doesNotMatch(source, /\b(?:readFile|writeFile|open|request|createServer|connect|listen|setTimeout|setInterval)\w*\b/);
// No namespace/default import, aliases, reexports or lazy imports can expose node:http I/O.
assert.equal((source.match(/\bvalidateHeaderValue\b/g) ?? []).length, 2);
assert.doesNotMatch(source, /\bexport\s+(?:\*|\{)|\bimport\s*\(/);
});
test('common reexports the exact validation bindings and errors keep their identity', async () => {
const validation = await import('../src/validation.mjs');
for (const name of ['ProxyError', 'refuse', 'validManagementKey', 'proxyTiming']) assert.equal(common[name], validation[name], name);
assert.throws(() => validation.refuse('fixture-refusal'), e => e instanceof common.ProxyError && e.code === 'fixture-refusal' && e.message === 'fixture-refusal');
assert.throws(() => common.refuse('fixture-refusal'), e => e instanceof validation.ProxyError);
});
test('validation retains management header parsing and timing bounds', async () => {
const { validManagementKey, proxyTiming, ProxyError } = await import('../src/validation.mjs');
for (const value of ['a', 'fx331-key', '!~']) assert.equal(validManagementKey(value), true);
for (const value of ['', 'with space', 'line\n', 'line\r', '\t', '\u007f', '\u0080', null, 7]) assert.equal(validManagementKey(value), false);
assert.deepEqual(proxyTiming({}), {});
assert.deepEqual(proxyTiming({ rotateGraceMinutes: 1, firstOutputSeconds: 1 }), { rotateGraceMinutes: 1, firstOutputSeconds: 1 });
assert.deepEqual(proxyTiming({ rotateGraceMinutes: 1440, firstOutputSeconds: 3600 }), { rotateGraceMinutes: 1440, firstOutputSeconds: 3600 });
for (const [field, max, code] of [['rotateGraceMinutes', 1440, 'proxy-rotate-grace-invalid'], ['firstOutputSeconds', 3600, 'proxy-first-output-invalid']]) {
for (const value of [0, -1, max + 1, 1.5, '120', null]) assert.throws(() => proxyTiming({ [field]: value }), e => e instanceof ProxyError && e.code === code);
}
});
+5
View File
@@ -49,6 +49,8 @@ const KILL_WITH_PARENT = ["/usr/bin/setpriv", "--pdeathsig", "KILL", "--"];
// is missing here. // is missing here.
export const RUNNER_FILES = Object.freeze([ export const RUNNER_FILES = Object.freeze([
"packages/harness/src/runner.mjs", "packages/harness/src/runner.mjs",
// Row 331: fixed output-exists marker; writes only within the existing run directory.
"packages/harness/src/first-output.mjs",
"packages/harness/src/gate.mjs", "packages/harness/src/gate.mjs",
"packages/harness/src/tools.mjs", "packages/harness/src/tools.mjs",
"packages/harness/src/git-tools.mjs", "packages/harness/src/git-tools.mjs",
@@ -57,6 +59,9 @@ export const RUNNER_FILES = Object.freeze([
"packages/harness/src/pi-extension.mjs", "packages/harness/src/pi-extension.mjs",
"packages/bus/src/client.mjs", "packages/bus/src/client.mjs",
"packages/bus/src/broker.mjs", "packages/bus/src/broker.mjs",
"packages/bus/src/proxy-page-vocabulary.mjs",
// Row 331 r5: parsing only; common.mjs and PIN remain outside the sandbox.
"packages/model-proxy/src/validation.mjs",
"packages/bus/src/address.mjs", "packages/bus/src/address.mjs",
"packages/bus/src/landing.mjs", "packages/bus/src/landing.mjs",
"packages/business/src/vocabulary.mjs", "packages/business/src/vocabulary.mjs",
@@ -0,0 +1,45 @@
// #650 direct marker proof through the existing confined runner and writable runDir.
// No new mounts, fake client configuration or live proxy.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../../bus/src/store.mjs';
import { Broker } from '../../bus/src/broker.mjs';
import { serve } from '../../bus/src/server.mjs';
import { newRun, runnerOf, sessionEnv, spawnSession, writeSessionFile } from '../src/session.mjs';
import { startTimeOf } from '../src/proc.mjs';
import { sandboxProblem } from '../src/sandbox.mjs';
import { createLoader } from '../../plugins/src/loader.mjs';
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
async function confined(t,{enabled=false}={}){
const root=fs.realpathSync(fs.mkdtempSync(join(tmpdir(),'marker650-')));let store,server,child,exited,runDir;
t.after(async()=>{const failures=[];if(child){try{child.kill('SIGTERM');let timer;try{await Promise.race([exited,new Promise(resolve=>{timer=setTimeout(()=>{child.kill('SIGKILL');resolve();},5000);})]);await exited;}finally{clearTimeout(timer);}}catch{failures.push('own-child-close');}}try{await server?.close();}catch{failures.push('server-close');}try{store?.close();}catch{failures.push('store-close');}try{fs.rmSync(root,{recursive:true,force:true});}catch{failures.push('root-cleanup');}if(failures.length)throw new AggregateError(failures.map(code=>Error(code)),'owned marker fixture cleanup failed');});
const dataRoot=join(root,'data');fs.mkdirSync(dataRoot,{mode:0o700});store=new Store(dataRoot);
const broker=new Broker({store,businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'pm',delivery:'pm'},roles:{pm:{authority:{withinRole:['message.send'],crossRole:[]}},coder:{authority:{withinRole:['message.send'],crossRole:[]}}}}}});
const brokerSocket=join(root,'bus.sock');server=await serve({broker,path:brokerSocket});
const made=newRun(dataRoot,'demo'),run=made.run;runDir=made.runDir;const workspace=join(root,'ws'),bundle=join(runDir,'bundle');fs.mkdirSync(workspace);fs.mkdirSync(bundle,{mode:0o700});
fs.writeFileSync(join(bundle,'prompt.md'),'# test\n');fs.writeFileSync(join(bundle,'policy.json'),JSON.stringify({harness:'pi',workspace,tools:[],typed:[],network:'none',needs:[],credentials:[]}));fs.writeFileSync(join(bundle,'tools.json'),'[]');
const adapter=join(root,'adapter.sh');fs.writeFileSync(adapter,"printf '%s' 'fx650-private-output'\nprintf 'done\\n' > \"$MOSAIC_TURN_MARKER\"\n",{mode:0o644});
if(enabled)fs.writeFileSync(join(runDir,'proxy-first-turn-enabled'),'enabled\n',{mode:0o600,flag:'wx'});
writeSessionFile(runDir,{business:'demo',instance:'coder',run,launchedBy:'pm',harness:'pi',provider:'fake',model:'fake-model',brokerSocket,workspace,sessionDir:join(runDir,'session'),adapter,bundle:{prompt:join(bundle,'prompt.md'),policy:join(bundle,'policy.json'),tools:join(bundle,'tools.json')},toolSocket:join(runDir,'tools.sock'),turnMarker:join(runDir,'turn.done'),turnEnd:'marker',harnessEnv:{},pollInterval:50});
const loader=createLoader({log:()=>{}}),loaded=await loader.load({harness:['pi']});assert.deepEqual(loaded.refused,[]);const harnessMounts=loader.modules('harness')[0].mounts({env:{}});
child=spawnSession({runDir,env:sessionEnv({HOME:process.env.HOME,PATH:process.env.PATH},run),paths:{workspace,adapter,harnessMounts,brokerSocket},business:'demo',role:'coder'});
exited=new Promise((resolve,reject)=>{child.once('error',reject);child.once('close',(code,signal)=>resolve({code,signal}));});await runnerOf(child);
const cap=broker.bindLaunch({business:'demo',role:'coder',run,harness:'pi',pid:child.pid,startTime:startTimeOf(child.pid)}),pm=broker.bindLaunch({business:'demo',role:'pm',run:'marker-pm',harness:'pi'});broker.request(pm,{verb:'role.claim'});child.stdin.end(JSON.stringify({cap})+'\n');
async function ask(body){const sent=broker.request(pm,{verb:'message.send',args:{to:'coder',body,class:'REQUEST'}}),deadline=Date.now()+15000;while(Date.now()<deadline){const reply=broker.request(pm,{verb:'message.receive'}).find(r=>r.in_reply_to===sent.id);if(reply)return reply.body;await delay(25);}throw Error('confined marker response not observed');}
return {runDir,ask,store,exited,send:body=>broker.request(pm,{verb:'message.send',args:{to:'coder',body,class:'REQUEST'}})};
}
const skip=sandboxProblem()??false; // Any unavailable confinement is UNVERIFIED, never green evidence.
test('650 confined actual stdout writes fixed output marker in existing runDir without changing reply',{skip,timeout:60000},async t=>{
const f=await confined(t),file=join(f.runDir,'proxy-first-output');await delay(150);assert.equal(fs.existsSync(file),false,'idle confined runner has no output marker');
assert.equal(await f.ask('first'),'fx650-private-output');assert.equal(fs.readFileSync(file,'utf8'),'output\n');assert.equal(fs.statSync(file).mode&0o777,0o600);
const stat=fs.statSync(file);assert.equal(await f.ask('second'),'fx650-private-output');assert.equal(fs.statSync(file).mtimeMs,stat.mtimeMs);assert.equal(fs.readFileSync(file,'utf8').includes('fx650-private-output'),false);assert.equal(fs.existsSync(join(f.runDir,'proxy-first-turn')),false,'non-proxy actual turns never publish first-turn timestamps');
});
test('650 confined actual adapter turn publishes first timestamp once and idle stays unarmed',{skip,timeout:60000},async t=>{
const f=await confined(t,{enabled:true}),file=join(f.runDir,'proxy-first-turn');await delay(150);assert.equal(fs.existsSync(file),false,'idle confined runner has no turn marker');
const before=process.hrtime.bigint();assert.equal(await f.ask('first'),'fx650-private-output');assert.equal(fs.existsSync(file),true,'actual confined turn publishes first-turn marker');
const value=fs.readFileSync(file,'utf8');assert.match(value,/^turn [0-9]+\n$/);const stamp=BigInt(value.trim().split(' ')[1]);assert.ok(stamp>=before&&stamp<=process.hrtime.bigint(),'host and confined process share monotonic clock');assert.equal(fs.statSync(file).mode&0o777,0o600);
const stat=fs.statSync(file);assert.equal(await f.ask('second'),'fx650-private-output');assert.equal(fs.readFileSync(file,'utf8'),value);assert.equal(fs.statSync(file).mtimeMs,stat.mtimeMs);assert.equal(fs.readdirSync(f.runDir).some(n=>n.startsWith('.proxy-first-turn-')),false,'atomic publication leaves no temp marker');
});
+11
View File
@@ -53,6 +53,17 @@ test("RUNNER_FILES is exactly what the runner and the harness sources it starts
assert.deepEqual([...seen].sort(), [...RUNNER_FILES].sort()); assert.deepEqual([...seen].sort(), [...RUNNER_FILES].sort());
}); });
test("row 331 mounts only the parsing validator and fixed marker code, never proxy state", (t) => {
const root = scratch(t);
const list = binds({ runDir: join(root, "run"), workspace: join(root, "ws"), adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") });
for (const relative of ["packages/model-proxy/src/validation.mjs", "packages/harness/src/first-output.mjs"]) {
const src = join(REPO, relative);
assert.deepEqual(list.filter(b => b.src === src), [{ kind: "ro", src, why: "a repository file the runner loads" }]);
}
assert.deepEqual(list.filter(b => b.src?.startsWith(join(REPO, "packages/model-proxy"))).map(b => b.src), [join(REPO, "packages/model-proxy/src/validation.mjs")]);
assert.equal(list.some(b => b.src === REPO || b.src === join(REPO, "packages/harness/src")), false);
});
test("sandboxProblem names a missing bwrap and a bwrap that fails", (t) => { test("sandboxProblem names a missing bwrap and a bwrap that fails", (t) => {
const root = scratch(t); const root = scratch(t);
assert.equal(sandboxProblem({ bwrap: join(root, "nope") }), `${join(root, "nope")} is missing`); assert.equal(sandboxProblem({ bwrap: join(root, "nope") }), `${join(root, "nope")} is missing`);
+20
View File
@@ -1104,3 +1104,23 @@ apply separately to each project and instance; a collaborator grant elsewhere
does not enable writes here. Grant hints refresh on each user chat render and does not enable writes here. Grant hints refresh on each user chat render and
background refresh, while the bus decides every operation from live grants. background refresh, while the bus decides every operation from live grants.
The existing user notice names the governance records visible to the owner. The existing user notice names the governance records visible to the owner.
## Model proxy page (P4, #1828)
The owner's `/proxy` page signs in Claude/Codex accounts, accepts write-only
z.ai API keys, sets account priority, and lists client-key IDs. It uses only
`/api/proxy/` routes over the owner's human bus transport. A missing instance
proxy refuses `not-configured`; it does not ask for a management key. That key
stays in the bus. No panel HTML or proxy-port forward is served.
The page and routes refuse granted users, tester listeners and loopback Hosts.
Writes also require `--send`, the Console's own Origin and same-origin Fetch
Metadata. Account display names use a provider label and stable hashed id;
arbitrary upstream labels and emails are not reflected. Key inputs and pasted
callback URLs are cleared when submitted and never saved in browser storage.
Only an `ok` status completes OAuth; accepted callbacks remain pending. At the
bus's default300-second timeout, status returns `error/login-timeout` once and
retires the state. Refreshing the page does not restore a pending login.
Usage is reserved on this page for P5; P4 adds no usage source. PIN changes
require the offline upgrade acceptance documented in packages/model-proxy.
+3 -3
View File
@@ -68,14 +68,14 @@ try {
// the system config, Business can't resolve authority and Settings has // the system config, Business can't resolve authority and Settings has
// no notifier. A refusal printed here names no path (redactor). // no notifier. A refusal printed here names no path (redactor).
// --send without a bus refuses to start rather than serve a composer that can't send. // --send without a bus refuses to start rather than serve a composer that can't send.
let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, libraryRevert = null, librarySave = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, decisionWrite = null, busLine, system = null; let bus = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, libraryFile = null, libraryStatus = null, librarySync = null, libraryRevert = null, librarySave = null, projectSave = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, proxyCall = null, decisionWrite = null, busLine, system = null;
let processing = null, ownerCall = null; let processing = null, ownerCall = null;
try { try {
system = loadSystem(); system = loadSystem();
const { dataRoot } = system; const { dataRoot } = system;
const call = ownerCall = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null }); const call = ownerCall = busCaller({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
// FED R6 (#1800): the bus-backed options, shared with each granted user's session (console-bus.mjs). // FED R6 (#1800): the bus-backed options, shared with each granted user's session (console-bus.mjs).
({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, libraryRevert, librarySave, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite } = consoleBus(call, { send: sendOn })); ({ processing, chat, bus, projectFiles, projectFile, libraryFile, libraryStatus, send, librarySync, libraryRevert, librarySave, settingsWrite, projectSave, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall } = consoleBus(call, { send: sendOn }));
// Row 252 (#1753): Approve and Deny on grant requests, through the console wrapper, only with // Row 252 (#1753): Approve and Deny on grant requests, through the console wrapper, only with
// --sso and --send. // --sso and --send.
if (ssoOn && sendOn) decisionWrite = consoleCall({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null }); if (ssoOn && sendOn) decisionWrite = consoleCall({ dataRoot, socket: socketPath(dataRoot), business: options.business ?? null });
@@ -107,7 +107,7 @@ try {
} }
let server; let server;
try { try {
server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, libraryRevert, librarySave, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, decisionWrite, userOptions, federation: system?.federation ?? null }); server = await startServer({ board: options.board, port: options.port, bus, reads, preview: options.preview, sso, proof, send, settingsWrite, projectFiles, projectFile, libraryFile, libraryStatus, librarySync, libraryRevert, librarySave, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall, processing, decisionWrite, userOptions, federation: system?.federation ?? null });
} catch (err) { sso?.close(); throw err; } } catch (err) { sso?.close(); throw err; }
let testerServer = null; let testerServer = null;
if (testerSocket) { if (testerSocket) {
+4
View File
@@ -8,6 +8,9 @@ import { views } from '../../bus/src/views.mjs';
// Row 194 (#1692, D7): the Agents section's verbs. The caller refuses any other, so the server's // Row 194 (#1692, D7): the Agents section's verbs. The caller refuses any other, so the server's
// agentWrite reaches only these. // agentWrite reaches only these.
export const AGENT_VERBS = ['instance.launch', 'instance.regenerate', 'instance.end', 'veto.set', 'instance.request']; export const AGENT_VERBS = ['instance.launch', 'instance.regenerate', 'instance.end', 'veto.set', 'instance.request'];
// P4 owner transport only; the HTTP boundary and broker both refuse users/testers.
export const PROXY_VERBS = Object.freeze(['proxy.accounts', 'proxy.login.start', 'proxy.login.submit',
'proxy.login.status', 'proxy.login.cancel', 'proxy.apikey.add', 'proxy.priority.set', 'proxy.keys']);
export const TESTER_AGENT_VERBS = ['instance.launch', 'instance.end', 'instance.request']; export const TESTER_AGENT_VERBS = ['instance.launch', 'instance.end', 'instance.request'];
export const agentCaller = (call, verbs) => async (verb, args) => { export const agentCaller = (call, verbs) => async (verb, args) => {
if (!verbs.includes(verb)) throw new Error(`the Console does not call ${verb}`); if (!verbs.includes(verb)) throw new Error(`the Console does not call ${verb}`);
@@ -18,6 +21,7 @@ export const agentCaller = (call, verbs) => async (verb, args) => {
// a granted user's options; it goes to createChat, which sends it only the `plugins` read. // a granted user's options; it goes to createChat, which sends it only the `plugins` read.
export function consoleBus(call, { send: sendOn = false, owner = null } = {}) { export function consoleBus(call, { send: sendOn = false, owner = null } = {}) {
const o = { const o = {
proxyCall: agentCaller(call, PROXY_VERBS),
processing: { read: () => call('processing.status'), halt: args => call('processing.halt', args), resume: args => call('processing.resume', args) }, processing: { read: () => call('processing.status'), halt: args => call('processing.halt', args), resume: args => call('processing.resume', args) },
chat: createChat({ call, owner }), chat: createChat({ call, owner }),
// Row 155 (#1649): `names` is a webui read only, so it sits beside the shared views. Row 165 // Row 155 (#1649): `names` is a webui read only, so it sits beside the shared views. Row 165
+52
View File
@@ -0,0 +1,52 @@
// Native Console routes: fixed path -> one owner human-transport call. Never
// imports a management client, reads proxy settings, or opens a proxy connection.
const routes = Object.freeze({
'/api/proxy/accounts': ['GET', 'proxy.accounts', []],
'/api/proxy/keys': ['GET', 'proxy.keys', []],
'/api/proxy/login/start': ['POST', 'proxy.login.start', ['provider']],
'/api/proxy/login/submit': ['POST', 'proxy.login.submit', ['redirect_url']],
'/api/proxy/login/status': ['POST', 'proxy.login.status', ['state']],
'/api/proxy/login/cancel': ['POST', 'proxy.login.cancel', ['state']],
'/api/proxy/apikey/add': ['POST', 'proxy.apikey.add', ['provider', 'key']],
'/api/proxy/priority/set': ['POST', 'proxy.priority.set', ['accountId', 'priority']],
});
const codes = ['not-configured', 'human-required', 'provider-not-allowed', 'login-state-unknown',
'callback-url-invalid', 'apikey-invalid', 'priority-invalid', 'priority-unconfirmed', 'priority-write-refused', 'proxy-down', 'invalid-request'];
const json = (res, status, value) => {
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }); res.end(JSON.stringify(value));
};
export async function proxyHTTP(req, res, { path, call, named, tester, granted, send, origin, user, log }) {
const route = routes[path];
const done = (status, value) => {
// No request body, query, login state, authorization URL or exception text.
log(`${new Date().toISOString()} ${route?.[0] ?? 'GET'} ${route ? path : '/api/proxy/unknown'} ${status} user=${JSON.stringify(user ?? '')}`);
return json(res, status, value);
};
if (tester || granted || !named) return done(403, { error: 'human-required' });
if (!route) return done(404, { error: 'not found' });
if (req.method !== route[0]) return done(405, { error: 'method not allowed' });
if (req.method === 'POST') {
if (!send) return done(403, { error: 'read-only' });
if (req.headersDistinct.origin?.length !== 1 || req.headers.origin !== origin) return done(403, { error: 'cross-origin request refused' });
if (req.headersDistinct['sec-fetch-site']?.length !== 1 || req.headers['sec-fetch-site'] !== 'same-origin') return done(403, { error: 'cross-site request refused' });
}
if (!call) return done(503, { error: 'not-configured' });
let args = {};
if (req.method === 'POST') {
try {
if ((req.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase() !== 'application/json') throw Error();
const chunks = []; let size = 0;
for await (const chunk of req) {
size += chunk.length; if (size > 16384) throw Error(); chunks.push(chunk);
}
args = JSON.parse(Buffer.concat(chunks));
if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).length !== route[2].length
|| route[2].some(k => !Object.hasOwn(args, k))) throw Error();
} catch { return done(400, { error: 'invalid-request' }); }
}
try { return done(200, await call(route[1], args)); }
catch (error) {
const code = codes.includes(error.code) ? error.code : 'outcome-unknown';
return done(code === 'human-required' ? 403 : ['not-configured', 'proxy-down', 'outcome-unknown'].includes(code) ? 503 : 400, { error: code });
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
<button id="processing-resume" type="button" class="btn" disabled hidden>Resume processing</button> <button id="processing-resume" type="button" class="btn" disabled hidden>Resume processing</button>
<p id="processing-unavailable" class="processing-note small muted">Processing controls are unavailable here. Use mosaic halt or mosaic resume from the instance CLI.</p> <p id="processing-unavailable" class="processing-note small muted">Processing controls are unavailable here. Use mosaic halt or mosaic resume from the instance CLI.</p>
<p id="processing-receipt" class="processing-receipt small" aria-live="polite"></p></div> <p id="processing-receipt" class="processing-receipt small" aria-live="polite"></p></div>
<h2 class="tree-title">Console</h2><ul class="s1-sections" id="sections"></ul><div id="board-tree"><h2 class="tree-title">Projects</h2><div id="projects"><p class="muted">Loading projects…</p></div><p class="small muted">Select a project to filter sessions. Waiting on you always shows all projects.</p></div></nav> <p><a href="/proxy">Model proxy</a></p><h2 class="tree-title">Console</h2><ul class="s1-sections" id="sections"></ul><div id="board-tree"><h2 class="tree-title">Projects</h2><div id="projects"><p class="muted">Loading projects…</p></div><p class="small muted">Select a project to filter sessions. Waiting on you always shows all projects.</p></div></nav>
<main id="main" class="content" tabindex="-1"><div id="signed-out" class="banner ref" role="alert" tabindex="-1" hidden></div><div id="bus-down" class="banner err" role="alert" hidden></div><div class="page-head" id="board-head"><div><h1>Control board</h1><p id="status" class="small muted" role="status">Loading board…</p></div><div class="filters"><label><input id="show-machine" type="checkbox">Show machine sessions</label></div></div> <main id="main" class="content" tabindex="-1"><div id="signed-out" class="banner ref" role="alert" tabindex="-1" hidden></div><div id="bus-down" class="banner err" role="alert" hidden></div><div class="page-head" id="board-head"><div><h1>Control board</h1><p id="status" class="small muted" role="status">Loading board…</p></div><div class="filters"><label><input id="show-machine" type="checkbox">Show machine sessions</label></div></div>
<section id="held-section" aria-labelledby="held-title"><h2 id="held-title">Held roles <span id="held-count" class="count">–</span></h2><p class="small muted">The stack's seats: each role a run holds on the bus, from the bus <code>agents</code> read. Machine sessions are under Show machine sessions.</p><div id="held"><div class="skel-rows" aria-busy="true"><span class="sr-only">Reading the held roles.</span><span class="skel"></span><span class="skel"></span></div></div></section> <section id="held-section" aria-labelledby="held-title"><h2 id="held-title">Held roles <span id="held-count" class="count">–</span></h2><p class="small muted">The stack's seats: each role a run holds on the bus, from the bus <code>agents</code> read. Machine sessions are under Show machine sessions.</p><div id="held"><div class="skel-rows" aria-busy="true"><span class="sr-only">Reading the held roles.</span><span class="skel"></span><span class="skel"></span></div></div></section>
<div id="error" class="banner err" role="alert" hidden></div> <div id="error" class="banner err" role="alert" hidden></div>
+26
View File
@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en" data-design="console" data-palette="harbor">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Model proxy · Mosaic Console</title>
<link rel="stylesheet" href="/shared/app.css"><link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/console.css"><link rel="stylesheet" href="/shell.css">
<script src="/proxy.js" defer></script></head>
<body><a class="skip" href="#main">Skip to content</a>
<header class="cmdbar"><a class="wordmark" href="/">Mosaic<small>Console</small></a><a href="/">Back to Console</a></header>
<main id="main" class="content" tabindex="-1">
<div class="page-head"><div><h1>Model proxy</h1><p>Accounts used by this instance, their priority, and the client keys assigned to agents.</p></div><button id="proxy-refresh" type="button">Refresh</button></div>
<p id="proxy-message" role="status" aria-live="polite">Reading accounts…</p>
<p id="proxy-readonly" class="muted" hidden>This Console is read-only. Sign-in and account changes are unavailable.</p>
<section class="panel" aria-labelledby="proxy-accounts-title"><h2 id="proxy-accounts-title">Accounts</h2>
<p>Higher priority is used first. Account names are stable identifiers; credential values are never displayed.</p>
<div class="table-wrap" tabindex="0" role="region" aria-label="Accounts"><table><thead><tr><th>Account</th><th>Provider</th><th>Status</th><th>Priority</th></tr></thead><tbody id="proxy-accounts"></tbody></table></div></section>
<section class="panel" aria-labelledby="proxy-signin-title"><h2 id="proxy-signin-title">Sign in to an account</h2>
<form id="proxy-login-start"><fieldset data-write disabled><legend>Subscription account</legend><label>Provider <select name="provider"><option value="claude">Claude</option><option value="codex">Codex</option></select></label><button type="submit">Start sign-in</button></fieldset></form>
<p id="proxy-login-message" role="status" aria-live="polite"></p>
<p><a id="proxy-authorize" target="_blank" rel="noopener noreferrer" hidden>Open account sign-in in a new tab</a></p>
<form id="proxy-login-submit"><fieldset data-write disabled><legend>Finish sign-in</legend><p>If the sign-in tab does not return, copy its full address and paste it here.</p><label>Callback address <input name="redirect_url" type="url" autocomplete="off" spellcheck="false" maxlength="8192" required></label><button type="submit">Submit address</button><button id="proxy-login-cancel" type="button">Cancel sign-in</button></fieldset></form></section>
<section class="panel" aria-labelledby="proxy-apikey-title"><h2 id="proxy-apikey-title">Add an API key</h2>
<form id="proxy-apikey"><fieldset data-write disabled><legend>z.ai</legend><label>API key <input name="key" type="password" autocomplete="off" spellcheck="false" maxlength="4096" required></label><p>The key is sent once and cleared from this field. It will never be shown again.</p><button type="submit">Add key</button></fieldset></form></section>
<section class="panel" aria-labelledby="proxy-keys-title"><h2 id="proxy-keys-title">Client keys</h2><p>Only key IDs are shown. Hire and fire manage agent keys.</p>
<div class="table-wrap" tabindex="0" role="region" aria-label="Client keys"><table><thead><tr><th>Key ID</th><th>Managed by</th><th>Agent</th><th>Issued</th><th>State</th></tr></thead><tbody id="proxy-keys"></tbody></table></div></section>
<section class="panel" id="proxy-usage" aria-labelledby="proxy-usage-title"><h2 id="proxy-usage-title">Usage</h2><p id="proxy-usage-status">Usage reporting will appear here when its view is available.</p></section>
</main></body></html>
+108
View File
@@ -0,0 +1,108 @@
// P4 page: credentials and OAuth state exist only during the current operation.
// No browser storage, request/body logging, key reveal, or automatic write retry.
(() => {
'use strict';
const $ = id => document.getElementById(id);
let canWrite = false, login = null, poll = null, busy = false;
const message = (text, loginOnly = false) => { $(loginOnly ? 'proxy-login-message' : 'proxy-message').textContent = text; };
const cell = value => { const td = document.createElement('td'); td.textContent = value ?? '—'; return td; };
async function call(path, args) {
const response = await fetch('/api/proxy/' + path, { cache: 'no-store', credentials: 'same-origin',
method: args === undefined ? 'GET' : 'POST',
...(args === undefined ? {} : { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(args) }),
});
const value = await response.json();
if (!response.ok) {
if (response.status === 401) throw new Error('Sign in to the Console again.');
if (value.error === 'not-configured') throw new Error('The model proxy is not configured for this instance.');
if (value.error === 'login-state-unknown') throw new Error('This sign-in has ended. Start a new sign-in.');
if (value.error === 'apikey-invalid') throw new Error('Enter a nonempty API key, with no spaces or line breaks.');
throw new Error('The operation could not be confirmed. Nothing was retried. Refresh before trying again.');
}
return value;
}
function endLogin() {
clearTimeout(poll); poll = null; login = null;
$('proxy-authorize').removeAttribute('href'); $('proxy-authorize').hidden = true;
$('proxy-login-submit').elements.redirect_url.value = '';
}
async function status() {
if (!login) return;
const active = login;
try {
const result = await call('login/status', { state: active.state });
if (active !== login) return;
if (result.status === 'wait') { message('Waiting for account sign-in…', true); poll = setTimeout(status, 2000); return; }
endLogin();
if (result.status === 'ok') { message('Account signed in.', true); await refresh(); }
else message(result.reason === 'login-timeout' ? 'Sign-in timed out. Start again.' : 'Account sign-in failed. Start again.', true);
} catch (error) { if (active === login) { endLogin(); message(error.message, true); } }
}
async function refresh() {
const [accounts, keys] = await Promise.all([call('accounts'), call('keys')]);
const rows = accounts.accounts.map(account => {
const row = document.createElement('tr');
row.append(cell(account.displayName), cell(account.provider), cell(account.status));
const td = document.createElement('td'), form = document.createElement('form');
const input = document.createElement('input'); input.type = 'number'; input.min = '-1000'; input.max = '1000'; input.step = '1'; input.value = account.priority;
input.setAttribute('aria-label', `Priority for ${account.displayName}`); input.disabled = !canWrite;
const save = document.createElement('button'); save.type = 'submit'; save.textContent = 'Set priority'; save.disabled = !canWrite;
form.append(input, save); form.addEventListener('submit', async event => {
event.preventDefault(); if (!canWrite || busy || !input.checkValidity()) return;
busy = true; save.disabled = true;
try { await call('priority/set', { accountId: account.accountId, priority: Number(input.value) }); message('Priority saved.'); await refresh(); }
catch (error) { message(error.message); }
finally { busy = false; save.disabled = !canWrite; }
});
td.append(form); row.append(td); return row;
});
$('proxy-accounts').replaceChildren(...rows);
if (!rows.length) { const row = document.createElement('tr'); const td = cell('No accounts configured.'); td.colSpan = 4; row.append(td); $('proxy-accounts').append(row); }
$('proxy-keys').replaceChildren(...keys.keys.map(key => {
const row = document.createElement('tr'); row.append(cell(key.keyId), cell(key.management), cell(key.agent), cell(key.issuedAt), cell(key.state)); return row;
}));
}
$('proxy-refresh').addEventListener('click', () => refresh().then(() => message('Accounts and client keys refreshed.')).catch(e => message(e.message)));
$('proxy-login-start').addEventListener('submit', async event => {
event.preventDefault(); if (!canWrite || busy || login) return;
busy = true;
try {
login = await call('login/start', { provider: event.currentTarget.elements.provider.value });
$('proxy-authorize').href = login.url; $('proxy-authorize').hidden = false;
window.open(login.url, '_blank', 'noopener,noreferrer');
message('Complete account sign-in in the new tab, or use the sign-in link.', true);
poll = setTimeout(status, 2000);
} catch (error) { message(error.message, true); }
finally { busy = false; }
});
$('proxy-login-submit').addEventListener('submit', async event => {
event.preventDefault(); if (!canWrite || busy || !login) return;
const input = event.currentTarget.elements.redirect_url, redirect_url = input.value; input.value = '';
busy = true;
try { await call('login/submit', { redirect_url }); message('Address submitted. Waiting for sign-in confirmation…', true); }
catch (error) { message(error.message, true); }
finally { busy = false; }
});
$('proxy-login-cancel').addEventListener('click', async () => {
if (!canWrite || busy || !login) return;
busy = true; clearTimeout(poll);
try { await call('login/cancel', { state: login.state }); endLogin(); message('Sign-in cancelled.', true); }
catch (error) { endLogin(); message(error.message, true); }
finally { busy = false; }
});
$('proxy-apikey').addEventListener('submit', async event => {
event.preventDefault(); if (!canWrite || busy) return;
const input = event.currentTarget.elements.key, key = input.value; input.value = '';
busy = true;
try { await call('apikey/add', { provider: 'z.ai', key }); message('API key added.'); await refresh(); }
catch (error) { message(error.message); }
finally { busy = false; }
});
window.addEventListener('pagehide', () => { endLogin(); $('proxy-apikey').elements.key.value = ''; });
fetch('/api/config', { cache: 'no-store' }).then(r => r.json()).then(config => {
canWrite = config.send === true && config.tester !== true && config.user !== true;
document.querySelectorAll('[data-write]').forEach(fieldset => { fieldset.disabled = !canWrite; });
$('proxy-readonly').hidden = canWrite;
return refresh();
}).then(() => message('Accounts and client keys loaded.')).catch(error => message(error.message));
})();
+16 -4
View File
@@ -1,3 +1,4 @@
import { proxyHTTP } from './proxy-http.mjs';
import { createServer } from 'node:http'; import { createServer } from 'node:http';
import { bindTesterSocket, TESTER_ORIGIN } from './tester-socket.mjs'; import { bindTesterSocket, TESTER_ORIGIN } from './tester-socket.mjs';
import { readFileSync, openSync, fstatSync, closeSync, constants } from 'node:fs'; import { readFileSync, openSync, fstatSync, closeSync, constants } from 'node:fs';
@@ -29,6 +30,8 @@ export function boardURL(value) {
const root = resolve(import.meta.dirname, 'public'); const root = resolve(import.meta.dirname, 'public');
const files = new Map([ const files = new Map([
['/', ['index.html', 'text/html; charset=utf-8']], ['/', ['index.html', 'text/html; charset=utf-8']],
['/proxy', ['proxy.html', 'text/html; charset=utf-8']],
['/proxy.js', ['proxy.js', 'text/javascript; charset=utf-8']],
...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'processing.js', 'agents.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]), ...['app.js', 'brand.js', 'bus.js', 'markdown-preview.js', 'chat.js', 'processing.js', 'agents.js', 'federation.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
...['shared/app.css', 'tokens.css', 'console.css', 'live.css', 'bus.css', 'shell.css', 'chat.css', 'processing.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]), ...['shared/app.css', 'tokens.css', 'console.css', 'live.css', 'bus.css', 'shell.css', 'chat.css', 'processing.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
['/editor.js', ['editor.js', 'text/javascript; charset=utf-8']], ['/editor.js', ['editor.js', 'text/javascript; charset=utf-8']],
@@ -650,7 +653,7 @@ async function libraryStatusRead(res, read, search) {
// `log` takes one line per send attempt that got past sign-in; never the body. `fieldsFile` is for // `log` takes one line per send attempt that got past sign-in; never the body. `fieldsFile` is for
// tests; the field table is otherwise the committed packages/tasks/fields.json. // tests; the field table is otherwise the committed packages/tasks/fields.json.
// `agentWrite` (row 194, D7) is `(verb, args) => Promise` for the Agents section's verbs and instance.request. // `agentWrite` (row 194, D7) is `(verb, args) => Promise` for the Agents section's verbs and instance.request.
export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, libraryStatus = null, libraryRevert = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) { export async function startServer({ host = '127.0.0.1', port = 7330, board = DEFAULT_BOARD, timeout = 20000, bus = null, reads = null, preview = null, sso = null, tester = false, testerSocket = null, testerAuthHeader = null, proof = null, testerProofHeader = null, send = null, settingsWrite = null, projectFiles = null, projectFile = null, projectSave = null, libraryStatus = null, libraryRevert = null, chat = null, nameWrite = null, assistantStop = null, instancesWrite = null, routesWrite = null, agentWrite = null, proxyCall = null, processing = null, decisionWrite = null, userOptions = null, federation = null, log = line => console.log(line), fieldsFile = undefined } = {}) {
if (!isLoopback(host)) throw new Error('refusing to bind to non-loopback host'); if (!isLoopback(host)) throw new Error('refusing to bind to non-loopback host');
if (host === 'localhost') host = '127.0.0.1'; if (host === 'localhost') host = '127.0.0.1';
if (tester && preview !== null) throw new Error('tester listener cannot use preview mode'); if (tester && preview !== null) throw new Error('tester listener cannot use preview mode');
@@ -668,7 +671,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
// `userOptions` (FED R6, #1800) is `session => options`, the bus-backed options over that session // `userOptions` (FED R6, #1800) is `session => options`, the bus-backed options over that session
// user's own transport (console-bus.mjs). Only a signed-in listener has user sessions. // user's own transport (console-bus.mjs). Only a signed-in listener has user sessions.
if (userOptions !== null && (sso === null || typeof userOptions !== 'function')) throw new Error('user sessions need --sso'); if (userOptions !== null && (sso === null || typeof userOptions !== 'function')) throw new Error('user sessions need --sso');
const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files }; const owner = { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall, processing, upstream, files };
const hub = federationView(federation); const hub = federationView(federation);
// An object with none of the three is refused too: asking for the preview never serves without it. // An object with none of the three is refused too: asking for the preview never serves without it.
if (preview !== null && !(preview = previewOptions({ publicHost: preview.host, authHeader: preview.header, allowUser: preview.user }))) { if (preview !== null && !(preview = previewOptions({ publicHost: preview.host, authHeader: preview.header, allowUser: preview.user }))) {
@@ -678,7 +681,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso'); if (send !== null && ((preview === null && sso === null) || typeof send !== 'function')) throw new Error('--send needs the preview mode (--public-host, --auth-header and --allow-user) or --sso');
const server = createServer(async (req, res) => { const server = createServer(async (req, res) => {
// A granted user's request rebinds these below; every other request keeps the owner's. // A granted user's request rebinds these below; every other request keeps the owner's.
let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing, upstream, files } = owner, granted = false, chatGrants = []; let { bus, reads, send, settingsWrite, projectFiles, projectFile, projectSave, libraryStatus, libraryRevert, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, proxyCall, processing, upstream, files } = owner, granted = false, chatGrants = [];
res.setHeader('x-content-type-options', 'nosniff'); res.setHeader('x-content-type-options', 'nosniff');
res.setHeader('referrer-policy', 'no-referrer'); res.setHeader('referrer-policy', 'no-referrer');
// Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and // Under --sso the Sign out form's POST is redirected to the provider's end-session endpoint, and
@@ -745,7 +748,7 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' }); if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only', message: 'this part of the Console is the business owner\'s' });
if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' }); if (userOptions === null) return json(res, 503, { error: 'not-configured', message: 'the Console has no user transport' });
({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session)); ({ bus, send, settingsWrite, projectFiles, projectFile, projectSave, chat, nameWrite, assistantStop, instancesWrite, routesWrite, agentWrite, processing } = userOptions(session));
reads = null; upstream = null; libraryStatus = null; libraryRevert = null; files = USER_FILES; granted = true; reads = null; upstream = null; libraryStatus = null; libraryRevert = null; files = USER_FILES; granted = true; proxyCall = null;
chatGrants = sso.chatGrants?.(session) ?? []; chatGrants = sso.chatGrants?.(session) ?? [];
} }
} }
@@ -755,6 +758,15 @@ export async function startServer({ host = '127.0.0.1', port = 7330, board = DEF
if (!values || values.length !== 1 || values[0] !== preview.user) return json(res, 401, { error: 'sign-in required' }); if (!values || values.length !== 1 || values[0] !== preview.user) return json(res, 401, { error: 'sign-in required' });
} }
const writer = tester ? { tester: true, user: 'tester', origin: TESTER_ORIGIN } : signer; const writer = tester ? { tester: true, user: 'tester', origin: TESTER_ORIGIN } : signer;
// P4 page and verbs are owner-only, including reads and static page assets.
// This runs after owner sign-in and the granted-user fence, before generic
// tester writes or static-file serving. No localhost preview grants access.
if (path === '/proxy' || path === '/proxy.js' || path.startsWith('/api/proxy/')) {
if (tester || granted || !named) return json(res, 403, { error: 'human-required' });
if (path.startsWith('/api/proxy/')) return proxyHTTP(req, res, { path, call: proxyCall, named, tester, granted,
send, origin: sso ? sso.publicOrigin : `https://${preview.host}`, user: signer?.user, log });
}
// Row 252: Approve and Deny. The tester listener never offers them. Anywhere else without a signed-in // Row 252: Approve and Deny. The tester listener never offers them. Anywhere else without a signed-in
// session on the public name under --sso and --send, they are read-only. // session on the public name under --sso and --send, they are read-only.
if (decisionPath) { if (decisionPath) {
@@ -0,0 +1,12 @@
// Import-parser architecture guard: adding the management client to the Console
// would move its key/connection boundary out of the bus, and must fail this test.
import test from 'node:test';
import assert from 'node:assert/strict';
import { build } from '../node_modules/esbuild/lib/main.js';
import { fileURLToPath } from 'node:url';
test('Console production module graph cannot import the model-proxy client', async () => {
const result = await build({ entryPoints: [new URL('../src/cli.mjs', import.meta.url), new URL('../src/serve.mjs', import.meta.url),
new URL('../src/console-bus.mjs', import.meta.url)].map(fileURLToPath), bundle: true, packages: 'external',
platform: 'node', format: 'esm', splitting: true, outdir: '/unused-proxy-architecture', write: false, metafile: true, logLevel: 'silent' });
assert.equal(Object.keys(result.metafile.inputs).some(file => file.endsWith('/model-proxy/src/client.mjs')), false);
});
@@ -0,0 +1,77 @@
// Native page behavior. HTTP authorization is covered by proxy-page.test.mjs;
// this stand-in serves the real page/script and records only route names/counts.
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { readFileSync } from 'node:fs';
import { browser } from './browser.mjs';
async function setup(t, readonly = false) {
const state = { routes: [], loginStatus: 'wait', receivedKey: false, release: null, priority: 0 };
const server = http.createServer(async (req, res) => {
const send = value => { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(value)); };
if (req.url === '/api/config') return send({ send: !readonly });
if (req.url.startsWith('/api/proxy/')) {
state.routes.push(req.url);
const chunks = []; for await (const c of req) chunks.push(c);
const args = chunks.length ? JSON.parse(Buffer.concat(chunks)) : {};
if (req.url.endsWith('/accounts')) return send({ accounts: [{ accountId: 'a'.repeat(12), provider: 'claude', displayName: '<img src=x onerror="window.injected=true">', status: 'ready', priority: state.priority }] });
if (req.url.endsWith('/keys')) return send({ keys: [{ keyId: 'b'.repeat(12), management: 'sentinel', agent: null, issuedAt: null, state: null }] });
if (req.url.endsWith('/apikey/add')) {
state.receivedKey = args.key === 'p4-browser-key';
await new Promise(r => { state.release = r; }); return send({ added: true });
}
if (req.url.endsWith('/login/start')) return send({ url: 'https://claude.ai/oauth/authorize?state=p4-state', state: 'p4-state' });
if (req.url.endsWith('/login/status')) return send({ status: state.loginStatus });
if (req.url.endsWith('/login/submit')) return send({ status: 'wait' });
if (req.url.endsWith('/login/cancel')) return send({ status: 'cancelled' });
if (req.url.endsWith('/priority/set')) { state.priority = args.priority; return send({ priority: args.priority }); }
}
if (req.url === '/proxy') { res.setHeader('content-type', 'text/html'); return res.end(readFileSync(new URL('../src/public/proxy.html', import.meta.url))); }
if (req.url === '/proxy.js') { res.setHeader('content-type', 'text/javascript'); return res.end(readFileSync(new URL('../src/public/proxy.js', import.meta.url))); }
res.statusCode = 404; res.end();
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
t.after(() => { state.release?.(); return new Promise(r => server.close(r)); });
const b = await browser({ machine: false }); t.after(() => b.close());
await b.call('Page.addScriptToEvaluateOnNewDocument', { source: "window.openFeatures=null;window.open=(url,target,features)=>{window.openFeatures=features;return null;};" });
await b.navigate(`http://127.0.0.1:${server.address().port}/proxy`);
const wait = expression => b.evaluate(`(async()=>{for(let n=0;n<200;n++){if(${expression})return true;await new Promise(r=>setTimeout(r,25));}throw Error('proxy UI condition timed out');})()`);
await wait('document.querySelector("#proxy-message").textContent.includes("loaded")');
return { b, state, wait };
}
test('proxy browser clears write-only key immediately, sends once and renders data as text', { timeout: 60000 }, async t => {
const { b, state, wait } = await setup(t);
assert.equal(await b.evaluate('window.injected===true'), false);
assert.equal(await b.evaluate('document.querySelectorAll("#proxy-accounts img").length'), 0);
assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").type'), 'password');
assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").autocomplete'), 'off');
await b.evaluate('document.querySelector("#proxy-apikey input").value="p4-browser-key";document.querySelector("#proxy-apikey").requestSubmit();true');
await wait('document.querySelector("#proxy-apikey input").value===""');
for (let n = 0; n < 100 && !state.release; n++) await new Promise(r => setTimeout(r, 20));
assert.equal(state.receivedKey, true);
state.release(); await wait('document.querySelector("#proxy-message").textContent.includes("added")');
assert.equal(state.routes.filter(r => r.endsWith('/apikey/add')).length, 1);
assert.equal(await b.evaluate('localStorage.length+sessionStorage.length'), 0);
assert.equal(await b.evaluate('document.body.textContent.includes("p4-browser-key")'), false);
});
test('proxy browser OAuth opens safely, callback wait stays pending, status ok completes', { timeout: 60000 }, async t => {
const { b, state, wait } = await setup(t);
await b.evaluate('document.querySelector("#proxy-login-start").requestSubmit();true');
await wait('!document.querySelector("#proxy-authorize").hidden');
assert.equal(await b.evaluate('window.openFeatures'), 'noopener,noreferrer');
assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").rel'), 'noopener noreferrer');
await b.evaluate('document.querySelector("#proxy-login-submit input").value="http://localhost/callback?state=p4-state&code=p4-code";document.querySelector("#proxy-login-submit").requestSubmit();true');
await wait('document.querySelector("#proxy-login-message").textContent.includes("Waiting")');
assert.equal(await b.evaluate('document.querySelector("#proxy-login-message").textContent.includes("signed in")'), false);
assert.equal(await b.evaluate('document.querySelector("#proxy-login-submit input").value===""'), true);
state.loginStatus = 'ok';
await wait('document.querySelector("#proxy-login-message").textContent==="Account signed in."');
assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").hasAttribute("href")'), false);
assert.equal(await b.evaluate('localStorage.length+sessionStorage.length'), 0);
});
test('proxy browser read-only disables account changes', { timeout: 60000 }, async t => {
const { b, state } = await setup(t, true);
assert.equal(await b.evaluate('[...document.querySelectorAll("[data-write]")].every(e=>e.disabled)'), true);
assert.equal(await b.evaluate('document.querySelector("#proxy-accounts input").disabled'), true);
assert.equal(state.routes.some(r => r.endsWith('/apikey/add') || r.endsWith('/login/start')), false);
});
@@ -0,0 +1,67 @@
// P4 named mutant: API key echoed in a response, error, log or durable record.
// Real HTTP -> human capability -> broker -> page -> management client -> fake.
import test from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fakeManagement } from '../../bus/tests/proxy-page-fixture.mjs';
import { Store } from '../../bus/src/store.mjs';
import { Broker } from '../../bus/src/broker.mjs';
import { startServer } from '../src/serve.mjs';
const scan = root => readdirSync(root, { withFileTypes: true }).flatMap(e => e.isDirectory() ? scan(join(root, e.name)) : [readFileSync(join(root, e.name))]);
test('API-key canary reaches fake once and zero responses/logs/receipts/store files', async t => {
const canary = 'p4-canary-key';
const fake = await fakeManagement(t);
const root = mkdtempSync(join(tmpdir(), 'p4-canary-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args),
businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } });
const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
const logs = [], output = [];
const server = await startServer({ port: 0, preview: { host: 'console.example', header: 'x-user', user: 'owner' },
send: async () => ({}), proxyCall: (verb, args) => b.request(cap, { verb, args }), log: line => logs.push(line) });
t.after(() => new Promise(r => server.close(r)));
const call = (path, data) => new Promise((resolve, reject) => {
const req = request({ hostname: '127.0.0.1', port: server.address().port, path, method: data ? 'POST' : 'GET',
headers: { host: 'console.example', 'x-user': 'owner', origin: 'https://console.example', 'sec-fetch-site': 'same-origin', 'content-type': 'application/json' } }, res => {
let text = ''; res.on('data', c => text += c); res.on('end', () => { output.push(text); resolve(res.statusCode); });
}); req.on('error', reject); req.end(data ? JSON.stringify(data) : undefined);
});
// Capture process output during the operation as well as structured log
// callbacks. Even a deliberate log-leak mutant must never print its canary.
const stdout = process.stdout.write, stderr = process.stderr.write;
const capture = (chunk, ...rest) => { output.push(Buffer.from(chunk)); const cb = rest.at(-1); if (typeof cb === 'function') cb(); return true; };
let added, accounts;
try {
process.stdout.write = capture; process.stderr.write = capture;
added = await call('/api/proxy/apikey/add', { provider: 'z.ai', key: canary });
accounts = await call('/api/proxy/accounts');
} finally { process.stdout.write = stdout; process.stderr.write = stderr; }
assert.equal(added, 200); assert.equal(accounts, 200);
assert.equal(fake.state.receives, 1);
assert.equal(fake.state.groups[0].keys[0]['api-key'] === canary, true);
for (const bytes of [...output, ...logs, JSON.stringify(fake.state.receipts), ...scan(root)])
assert.equal(Buffer.from(bytes).includes(Buffer.from(canary)), false, 'credential value escaped');
});
test('OAuth URLs, state and code stay out of broker records and management receipts', async t => {
const fake = await fakeManagement(t);
const root = mkdtempSync(join(tmpdir(), 'p4-oauth-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args),
businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } });
const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
const call = (verb, args = {}) => b.request(cap, { verb, args });
const login = await call('proxy.login.start', { provider: 'claude' });
const callback = 'http://localhost/callback?state=' + login.state + '&code=p4-private-code';
assert.deepEqual(await call('proxy.login.submit', { redirect_url: callback }), { status: 'wait' });
assert.deepEqual(await call('proxy.login.status', { state: login.state }), { status: 'wait' });
for (const value of [login.url, login.state, callback, 'p4-private-code']) {
assert.equal(JSON.stringify(fake.state.receipts).includes(value), false);
for (const bytes of scan(root)) assert.equal(bytes.includes(Buffer.from(value)), false);
}
});
+115
View File
@@ -0,0 +1,115 @@
// P4: real HTTP boundary; mutant: proxy route moved ahead of granted-user fence.
import test from 'node:test';
import assert from 'node:assert/strict';
import { request } from 'node:http';
import { startServer } from '../src/serve.mjs';
import { consoleBus } from '../src/console-bus.mjs';
const preview = { host: 'console.example', header: 'x-user', user: 'owner' };
const headers = { host: preview.host, 'x-user': 'owner', origin: 'https://console.example',
'sec-fetch-site': 'same-origin', 'content-type': 'application/json' };
const routes = [
['accounts', 'GET', 'proxy.accounts', {}], ['keys', 'GET', 'proxy.keys', {}],
['login/start', 'POST', 'proxy.login.start', { provider: 'claude' }],
['login/submit', 'POST', 'proxy.login.submit', { redirect_url: 'http://localhost/callback?state=fixture&code=fake' }],
['login/status', 'POST', 'proxy.login.status', { state: 'fixture' }],
['login/cancel', 'POST', 'proxy.login.cancel', { state: 'fixture' }],
['apikey/add', 'POST', 'proxy.apikey.add', { provider: 'z.ai', key: 'p4-canary-key' }],
['priority/set', 'POST', 'proxy.priority.set', { accountId: 'a'.repeat(12), priority: 5 }],
];
const call = (s, path, method = 'GET', value, hs = headers) => new Promise((resolve, reject) => {
const req = request({ hostname: '127.0.0.1', port: s.address().port, path, method, headers: hs }, res => {
let text = ''; res.on('data', c => text += c); res.on('end', () => resolve({ status: res.statusCode, text }));
});
req.on('error', reject); req.end(value === undefined ? undefined : JSON.stringify(value));
});
async function setup(t, options = {}) {
const calls = [], logs = [];
const s = await startServer({ port: 0, preview, send: async () => ({}),
proxyCall: async (verb, args) => { calls.push([verb, args]); return { ok: true }; },
log: l => logs.push(l), ...options });
t.after(() => new Promise(r => s.close(r)));
return { s, calls, logs };
}
test('owner proxy routes each make exactly one allowlisted call', async t => {
const { s, calls, logs } = await setup(t);
for (const [path, method, verb, args] of routes) {
const r = await call(s, '/api/proxy/' + path, method, method === 'POST' ? args : undefined);
assert.equal(r.status, 200, path);
assert.deepEqual(calls.at(-1), [verb, args]);
}
assert.equal(calls.length, routes.length);
for (const value of ['p4-canary-key', 'fixture', 'redirect_url']) assert.equal(logs.join('\n').includes(value), false);
});
for (const [name, hs, code] of [
['signed-out', { ...headers, 'x-user': 'other' }, 401],
['loopback', { ...headers, host: '127.0.0.1' }, 403],
['cross-origin', { ...headers, origin: 'https://other.example' }, 403],
['missing Fetch Metadata', Object.fromEntries(Object.entries(headers).filter(([k]) => k !== 'sec-fetch-site')), 403],
]) test(`proxy write refuses ${name}`, async t => {
const { s, calls } = await setup(t);
assert.equal((await call(s, '/api/proxy/apikey/add', 'POST', { provider: 'z.ai', key: 'fixture-key' }, hs)).status, code);
assert.deepEqual(calls, []);
});
test('all proxy writes are read-only without --send', async t => {
const { s, calls } = await setup(t, { send: null });
for (const [path, method, , args] of routes.filter(r => r[1] === 'POST'))
assert.equal((await call(s, '/api/proxy/' + path, method, args)).status, 403, path);
assert.deepEqual(calls, []);
});
test('granted user never reaches owner page or any proxy route', async t => {
const sso = { publicHost: preview.host, publicOrigin: headers.origin, formTargets: () => [],
session: () => ({ name: 'Alice', user: 'alice' }) };
const { s, calls } = await setup(t, { preview: null, sso, userOptions: () => { throw Error('must not bind'); } });
assert.equal((await call(s, '/proxy')).status, 403);
for (const [path, method, , args] of routes)
assert.equal((await call(s, '/api/proxy/' + path, method, method === 'POST' ? args : undefined)).status, 403, path);
assert.deepEqual(calls, []);
});
test('native page offers write-only key input and no panel script', async t => {
const { s } = await setup(t);
const r = await call(s, '/proxy');
assert.equal(r.status, 200);
assert.match(r.text, /type="password"/);
assert.match(r.text, /autocomplete="off"/);
assert.doesNotMatch(r.text, /management\.html|iframe/);
});
test('Console caller rejects every non-proxy verb', async () => {
const calls = [];
const o = consoleBus(async (...a) => { calls.push(a); return {}; }, { send: true });
assert.equal(typeof o.proxyCall, 'function');
await assert.rejects(o.proxyCall('message.send', {}));
assert.deepEqual(calls, []);
});
test('tester socket refuses proxy page, reads and writes before any owner call', async t => {
const { mkdtempSync, rmSync } = await import('node:fs');
const { tmpdir } = await import('node:os');
const { join } = await import('node:path');
const { startTesterSocket } = await import('../src/tester-socket.mjs');
const { testerRequest } = await import('../../cli/src/tester-http.mjs');
const root = mkdtempSync(join(tmpdir(), 'p4-tester-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
let calls = 0;
const s = await startTesterSocket({ dataRoot: root, proxyCall: async () => { calls++; return {}; }, log: () => {} });
t.after(() => new Promise(r => s.close(r)));
const page = await testerRequest({ dataRoot: root, path: '/proxy' });
assert.equal(page.status, 403);
for (const [path, method, , args] of routes) {
const r = await testerRequest({ dataRoot: root, path: '/api/proxy/' + path, method, body: method === 'POST' ? args : undefined });
assert.equal(r.status, 403, path);
}
assert.equal(calls, 0);
});
test('malformed API-key JSON never appears in errors or logs', async t => {
const { s, calls, logs } = await setup(t);
const canary = 'p4-canary-key';
const r = await new Promise((resolve, reject) => {
const req = request({ hostname: '127.0.0.1', port: s.address().port, path: '/api/proxy/apikey/add', method: 'POST', headers }, res => {
let text = ''; res.on('data', c => text += c); res.on('end', () => resolve({ status: res.statusCode, text }));
}); req.on('error', reject); req.end('{"key":"' + canary);
});
assert.equal(r.status, 400);
assert.equal(r.text.includes(canary), false); assert.equal(logs.join('\n').includes(canary), false);
assert.deepEqual(calls, []);
});
+1 -1
View File
@@ -353,7 +353,7 @@ async function validate(document, file) {
const { loadProxy } = await import("../packages/model-proxy/src/settings.mjs"); const { loadProxy } = await import("../packages/model-proxy/src/settings.mjs");
proxy = loadProxy(path.dirname(file), dataRoot); proxy = loadProxy(path.dirname(file), dataRoot);
} }
} catch { fail(2, "proxy-config-invalid"); } } catch (error) { fail(2, ['proxy-rotate-grace-invalid', 'proxy-first-output-invalid'].includes(error.code) ? error.code : "proxy-config-invalid"); }
return { return {
configVersion: document.configVersion, configVersion: document.configVersion,