@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.
Files changed (80) hide show
  1. package/.env.example +3 -0
  2. package/AGENTS.md +2 -2
  3. package/CHANGELOG.md +30 -0
  4. package/Dockerfile +5 -1
  5. package/THIRD_PARTY_NOTICES.md +6 -3
  6. package/bin/ez +7 -0
  7. package/docker/broker-readiness.mjs +54 -0
  8. package/docker/broker-update-smoke.mjs +100 -0
  9. package/docker/run.ts +1 -1
  10. package/docker/upgrade-smoke.mjs +1 -1
  11. package/docs/application-channel.md +32 -1
  12. package/docs/architecture/ai-selection.md +23 -4
  13. package/docs/docker-runtime.md +57 -0
  14. package/docs/isolated-plugin-update-readiness.md +66 -0
  15. package/docs/releasing.md +4 -0
  16. package/docs/scheduling.md +141 -6
  17. package/docs/selective-monitoring.md +13 -0
  18. package/docs/upgrades.md +33 -4
  19. package/docs/work-preflight.md +9 -0
  20. package/package.json +1 -1
  21. package/src/ai-cli.ts +28 -15
  22. package/src/ai.ts +98 -15
  23. package/src/application-channel.ts +52 -12
  24. package/src/application-cli.ts +7 -1
  25. package/src/application-origin.ts +2 -0
  26. package/src/audio.ts +37 -22
  27. package/src/auth-profile.ts +53 -0
  28. package/src/client-defaults.ts +3 -2
  29. package/src/codex-session.ts +14 -3
  30. package/src/config.ts +9 -0
  31. package/src/control-state.ts +31 -22
  32. package/src/delivery-socket-client.d.mts +2 -0
  33. package/src/delivery-socket-client.mjs +2 -0
  34. package/src/delivery-socket.ts +26 -1
  35. package/src/event-sources.ts +2 -2
  36. package/src/executor.ts +81 -7
  37. package/src/files.ts +47 -3
  38. package/src/host-executor.ts +68 -34
  39. package/src/index.ts +227 -110
  40. package/src/install-tools.mjs +8 -5
  41. package/src/menu.ts +9 -7
  42. package/src/plugin-broker.mjs +50 -14
  43. package/src/plugins/manager.mjs +34 -8
  44. package/src/run-evidence.ts +78 -0
  45. package/src/runs.ts +24 -2
  46. package/src/schedule-cli.ts +100 -13
  47. package/src/schedule-preflight.ts +23 -0
  48. package/src/scheduled-tasks.ts +16 -12
  49. package/src/scheduler.ts +85 -19
  50. package/src/scripts.ts +167 -0
  51. package/src/setup-fallback.ts +122 -0
  52. package/src/setup.ts +2 -2
  53. package/src/task-capability.ts +17 -1
  54. package/src/task-executor.ts +13 -9
  55. package/src/task-mcp.ts +20 -8
  56. package/src/tasks.ts +89 -9
  57. package/src/updates/runtime.mjs +107 -30
  58. package/src/updates/supervisor.mjs +27 -6
  59. package/test/ai.test.ts +40 -0
  60. package/test/application-channel.test.ts +16 -1
  61. package/test/audio.test.ts +19 -0
  62. package/test/auth-profiles.test.ts +187 -0
  63. package/test/config.test.ts +8 -0
  64. package/test/executor.test.ts +37 -0
  65. package/test/files.test.ts +44 -0
  66. package/test/host-executor.test.ts +45 -2
  67. package/test/install-json.test.mjs +21 -0
  68. package/test/plugin-broker.test.mjs +67 -0
  69. package/test/plugin-manager.test.mjs +44 -0
  70. package/test/private-speech-config.test.ts +22 -0
  71. package/test/run-evidence.test.ts +78 -0
  72. package/test/schedule-preflight.test.ts +49 -0
  73. package/test/scheduler-host.test.ts +7 -0
  74. package/test/scheduler-relay.test.ts +38 -3
  75. package/test/scheduler.test.ts +1 -1
  76. package/test/script-schedules.test.ts +219 -0
  77. package/test/setup-fallback.test.ts +245 -0
  78. package/test/task-native.test.ts +50 -12
  79. package/test/tasks.test.ts +97 -4
  80. package/test/updates.test.mjs +100 -11
@@ -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?'':'container-id';
123
- if(args[0]==='inspect')return 'sha256:'+'a'.repeat(64);
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 refreshes broker version inventory after registry activation',async t=>{
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('restart')&&args.at(-1)==='plugin-broker') observed=(await read(path.join(f.home,'registry.json'))).plugins.sample.manifest.version;
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('restart')).length,2);
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
+ });