88 lines
14 KiB
JavaScript
88 lines
14 KiB
JavaScript
import {ownerConsole} from './hire-fixtures.mjs';
|
|
// #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 { busOrganization } from '../src/organization-context.mjs';
|
|
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../organization/tests/helpers.mjs';
|
|
import { loadBusiness } from '../../organization/src/organization-config.mjs';
|
|
import { resolveInstance } from '../../organization/src/resolve.mjs';
|
|
import { organizationWriter } from '../../organization/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\n## Authority\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:busOrganization(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(ownerConsole(bus.broker),{verb:'hire.start.console',args:{op:'fire',level:'org',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');}};
|
|
}
|