@jc_stack/ez-agents 0.1.0-beta.46 → 0.1.0-beta.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +2 -0
- package/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +1 -1
- package/Dockerfile +13 -9
- package/docker/engine-pnpm-workspace.yaml +4 -0
- package/docker/smoke.mjs +1 -1
- package/docs/architecture/ai-selection.md +8 -2
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/development-and-testing.md +1 -1
- package/docs/docker-runtime.md +10 -0
- package/docs/plugin-guide.md +1 -1
- package/docs/plugins.md +1 -1
- package/docs/releasing.md +1 -1
- package/docs/scheduling.md +17 -3
- package/docs/upgrades.md +3 -1
- package/package.json +4 -3
- package/pnpm-workspace.yaml +7 -0
- package/scripts/smoke-scheduler.ts +4 -1
- package/src/ai.ts +20 -2
- package/src/delivery-socket.ts +15 -1
- package/src/host-executor.ts +5 -0
- package/src/index.ts +2 -0
- package/src/install-tools.mjs +11 -5
- package/src/model-policy.ts +4 -0
- package/src/runs.ts +4 -1
- package/src/schedule-cli.ts +12 -3
- package/src/scheduler.ts +28 -3
- package/src/task-executor.ts +2 -2
- package/src/updates/runtime.mjs +7 -4
- package/src/updates/supervisor.mjs +23 -13
- package/test/ai.test.ts +45 -5
- package/test/channel-owner.test.ts +2 -2
- package/test/desktop-bridge.test.ts +2 -1
- package/test/failure.test.ts +4 -4
- package/test/host-executor.test.ts +4 -4
- package/test/install-tools.test.mjs +6 -5
- package/test/native-tasks.test.ts +1 -1
- package/test/schedule-cli.test.ts +47 -3
- package/test/scheduled-tasks.test.ts +2 -2
- package/test/scheduler-host.test.ts +14 -3
- package/test/scheduler-relay.test.ts +47 -2
- package/test/scheduler.test.ts +21 -1
- package/test/tasks.test.ts +1 -1
- package/test/updates.test.mjs +32 -9
package/test/updates.test.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { perform, environment, packageManager, backupStateDirectory, execute } f
|
|
|
13
13
|
import { atomic, snapshot, compose, prepareCommand } from '../src/plugins/manager.mjs';
|
|
14
14
|
import { bindUpdates } from '../src/updates/binding.mjs';
|
|
15
15
|
import { status as runtimeStatus } from '../src/updates/status.mjs';
|
|
16
|
-
import { providerEnvironment, queueAutomatic, waitForHostHeartbeat } from '../src/updates/supervisor.mjs';
|
|
16
|
+
import { providerEnvironment, queueAutomatic, waitForHostHeartbeat, withIdleUpgrade } from '../src/updates/supervisor.mjs';
|
|
17
17
|
const exec=promisify(execFile);
|
|
18
18
|
|
|
19
19
|
test('structured runtime output excludes warnings but failures retain diagnostics',async()=>{
|
|
@@ -113,10 +113,10 @@ test('root preparation leaves private plugin updates readable by the installer',
|
|
|
113
113
|
const script=`const fs=require('fs');fs.readFileSync(${JSON.stringify(path.join(dir,'job.json'))});fs.readFileSync(${JSON.stringify(path.join(dir,'candidate.tgz'))});`;
|
|
114
114
|
await exec('setpriv',['--reuid=20001','--regid=20002','--clear-groups','--no-new-privs',process.execPath,'-e',script]);
|
|
115
115
|
});
|
|
116
|
-
function runtime(f,{fail,stopped=false,buildArgs={CODEX_CLI_VERSION:'0.156.1'}}={}) {
|
|
116
|
+
function runtime(f,{fail,stopped=false,buildArgs={CODEX_CLI_VERSION:'0.156.1'},latest='0.160.0'}={}) {
|
|
117
117
|
const calls=[];let failed=false;
|
|
118
118
|
const execute=async(command,args,opts)=>{
|
|
119
|
-
calls.push([command,...args]);if(fail&&!failed&&fail(command,args)){failed=true;throw Error('Synthetic failure');}
|
|
119
|
+
calls.push([command,...args]);if(args.includes('view'))return JSON.stringify(latest);if(fail&&!failed&&fail(command,args)){failed=true;throw Error('Synthetic failure');}
|
|
120
120
|
if(args.at(-1)==='--version')return '10.30.3';
|
|
121
121
|
if(args.includes('config'))return JSON.stringify({services:{relay:{build:{args:buildArgs}}}});
|
|
122
122
|
if(args.includes('ps'))return stopped?'':'container-id';
|
|
@@ -306,7 +306,7 @@ test('main transaction stages before stopping, pins rollback image, preserves st
|
|
|
306
306
|
assert.notEqual(await fs.readFile(path.join(active,'bin/example.mjs'),'utf8'),'tampered');
|
|
307
307
|
assert(r.calls.findIndex(c=>c.includes('build'))<r.calls.findIndex(c=>c[0]==='stopHost'));
|
|
308
308
|
const build=r.calls.find(c=>c[0]==='docker'&&c[1]==='build');
|
|
309
|
-
assert.equal(build[build.indexOf('--build-arg')+1],'CODEX_CLI_VERSION=0.
|
|
309
|
+
assert.equal(build[build.indexOf('--build-arg')+1],'CODEX_CLI_VERSION=0.160.0');
|
|
310
310
|
const status=await command(f.home,['status']);assert(!JSON.stringify(status).includes('private-test-token'));assert(!('rollback'in status.jobs[0]));
|
|
311
311
|
assert.equal((await fs.stat(path.join(jobPath(f.home,job.id),'job.json'))).mode&0o777,0o600);
|
|
312
312
|
});
|
|
@@ -320,12 +320,14 @@ test('main activation and rollback bind guidance from the selected package root'
|
|
|
320
320
|
assert.equal((await perform(failed.home,await queued(failed),runtime(failed,{fail:(_command,args)=>args.includes('up')}))).status,'rolled-back');
|
|
321
321
|
assert.equal(await fs.readFile(path.join(failed.agent.workspace,'AGENTS.md'),'utf8'),'old guidance\n');
|
|
322
322
|
});
|
|
323
|
-
test('
|
|
323
|
+
test('latest overrides stale deployment pins and changing latest changes image bindings',async t=>{
|
|
324
324
|
const tags=[],hashes=[];
|
|
325
325
|
for(const pin of ['0.153.4','0.156.1']) {
|
|
326
|
-
const f=await fixture(t),job=await queued(f),r=runtime(f,{buildArgs:{CODEX_CLI_VERSION:pin}
|
|
326
|
+
const f=await fixture(t),job=await queued(f),r=runtime(f,{buildArgs:{CODEX_CLI_VERSION:'0.153.4'},latest:pin});
|
|
327
327
|
assert.equal((await perform(f.home,job,r)).status,'completed');
|
|
328
328
|
const build=r.calls.find(c=>c[0]==='docker'&&c[1]==='build'),tag=build[build.indexOf('-t')+1];
|
|
329
|
+
assert.ok(build.includes(`CODEX_CLI_VERSION=${pin}`));
|
|
330
|
+
assert.equal((await read(path.join(jobPath(f.home,job.id),'job.json'))).codexVersion,pin);
|
|
329
331
|
assert.match(await fs.readFile(path.join(f.config.deploymentDir,'docker.env'),'utf8'),new RegExp(tag));
|
|
330
332
|
tags.push(tag);hashes.push(job.sha256);
|
|
331
333
|
}
|
|
@@ -402,7 +404,7 @@ test('isolated plugin replacement refreshes broker version inventory after regis
|
|
|
402
404
|
test('plugin update preserves a created but never running service as stopped',async t=>{
|
|
403
405
|
const f=await fixture(t,'plugin'),job=await queued(f),r=runtime(f),execute=r.execute;
|
|
404
406
|
r.execute=async(command,args,options)=>{
|
|
405
|
-
if(args.includes('ps')) {r.calls.push([command,...args]);return args.includes('--status')?'':'created-container';}
|
|
407
|
+
if(args.includes('ps')) {r.calls.push([command,...args]);if(args.includes('view'))return JSON.stringify(latest);return args.includes('--status')?'':'created-container';}
|
|
406
408
|
return execute(command,args,options);
|
|
407
409
|
};
|
|
408
410
|
assert.equal((await perform(f.home,job,r)).status,'completed');
|
|
@@ -495,7 +497,7 @@ for(const provider of ['pnpm','corepack']) test(`supervisor with only ${provider
|
|
|
495
497
|
// Package archives never contain node_modules; the fake pnpm below provisions the fixture loader.
|
|
496
498
|
await fs.rm(path.join(f.source,'node_modules'),{recursive:true});
|
|
497
499
|
const log=path.join(f.root,'commands.jsonl');
|
|
498
|
-
await fs.writeFile(path.join(fake,provider),`#!${process.execPath}\nif(${JSON.stringify(provider)}==='corepack'&&process.argv[2]!=='pnpm@10.30.3')throw Error('Unpinned manager');if(process.argv.includes('--version')){console.log('10.30.3');process.exit(0)}const fs=require('fs');fs.mkdirSync('node_modules/tsx/dist',{recursive:true});fs.writeFileSync('node_modules/tsx/dist/loader.mjs','');`,{mode:0o755});
|
|
500
|
+
await fs.writeFile(path.join(fake,provider),`#!${process.execPath}\nif(${JSON.stringify(provider)}==='corepack'&&process.argv[2]!=='pnpm@10.30.3')throw Error('Unpinned manager');if(process.argv.includes('view')){console.log(JSON.stringify('0.160.0'));process.exit(0)}if(process.argv.includes('--version')){console.log('10.30.3');process.exit(0)}const fs=require('fs');fs.mkdirSync('node_modules/tsx/dist',{recursive:true});fs.writeFileSync('node_modules/tsx/dist/loader.mjs','');`,{mode:0o755});
|
|
499
501
|
await fs.writeFile(path.join(fake,'docker'),`#!${process.execPath}\nconst fs=require('fs');const a=process.argv.slice(2);fs.appendFileSync(${JSON.stringify(log)},JSON.stringify(a)+'\\n');if(a.includes('config'))console.log(JSON.stringify({services:{relay:{build:{args:{}}}}}));if(a.includes('ps'))console.log('cid');if(a[0]==='inspect')console.log('sha256:'+'a'.repeat(64));`,{mode:0o755});
|
|
500
502
|
const wrapper=path.join(f.root,'supervisor.mjs'),module=new URL('../src/updates/supervisor.mjs',import.meta.url).href;
|
|
501
503
|
await fs.writeFile(wrapper,`import {supervise} from ${JSON.stringify(module)};const a=new AbortController();process.on('SIGTERM',()=>a.abort());await supervise(${JSON.stringify(f.config.deploymentDir)},a.signal,{discover:async()=>{${provider==='pnpm' ? "throw Error('Synthetic discovery failure')" : 'return []'}}});`);
|
|
@@ -510,6 +512,7 @@ for(const provider of ['pnpm','corepack']) test(`supervisor with only ${provider
|
|
|
510
512
|
const requester=await exec(process.execPath,['--input-type=module','-e',`import {submit} from ${JSON.stringify(new URL('../src/updates/control.mjs',import.meta.url).href)};console.log(JSON.stringify(await submit(${JSON.stringify(f.home)},${JSON.stringify(job.id)},false)));`]);
|
|
511
513
|
assert.equal(JSON.parse(requester.stdout).status,'queued');
|
|
512
514
|
await new Promise(r=>setTimeout(r,1800));assert.equal((await read(path.join(jobPath(f.home,job.id),'job.json'))).status,'queued');
|
|
515
|
+
await assert.rejects(fs.readFile(path.join(f.agent.controlDir,'upgrade-pause.json')),e=>e.code==='ENOENT');
|
|
513
516
|
await fs.rm(running);
|
|
514
517
|
await wait(async()=>{const j=await read(path.join(jobPath(f.home,job.id),'job.json'));if(j.status==='failed'||j.status==='rolled-back')throw Error(JSON.stringify(j)+first.output());return j.status==='completed';});
|
|
515
518
|
const newBeat=await heartbeat();assert.notEqual(newBeat.pid,oldBeat.pid);assert.equal(await firstClosed,0,first.output());
|
|
@@ -529,6 +532,26 @@ for(const provider of ['pnpm','corepack']) test(`supervisor with only ${provider
|
|
|
529
532
|
const closed3=new Promise(r=>third.p.once('close',r));third.p.kill('SIGTERM');assert.equal(await closed3,0,third.output());
|
|
530
533
|
});
|
|
531
534
|
|
|
535
|
+
test('queued upgrades keep admission open while busy and release a raced or aborted pause',async t=>{
|
|
536
|
+
const root=await fs.mkdtemp(path.join(tmpdir(),'ez-upgrade-admission-'));
|
|
537
|
+
t.after(()=>fs.rm(root,{recursive:true,force:true}));
|
|
538
|
+
const pause=path.join(root,'upgrade-pause.json'),job={id:'test'},abort=new AbortController();
|
|
539
|
+
const absent=()=>assert.rejects(fs.readFile(pause),e=>e.code==='ENOENT');
|
|
540
|
+
let applied=0;
|
|
541
|
+
const apply=async()=>{assert.equal((await read(pause)).id,job.id);applied++;return 'applied';};
|
|
542
|
+
await withIdleUpgrade(root,job,abort.signal,apply,{isIdle:async()=>false});
|
|
543
|
+
await absent();assert.equal(applied,0);
|
|
544
|
+
let checks=0;
|
|
545
|
+
await withIdleUpgrade(root,job,abort.signal,apply,{isIdle:async()=>++checks===1,wait:async()=>{}});
|
|
546
|
+
await absent();assert.equal(applied,0);
|
|
547
|
+
assert.equal(await withIdleUpgrade(root,job,abort.signal,apply,{isIdle:async()=>true,wait:async()=>{}}),'applied');
|
|
548
|
+
await absent();assert.equal(applied,1);
|
|
549
|
+
await assert.rejects(withIdleUpgrade(root,job,abort.signal,async()=>{throw Error('apply failed');},{isIdle:async()=>true,wait:async()=>{}}),/apply failed/);
|
|
550
|
+
await absent();
|
|
551
|
+
await withIdleUpgrade(root,job,abort.signal,apply,{isIdle:async()=>true,wait:async()=>abort.abort()});
|
|
552
|
+
await absent();assert.equal(applied,1);
|
|
553
|
+
});
|
|
554
|
+
|
|
532
555
|
test('npm candidates verify exact version and integrity; automatic policy is enforced again at execution',async t=>{
|
|
533
556
|
const f=await fixture(t),data=await fs.readFile(await f.pack()),original=globalThis.fetch;
|
|
534
557
|
const {createHash}=await import('node:crypto');
|
|
@@ -627,7 +650,7 @@ for(const mode of ['exact','wrong-image','unhealthy']) test(`recovery reuses onl
|
|
|
627
650
|
const f=await fixture(t),job=await queued(f),r=runtime(f);await perform(f.home,job,r);
|
|
628
651
|
const interrupted=await read(path.join(jobPath(f.home,job.id),'job.json'));interrupted.status='applying';
|
|
629
652
|
const calls=[],execute=async(command,args,options)=>{
|
|
630
|
-
calls.push([command,...args]);
|
|
653
|
+
calls.push([command,...args]);if(args.includes('view'))return JSON.stringify(latest);
|
|
631
654
|
if(args.includes('ps'))return 'b'.repeat(64);
|
|
632
655
|
if(args[0]==='inspect'&&args.includes('{{json .}}'))return JSON.stringify({Image:'sha256:'+(mode==='wrong-image'?'c':'a').repeat(64),State:{Running:true,Health:{Status:mode==='unhealthy'?'unhealthy':'healthy'}}});
|
|
633
656
|
if(mode==='exact'&&args.includes('up'))throw Error('Pinned image index no longer exists');
|