fix: account for processing controls in image inputs and launch fixtures
This commit is contained in:
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);
|
||||
|
||||
@@ -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'){
|
||||
|
||||
Reference in new issue
Block a user