fix: account for processing controls in image inputs and launch fixtures

This commit is contained in:
rocko committed 2026-10-21 19:32:38 -05:00
1 parent c78f48946a
commit eb3abba12f
6 files changed
+38 -3

No files matched your search

@@ -3,10 +3,18 @@ import { mkdtempSync,rmSync,writeFileSync,mkdirSync,cpSync,readFileSync,statSync
import { spawnSync } from 'node:child_process';
const image=await import('../../../scripts/testing/lane-image.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
const docker=await import('../../../scripts/testing/lane-docker.mjs').catch(e=>{if(e.code!=='ERR_MODULE_NOT_FOUND')throw e;return {};});
const processingInputs=['packages/processing-control/src/state.mjs','packages/processing-control/src/entrypoint.mjs'];
function copyProcessingInputs(root){
mkdirSync(join(root,'packages/processing-control/src'),{recursive:true});
for(const file of processingInputs)cpSync(new URL('../../../'+file,import.meta.url),join(root,file));
}
test('a changed COPY input or actual image ID refuses compatible image sharing',t=>{
assert.equal(typeof image.imageFingerprint,'function','image fingerprint required');
const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true}));
for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f));
copyProcessingInputs(root);
writeFileSync(join(root,'RELEASE'),'0.0.1\n');
for(const d of ['contracts','src','adapters']){mkdirSync(join(root,d));writeFileSync(join(root,d,'one'),'same');}
const digest=image.imageFingerprint(root),id='sha256:'+'a'.repeat(64);const receipt={inputDigest:digest,imageId:id,tag:'mosaic-poc-agent:0.85.1-r0.0.1',baseDigest:'node@sha256:'+'b'.repeat(64),platform:'linux/amd64'};
@@ -25,6 +33,7 @@ test('exclusive image preparation records actual image ID and build wall time an
assert.equal(typeof image.prepareImage,'function','prepareImage is required');
const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true}));
for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f));
copyProcessingInputs(root);
writeFileSync(join(root,'RELEASE'),'0.0.1\n');for(const d of ['contracts','src','adapters'])mkdirSync(join(root,d));
const calls=[];const invoke=(args)=>{calls.push(args);if(args[0]==='compose')return '';return JSON.stringify(args[2]==='node:24-bookworm-slim'?{RepoDigests:['node@sha256:'+'b'.repeat(64)]}:{Id:'sha256:'+'a'.repeat(64),Os:'linux',Architecture:'amd64'});};
assert.throws(()=>image.prepareImage(root,{grant:'',tempRoot:root},invoke),/grant/);
@@ -40,6 +49,7 @@ test('cleanup refuses a foreign Docker label and removes only an exact owned pro
function imageFixture(t){
const root=mkdtempSync('/mnt/storage/scratch/gi-');t.after(()=>rmSync(root,{recursive:true,force:true}));
for(const f of ['Containerfile','package.json','package-lock.json'])cpSync(new URL('../../../'+f,import.meta.url),join(root,f));
copyProcessingInputs(root);
writeFileSync(join(root,'RELEASE'),'0.0.1\n');for(const d of ['contracts','src','adapters'])mkdirSync(join(root,d));
const receipt={inputDigest:image.imageFingerprint(root),imageId:'sha256:'+'a'.repeat(64),tag:'mosaic-poc-agent:0.85.1-r0.0.1',baseDigest:'node@sha256:'+'b'.repeat(64),platform:'linux/amd64'};
return {root,receipt};
@@ -134,3 +144,12 @@ test('guard owns every Compose file argument and refuses caller configuration ov
for(const args of [['compose','-f','foreign.yaml','run','mosaic-agent'],['compose','--project-directory','/tmp','run','mosaic-agent']])
assert.throws(()=>docker.guardDocker(args,run),/unowned command/);
});
// Row 191 image-input manifest: either copied reader must invalidate the pinned image.
for(const file of processingInputs)test(`changing ${file} invalidates the image receipt`,t=>{
const {root,receipt}=imageFixture(t);
const inspect=()=>({Id:receipt.imageId,Os:'linux',Architecture:'amd64'});
assert.equal(image.verifyImage(receipt,root,inspect).imageId,receipt.imageId);
writeFileSync(join(root,file),readFileSync(join(root,file),'utf8')+'\n// changed fixture input\n');
assert.throws(()=>image.verifyImage(receipt,root,inspect),/image inputs differ/);
});
@@ -6,6 +6,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {createLauncher} from '../src/launcher.mjs';
import {initializeControl} from '../../processing-control/src/state.mjs';
import {fixture,tmp} from './helpers.mjs';
import {launchLogFile,SESSION_CREDENTIALS} from '../../seat/src/session.mjs';
@@ -19,6 +20,7 @@ function setup(t,{harness='claude-code',account=ACCOUNT,set=['CLAUDE_CODE_OAUTH_
if(account)doc.launch.account=account;
return doc;
}),ops=[];
initializeControl(f.dataRoot);
for(const k of SESSION_CREDENTIALS.flat())delete f.env[k];
for(const k of set)f.env[k]=VALUE;
const host={business:'acme',beforeClose:()=>{},op:async x=>{
@@ -6,14 +6,16 @@ import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {fileURLToPath} from 'node:url';
import {createLauncher} from '../src/launcher.mjs';
import {initializeControl} from '../../processing-control/src/state.mjs';
import {fixture,tmp} from './helpers.mjs';
import {launchLogFile} from '../../seat/src/session.mjs';
const HIT={path:['tokens','instances','coder'],limit:100,count:120,unknownTurns:1};
// answer is what the mayLaunch op returns, or an Error it throws.
function setup(t,answer){
function setup(t,answer,{initialize=true}={}){
const root=tmp(t),f=fixture(root,'acme',doc=>{doc.roles.coder.vars.model='claude-sonnet-5-5';return doc;}),ops=[];
if(initialize)initializeControl(f.dataRoot);
const host={business:'acme',beforeClose:()=>{},op:async x=>{
ops.push(x);
if(x.op==='identity')return {business:'acme',role:'pm'};
@@ -78,3 +80,11 @@ test('REQ-LIMIT-4 the launcher reaches token-limit only through the bus: launche
assert.doesNotMatch(src,/modules\(\s*["'](?!harness["'])/);
assert.doesNotMatch(src,/\bimport\s*\(/);
});
// Row 191: only trusted bus startup initializes control; an early seat launch refuses.
test('a launcher before trusted control initialization refuses processing-uninitialized',async t=>{
const {launcher,ops}=setup(t,{allow:true},{initialize:false});
assert.deepEqual(await launcher.fromSession('fixture-cap','coder'),{ok:false,error:'processing-uninitialized'});
assert.equal(ops.some(x=>x.op==='mayLaunch'||x.op==='authorizeLaunch'),false);
assert.deepEqual(launcher.registry.sessions,[]);
});
@@ -4,11 +4,13 @@ import assert from 'node:assert/strict';
import {join} from 'node:path';
import {readFileSync,writeFileSync} from 'node:fs';
import {createLauncher} from '../src/launcher.mjs';
import {initializeControl} from '../../processing-control/src/state.mjs';
import {fixture,tmp} from './helpers.mjs';
import {systemFor,writeJson} from '../../business/tests/helpers.mjs';
import {launchLogFile} from '../../seat/src/session.mjs';
test('each launch rereads limits and model selections; lowered limits keep sessions and log exact hit',async t=>{
const root=tmp(t),f=fixture(root,'acme',doc=>{doc.roles.coder.vars.model='claude-sonnet-5-5';return doc;}),file=join(root,'config/businesses/acme.json'),ops=[];
initializeControl(f.dataRoot);
const host={business:'acme',beforeClose:()=>{},op:async x=>{ops.push(x);if(x.op==='identity')return {business:'acme',role:'pm'};if(x.op==='mayLaunch')return {allow:true};if(x.op==='authorizeLaunch')throw Object.assign(Error('fixture authorization stop'),{code:'fixture-policy'});return {};}};
const launcher=createLauncher({host,system:JSON.parse(readFileSync(f.config,'utf8')),env:f.env,bwrap:'/usr/bin/true',log:()=>{}});
const session={business:'acme',instance:'reviewer',run:'fixture-existing',harness:'pi',provider:'zai',model:'claude-sonnet-5-5'};launcher.registry.add(session);
@@ -7,6 +7,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {readFileSync} from 'node:fs';
import {createLauncher} from '../src/launcher.mjs';
import {initializeControl} from '../../processing-control/src/state.mjs';
import {fakeHarnesses,fixture,tmp} from './helpers.mjs';
import {launchLogFile,SESSION_CREDENTIALS} from '../../seat/src/session.mjs';
@@ -25,6 +26,7 @@ const ON=['pi','claude-code','down','sparse','metered','broken','throws','absent
// vars are the coder's whole agent vars; edit changes the rest of the file.
async function setup(t,vars,{on=ON,edit=doc=>doc,env:more={}}={}){
const root=tmp(t),f=fixture(root,'acme',doc=>{doc.roles.coder.vars=vars;doc.plugins={...doc.plugins,harness:on};return edit(doc);}),ops=[];
initializeControl(f.dataRoot);
Object.assign(f.env,more);
const host={business:'acme',beforeClose:()=>{},op:async x=>{
ops.push(x);
+2 -2
View File
@@ -6,11 +6,11 @@ import { refuse } from './lane-state.mjs';
export function imageFingerprint(checkout){
const dockerfile=readFileSync(join(checkout,'Containerfile'),'utf8');
const copies=dockerfile.split('\n').filter(l=>/^\s*(COPY|ADD)\s/i.test(l)).map(l=>l.trim());
const expected=['COPY package.json package-lock.json ./','COPY contracts /opt/mosaic/contracts','COPY src /opt/mosaic/src','COPY adapters /opt/mosaic/adapters'];
const expected=['COPY package.json package-lock.json ./','COPY contracts /opt/mosaic/contracts','COPY src /opt/mosaic/src','COPY packages/processing-control/src/state.mjs packages/processing-control/src/entrypoint.mjs /opt/mosaic/processing-control/','COPY adapters /opt/mosaic/adapters'];
if(JSON.stringify(copies)!==JSON.stringify(expected))refuse('image COPY inputs changed; manifest review required');
const hash=createHash('sha256');
const visit=rel=>{const path=join(checkout,rel),st=lstatSync(path);if(st.isSymbolicLink())refuse('image input is a symlink');hash.update(JSON.stringify([rel,st.mode&0o777,st.isDirectory()?'d':'f'])+'\n');if(st.isDirectory())for(const n of readdirSync(path).sort())visit(`${rel}/${n}`);else if(st.isFile())hash.update(readFileSync(path));else refuse('unsupported image input');};
for(const rel of ['Containerfile','package.json','package-lock.json','contracts','src','adapters'])visit(rel);
for(const rel of ['Containerfile','package.json','package-lock.json','contracts','src','adapters','packages/processing-control/src/state.mjs','packages/processing-control/src/entrypoint.mjs'])visit(rel);
return hash.digest('hex');
}
export function inspectImage(tag,docker=process.env.MOSAIC_GATE_REAL_DOCKER??'docker'){