diff --git a/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts b/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts index f9c5fc3..2cb058a 100644 --- a/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts +++ b/packages/mosaic/src/mutator-gate/mutator-gate.acceptance.spec.ts @@ -22,6 +22,7 @@ interface BrokerPaths { const frameworkRoot = new URL('../../framework/', import.meta.url).pathname; const daemonPath = join(frameworkRoot, 'tools/lease-broker/daemon.py'); const gatePath = join(frameworkRoot, 'tools/lease-broker/mutator-gate.py'); +const launcherPath = join(frameworkRoot, 'tools/lease-broker/launch-runtime.py'); const claudeSettingsPath = join(frameworkRoot, 'runtime/claude/settings.json'); const piExtensionPath = join(frameworkRoot, 'runtime/pi/mosaic-extension.ts'); const children: ChildProcess[] = []; @@ -288,6 +289,51 @@ describe('whole mutator-class lease gate', () => { }); }); + test('runtime launcher anchors broker identity before exec and fails closed without broker', async () => { + const { socket } = await startBroker(); + const probe = [ + 'import json,os,socket', + 's=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM)', + "s.connect(os.environ['MOSAIC_LEASE_BROKER_SOCKET'])", + "request={'action':'authorize_tool','session_id':os.environ['MOSAIC_LEASE_SESSION_ID'],'runtime_generation':int(os.environ['MOSAIC_RUNTIME_GENERATION']),'runtime':'claude','tool_name':'Read'}", + "s.sendall((json.dumps(request)+'\\n').encode())", + 's.shutdown(socket.SHUT_WR)', + "print(json.dumps({'session_id':os.environ['MOSAIC_LEASE_SESSION_ID'],'reply':json.loads(s.recv(65536))}))", + ].join(';'); + const launched = spawnSync( + 'python3', + [launcherPath, '--runtime', 'claude', '--', 'python3', '-c', probe], + { + encoding: 'utf8', + env: { + ...process.env, + MOSAIC_LEASE_BROKER_SOCKET: socket, + MOSAIC_RUNTIME_GENERATION: '1', + }, + }, + ); + expect(launched.status, launched.stderr).toBe(0); + expect(JSON.parse(launched.stdout)).toMatchObject({ + session_id: expect.stringMatching(/^[a-f0-9]{64}$/), + reply: { ok: true, decision: 'allow' }, + }); + + const unavailable = spawnSync( + 'python3', + [launcherPath, '--runtime', 'claude', '--', 'python3', '-c', "print('EXECUTED')"], + { + encoding: 'utf8', + env: { + ...process.env, + MOSAIC_LEASE_BROKER_SOCKET: join(tmpdir(), 'missing-mosaic-broker.sock'), + MOSAIC_RUNTIME_GENERATION: '1', + }, + }, + ); + expect(unavailable.status).not.toBe(0); + expect(unavailable.stdout).not.toContain('EXECUTED'); + }); + test('Claude and Pi runtime adapters consult the broker for every tool class', async () => { const { socket } = await startBroker(); const sessionId = await register(socket);