@jc_stack/ez-agents 0.1.0-beta.47 → 0.1.0-beta.49
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/.env.example +3 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +30 -0
- package/Dockerfile +5 -1
- package/THIRD_PARTY_NOTICES.md +6 -3
- package/bin/ez +7 -0
- package/docker/broker-readiness.mjs +54 -0
- package/docker/broker-update-smoke.mjs +100 -0
- package/docker/run.ts +1 -1
- package/docker/upgrade-smoke.mjs +1 -1
- package/docs/application-channel.md +32 -1
- package/docs/architecture/ai-selection.md +23 -4
- package/docs/docker-runtime.md +57 -0
- package/docs/isolated-plugin-update-readiness.md +66 -0
- package/docs/releasing.md +4 -0
- package/docs/scheduling.md +141 -6
- package/docs/selective-monitoring.md +13 -0
- package/docs/upgrades.md +33 -4
- package/docs/work-preflight.md +9 -0
- package/package.json +1 -1
- package/src/ai-cli.ts +28 -15
- package/src/ai.ts +98 -15
- package/src/application-channel.ts +52 -12
- package/src/application-cli.ts +7 -1
- package/src/application-origin.ts +2 -0
- package/src/audio.ts +37 -22
- package/src/auth-profile.ts +53 -0
- package/src/client-defaults.ts +3 -2
- package/src/codex-session.ts +14 -3
- package/src/config.ts +9 -0
- package/src/control-state.ts +31 -22
- package/src/delivery-socket-client.d.mts +2 -0
- package/src/delivery-socket-client.mjs +2 -0
- package/src/delivery-socket.ts +26 -1
- package/src/event-sources.ts +2 -2
- package/src/executor.ts +81 -7
- package/src/files.ts +47 -3
- package/src/host-executor.ts +68 -34
- package/src/index.ts +227 -110
- package/src/install-tools.mjs +8 -5
- package/src/menu.ts +9 -7
- package/src/plugin-broker.mjs +50 -14
- package/src/plugins/manager.mjs +34 -8
- package/src/run-evidence.ts +78 -0
- package/src/runs.ts +24 -2
- package/src/schedule-cli.ts +100 -13
- package/src/schedule-preflight.ts +23 -0
- package/src/scheduled-tasks.ts +16 -12
- package/src/scheduler.ts +85 -19
- package/src/scripts.ts +167 -0
- package/src/setup-fallback.ts +122 -0
- package/src/setup.ts +2 -2
- package/src/task-capability.ts +17 -1
- package/src/task-executor.ts +13 -9
- package/src/task-mcp.ts +20 -8
- package/src/tasks.ts +89 -9
- package/src/updates/runtime.mjs +107 -30
- package/src/updates/supervisor.mjs +27 -6
- package/test/ai.test.ts +40 -0
- package/test/application-channel.test.ts +16 -1
- package/test/audio.test.ts +19 -0
- package/test/auth-profiles.test.ts +187 -0
- package/test/config.test.ts +8 -0
- package/test/executor.test.ts +37 -0
- package/test/files.test.ts +44 -0
- package/test/host-executor.test.ts +45 -2
- package/test/install-json.test.mjs +21 -0
- package/test/plugin-broker.test.mjs +67 -0
- package/test/plugin-manager.test.mjs +44 -0
- package/test/private-speech-config.test.ts +22 -0
- package/test/run-evidence.test.ts +78 -0
- package/test/schedule-preflight.test.ts +49 -0
- package/test/scheduler-host.test.ts +7 -0
- package/test/scheduler-relay.test.ts +38 -3
- package/test/scheduler.test.ts +1 -1
- package/test/script-schedules.test.ts +219 -0
- package/test/setup-fallback.test.ts +245 -0
- package/test/task-native.test.ts +50 -12
- package/test/tasks.test.ts +97 -4
- package/test/updates.test.mjs +100 -11
package/test/updates.test.mjs
CHANGED
|
@@ -9,11 +9,11 @@ import { createServer } from 'node:net';
|
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
10
|
import { extract, digest, version, newer, compatible } from '../src/updates/artifact.mjs';
|
|
11
11
|
import { prepare, submit, command, read, jobPath, eligibility, jobs, cleanupStaleBackups, reviewedPluginDeploymentMigration } from '../src/updates/control.mjs';
|
|
12
|
-
import { perform, environment, packageManager, backupStateDirectory, execute } from '../src/updates/runtime.mjs';
|
|
12
|
+
import { perform, environment, packageManager, backupStateDirectory, execute, brokerNeedsRefresh } from '../src/updates/runtime.mjs';
|
|
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, withIdleUpgrade } from '../src/updates/supervisor.mjs';
|
|
16
|
+
import { providerEnvironment, queueAutomatic, waitForHostHeartbeat, withIdleUpgrade, withUpgrade } 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()=>{
|
|
@@ -114,13 +114,17 @@ test('root preparation leaves private plugin updates readable by the installer',
|
|
|
114
114
|
await exec('setpriv',['--reuid=20001','--regid=20002','--clear-groups','--no-new-privs',process.execPath,'-e',script]);
|
|
115
115
|
});
|
|
116
116
|
function runtime(f,{fail,stopped=false,buildArgs={CODEX_CLI_VERSION:'0.156.1'},latest='0.160.0'}={}) {
|
|
117
|
-
const calls=[];let failed=false;
|
|
117
|
+
const calls=[];let failed=false,brokerImage='sha256:'+'a'.repeat(64);
|
|
118
118
|
const execute=async(command,args,opts)=>{
|
|
119
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
|
-
if(args.includes('ps'))return stopped?'':'
|
|
123
|
-
if(args
|
|
122
|
+
if(args.includes('ps'))return stopped?'':'a'.repeat(64);
|
|
123
|
+
if(args.some(arg=>arg.includes("const fs=require('fs'),crypto=require('crypto')")))return digest(await fs.readFile(path.join(f.config.deploymentDir,'host-executor.json')));
|
|
124
|
+
if(args.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED'))) {const [hostSha256,registrySha256,configSha256]=args.slice(-3);return JSON.stringify({version:1,ready:true,hostSha256,registrySha256,configSha256,plugins:[]});}
|
|
125
|
+
if(args[0]==='image'&&args[1]==='inspect')return 'sha256:'+'b'.repeat(64);
|
|
126
|
+
if(args.includes('--force-recreate'))brokerImage=(await fs.readFile(args[args.lastIndexOf('--env-file')+1],'utf8')).trim().split('=')[1];
|
|
127
|
+
if(args[0]==='inspect')return brokerImage;
|
|
124
128
|
if(args[0]==='volume'&&args[1]==='ls')return 'existing';
|
|
125
129
|
if(args[0]==='volume')return JSON.stringify([{Name:args[2]}]);
|
|
126
130
|
if(args[0]==='run')await fs.writeFile(opts.outputFile,'retained-private-state',{mode:0o600});
|
|
@@ -384,22 +388,56 @@ test('plugin updates register additive command routes on an unchanged deployment
|
|
|
384
388
|
const dispatch=await prepareCommand(f.home,'query',[]);assert.deepEqual(dispatch.argv.slice(-4),['node','sample','/app/bin/example.mjs','query']);
|
|
385
389
|
assert(r.calls.some(call=>call.includes('up')));
|
|
386
390
|
});
|
|
387
|
-
test('isolated plugin replacement
|
|
391
|
+
test('isolated plugin replacement preserves the live broker and verifies activation and rollback',async t=>{
|
|
388
392
|
const f=await fixture(t,'plugin');
|
|
389
393
|
await fs.appendFile(path.join(f.config.deploymentDir,'docker.env'),'EZ_EXECUTOR_TRANSPORT=local\n');
|
|
390
394
|
const job=await queued(f),r=runtime(f),execute=r.execute;
|
|
391
395
|
let observed;
|
|
392
396
|
r.execute=async(command,args,options)=>{
|
|
393
|
-
if(args.includes('
|
|
397
|
+
if(args.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED'))) observed=(await read(path.join(f.home,'registry.json'))).plugins.sample.manifest.version;
|
|
398
|
+
if(args.includes('--force-recreate')) {
|
|
399
|
+
assert.equal(args.at(-1),'plugin-broker');
|
|
400
|
+
for(const flag of ['--no-deps','--no-build','--wait'])assert(args.includes(flag));
|
|
401
|
+
assert.equal(args[args.indexOf('--pull')+1],'never');
|
|
402
|
+
assert.equal(await fs.readFile(args[args.lastIndexOf('--env-file')+1],'utf8'),'EZ_RELAY_IMAGE=sha256:'+ 'a'.repeat(64)+'\n');
|
|
403
|
+
}
|
|
394
404
|
return execute(command,args,options);
|
|
395
405
|
};
|
|
396
406
|
assert.equal((await perform(f.home,job,r)).status,'completed');
|
|
397
|
-
assert.equal(observed,'0.1.1');
|
|
398
|
-
assert.equal(r.calls.filter(call=>call.includes('restart')).length,1);
|
|
407
|
+
assert.equal(observed,'0.1.1');assert.equal(job.brokerReadiness.ready,true);
|
|
399
408
|
const interrupted=await read(path.join(jobPath(f.home,job.id),'job.json'));interrupted.status='applying';
|
|
400
409
|
assert.equal((await perform(f.home,interrupted,r)).status,'rolled-back');
|
|
401
|
-
assert.equal(observed,'0.1.0');
|
|
402
|
-
assert.equal(r.calls.filter(call=>call.includes('
|
|
410
|
+
assert.equal(observed,'0.1.0');assert.equal(interrupted.rollbackBrokerReadiness.ready,true);
|
|
411
|
+
assert.equal(r.calls.filter(call=>call.includes('--force-recreate')).length,0);
|
|
412
|
+
assert(!r.calls.some(call=>call.includes('restart')||call[0]==='stopHost'));
|
|
413
|
+
assert.equal((await read(path.join(f.agent.controlDir,'plugin-broker-plugins.json'))).plugins[0].version,'0.1.0');
|
|
414
|
+
});
|
|
415
|
+
for(const boundary of ['probe','readback'])test(`isolated broker ${boundary} failure cannot complete an update`,async t=>{
|
|
416
|
+
const f=await fixture(t,'plugin');await fs.appendFile(path.join(f.config.deploymentDir,'docker.env'),'EZ_EXECUTOR_TRANSPORT=local\n');
|
|
417
|
+
const job=await queued(f),r=runtime(f),base=r.execute;
|
|
418
|
+
r.execute=async(c,a,o)=>{
|
|
419
|
+
if(boundary==='recreate'&&a.includes('--force-recreate')||boundary==='probe'&&a.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED')))throw Error('Synthetic broker failure');
|
|
420
|
+
if(boundary==='readback'&&a.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED')))return JSON.stringify({version:1,ready:true,hostSha256:'0'.repeat(64)});
|
|
421
|
+
return base(c,a,o);
|
|
422
|
+
};
|
|
423
|
+
const result=await perform(f.home,job,r);
|
|
424
|
+
assert.equal(result.status,'recovery-required');assert(result.error);assert(result.recoveryError);
|
|
425
|
+
assert.equal((await read(path.join(f.home,'registry.json'))).plugins.sample.manifest.version,'0.1.0');
|
|
426
|
+
});
|
|
427
|
+
test('main isolated activation and retained-relay rollback also refresh the broker',async t=>{
|
|
428
|
+
const f=await fixture(t);await fs.appendFile(path.join(f.config.deploymentDir,'docker.env'),'EZ_EXECUTOR_TRANSPORT=local\n');
|
|
429
|
+
const job=await queued(f),r=runtime(f);
|
|
430
|
+
assert.equal((await perform(f.home,job,r)).status,'completed');assert.equal(job.brokerReadiness.ready,true);
|
|
431
|
+
const base=r.execute;r.execute=async(c,a,o)=>a[0]==='inspect'&&a.includes('{{json .}}')?JSON.stringify({Image:'sha256:'+'a'.repeat(64),State:{Running:true,Health:{Status:'healthy'}}}):base(c,a,o);
|
|
432
|
+
const interrupted=await read(path.join(jobPath(f.home,job.id),'job.json'));interrupted.status='applying';
|
|
433
|
+
assert.equal((await perform(f.home,interrupted,r)).status,'rolled-back');
|
|
434
|
+
assert.equal(interrupted.rollbackBrokerReadiness.ready,true);
|
|
435
|
+
assert.equal(r.calls.filter(call=>call.includes('--force-recreate')).length,2);
|
|
436
|
+
assert.equal(r.calls.filter(call=>call.includes('up')&&call.at(-1)==='relay').length,1);
|
|
437
|
+
});
|
|
438
|
+
test('host-capable updates never inspect, recreate or probe an isolated broker',async t=>{
|
|
439
|
+
const f=await fixture(t,'plugin'),r=runtime(f);assert.equal((await perform(f.home,await queued(f),r)).status,'completed');
|
|
440
|
+
assert(!r.calls.some(call=>call.includes('plugin-broker')||call.some(arg=>arg.includes('EZ_BROKER_STRUCTURAL_READINESS_FAILED'))));
|
|
403
441
|
});
|
|
404
442
|
test('plugin update preserves a created but never running service as stopped',async t=>{
|
|
405
443
|
const f=await fixture(t,'plugin'),job=await queued(f),r=runtime(f),execute=r.execute;
|
|
@@ -662,3 +700,54 @@ for(const mode of ['exact','wrong-image','unhealthy']) test(`recovery reuses onl
|
|
|
662
700
|
assert.equal(calls.some(c=>c.includes('stop')),mode!=='exact');
|
|
663
701
|
assert.equal((await read(path.join(f.home,'config.json'))).packageRoot,f.old);
|
|
664
702
|
});
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
test('plugin upgrade builds during a native turn, then gates and drains calls for activation',async t=>{
|
|
706
|
+
const f=await fixture(t,'plugin'),job=await queued(f),r=runtime(f),execute=r.execute;
|
|
707
|
+
await fs.mkdir(path.join(f.agent.controlDir,'host-executor'));
|
|
708
|
+
await atomic(path.join(f.agent.controlDir,'host-executor/task.running.json'),{pid:process.pid});
|
|
709
|
+
let built,continueBuild;
|
|
710
|
+
const building=new Promise(resolve=>built=resolve),hold=new Promise(resolve=>continueBuild=resolve);
|
|
711
|
+
r.execute=async(command,args,options)=>{
|
|
712
|
+
if(args.includes('build')) {
|
|
713
|
+
await assert.rejects(fs.access(path.join(f.home,'registry.lock')),{code:'ENOENT'});
|
|
714
|
+
built();await hold;
|
|
715
|
+
}
|
|
716
|
+
return execute(command,args,options);
|
|
717
|
+
};
|
|
718
|
+
const upgrade=withUpgrade(f.agent.controlDir,job,new AbortController().signal,()=>perform(f.home,job,r),{isIdle:async()=>false});
|
|
719
|
+
await building;
|
|
720
|
+
const oldCall=await prepareCommand(f.home,'sample',[],{invocation:true});
|
|
721
|
+
continueBuild();
|
|
722
|
+
for(let n=0;n<100&&!await fs.access(path.join(f.home,'registry.lock')).then(()=>true,()=>false);n++)await new Promise(resolve=>setTimeout(resolve,10));
|
|
723
|
+
await fs.access(path.join(f.home,'registry.lock'));
|
|
724
|
+
assert(!r.calls.some(call=>call.includes('stop')));
|
|
725
|
+
let admitted=false;
|
|
726
|
+
const nextCall=prepareCommand(f.home,'sample',[],{invocation:true}).then(command=>{admitted=true;return command;});
|
|
727
|
+
await new Promise(resolve=>setTimeout(resolve,50));assert.equal(admitted,false);
|
|
728
|
+
await oldCall.release();
|
|
729
|
+
assert.equal((await upgrade).status,'completed');
|
|
730
|
+
const command=await nextCall;assert.equal(command.revision,(await read(path.join(f.home,'registry.json'))).plugins.sample.revision);await command.release();
|
|
731
|
+
await fs.access(path.join(f.agent.controlDir,'host-executor/task.running.json'));
|
|
732
|
+
await assert.rejects(fs.access(path.join(f.agent.controlDir,'upgrade-pause.json')),{code:'ENOENT'});
|
|
733
|
+
assert(!r.calls.some(call=>call[0]==='stopHost'||call.includes('--force-recreate')));
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
test('core upgrades still defer for active work; cancelled plugin admission never applies',async t=>{
|
|
737
|
+
const root=await fs.mkdtemp(path.join(tmpdir(),'ez-plugin-admission-'));t.after(()=>fs.rm(root,{recursive:true,force:true}));
|
|
738
|
+
const controller=new AbortController();let applied=0;
|
|
739
|
+
await withUpgrade(root,{id:'core',target:'main'},controller.signal,async()=>applied++,{isIdle:async()=>false});assert.equal(applied,0);
|
|
740
|
+
controller.abort();await withUpgrade(root,{id:'plugin',target:'sample'},controller.signal,async()=>applied++);assert.equal(applied,0);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
test('changed single-file broker binding retains full native drain and broker recreation',async t=>{
|
|
745
|
+
const f=await fixture(t,'plugin'),job=await queued(f),r=runtime(f);
|
|
746
|
+
await fs.appendFile(path.join(f.config.deploymentDir,'docker.env'),'EZ_EXECUTOR_TRANSPORT=local\n');
|
|
747
|
+
const execute=r.execute;r.execute=async(c,a,o)=>a.some(arg=>arg.includes("const fs=require('fs'),crypto=require('crypto')"))?'0'.repeat(64):execute(c,a,o);
|
|
748
|
+
assert.equal(await brokerNeedsRefresh(f.config,r.execute),true);
|
|
749
|
+
let applied=false;
|
|
750
|
+
await withUpgrade(f.agent.controlDir,job,new AbortController().signal,async()=>{applied=true;},{requiresIdle:true,isIdle:async()=>false});assert.equal(applied,false);
|
|
751
|
+
assert.equal((await perform(f.home,job,{...r,refreshPluginBroker:true})).status,'completed');
|
|
752
|
+
assert.equal(r.calls.filter(call=>call.includes('--force-recreate')).length,1);
|
|
753
|
+
});
|