@jc_stack/ez-agents 0.1.0-beta.33 → 0.1.0-beta.36

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 (157) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +9 -16
  3. package/AGENTS.md +17 -9
  4. package/CHANGELOG.md +92 -0
  5. package/Dockerfile +11 -9
  6. package/README.md +32 -27
  7. package/THIRD_PARTY_NOTICES.md +4 -4
  8. package/bin/ez +97 -0
  9. package/bin/ezenciel-agents-plugin-broker.mjs +16 -0
  10. package/bin/ezenciel-agents-provision-telegram.mjs +13 -0
  11. package/bin/ezenciel-agents.mjs +1 -1
  12. package/compose.yaml +42 -5
  13. package/docker/entrypoint.sh +1 -1
  14. package/docker/healthcheck.mjs +12 -4
  15. package/docker/run.ts +5 -6
  16. package/docker/smoke.mjs +32 -7
  17. package/docs/application-channel.md +38 -10
  18. package/docs/architecture/ai-selection.md +3 -1
  19. package/docs/architecture/authority-boundaries.md +2 -2
  20. package/docs/channel-backend.md +4 -3
  21. package/docs/docker-runtime.md +106 -21
  22. package/docs/host-service.md +17 -6
  23. package/docs/local-qa.md +2 -2
  24. package/docs/plugin-guide.md +202 -0
  25. package/docs/plugins.md +63 -2
  26. package/docs/releasing.md +13 -0
  27. package/docs/repair.md +1 -1
  28. package/docs/responsive-channels.md +1 -1
  29. package/docs/scheduling.md +4 -4
  30. package/docs/setup.md +96 -16
  31. package/docs/standalone-cli.md +6 -5
  32. package/docs/upgrades.md +46 -23
  33. package/package.json +5 -5
  34. package/scripts/smoke-scheduler.ts +12 -4
  35. package/scripts/smoke.ts +14 -11
  36. package/src/agent-guidance.ts +6 -9
  37. package/src/agent-install.ts +44 -9
  38. package/src/ai-cli.ts +9 -7
  39. package/src/ai.ts +7 -7
  40. package/src/application-channel.ts +88 -17
  41. package/src/application-cli.ts +2 -2
  42. package/src/approval-cli.ts +14 -7
  43. package/src/approval.ts +27 -37
  44. package/src/channel-backend.ts +2 -3
  45. package/src/codex-session.ts +5 -3
  46. package/src/config.ts +18 -28
  47. package/src/control-state.ts +187 -9
  48. package/src/conversation-menu.ts +6 -17
  49. package/src/delivery-socket-client.d.mts +4 -0
  50. package/src/delivery-socket-client.mjs +104 -0
  51. package/src/delivery-socket.ts +354 -0
  52. package/src/execution-authority.ts +21 -5
  53. package/src/executor.ts +110 -31
  54. package/src/files.ts +38 -11
  55. package/src/host-executor.ts +50 -17
  56. package/src/inbox.ts +15 -49
  57. package/src/index.ts +96 -52
  58. package/src/install-config.ts +2 -2
  59. package/src/install-tools.mjs +100 -23
  60. package/src/isolation.ts +25 -0
  61. package/src/ledger-endpoint.d.mts +2 -0
  62. package/src/ledger-endpoint.mjs +17 -0
  63. package/src/menu.ts +16 -8
  64. package/src/message-history.ts +13 -18
  65. package/src/message.ts +35 -18
  66. package/src/plugin-broker.mjs +396 -0
  67. package/src/plugins/connection.mjs +2 -2
  68. package/src/plugins/manager.mjs +178 -26
  69. package/src/plugins/native-tasks.mjs +2 -2
  70. package/src/prompt-suffix.ts +12 -0
  71. package/src/react.ts +6 -4
  72. package/src/runs.ts +156 -243
  73. package/src/schedule-cli.ts +29 -10
  74. package/src/scheduled-tasks.ts +53 -10
  75. package/src/scheduler.ts +20 -7
  76. package/src/service.ts +3 -3
  77. package/src/setup.ts +11 -7
  78. package/src/software-status.ts +36 -4
  79. package/src/task-executor.ts +6 -3
  80. package/src/task-workspace.ts +4 -3
  81. package/src/tasks.ts +1 -1
  82. package/src/telegram-provisioning-cli.ts +22 -0
  83. package/src/telegram-provisioning.ts +214 -0
  84. package/src/updates/control.mjs +77 -7
  85. package/src/updates/runtime.mjs +29 -9
  86. package/src/updates/status.mjs +25 -3
  87. package/src/updates/supervisor.mjs +31 -18
  88. package/src/version.ts +20 -0
  89. package/src/workspace-confine.ts +38 -0
  90. package/src/workspace.ts +24 -20
  91. package/templates/agent-purpose.md +1 -0
  92. package/templates/failure-review.md +1 -1
  93. package/templates/maintainer-purpose.md +1 -1
  94. package/test/agent-guidance.test.ts +18 -88
  95. package/test/agent-install.test.ts +48 -0
  96. package/test/ai-cli.test.ts +18 -0
  97. package/test/ai.test.ts +84 -0
  98. package/test/application-channel.test.ts +66 -15
  99. package/test/application-controls.test.ts +56 -3
  100. package/test/application-only.test.ts +16 -13
  101. package/test/channel-backend.test.ts +3 -7
  102. package/test/channel-delivery.test.ts +4 -1
  103. package/test/channel-owner.test.ts +6 -3
  104. package/test/codex-context.test.ts +63 -3
  105. package/test/codex-session.test.ts +6 -1
  106. package/test/config.test.ts +47 -37
  107. package/test/control-state.test.ts +83 -4
  108. package/test/conversation-menu.test.ts +2 -4
  109. package/test/delivery-transport.test.ts +72 -0
  110. package/test/docker-runtime.test.ts +27 -14
  111. package/test/engine-handoff.test.ts +12 -8
  112. package/test/event-sources.test.ts +2 -2
  113. package/test/execution-authority.test.ts +9 -9
  114. package/test/executor.test.ts +47 -11
  115. package/test/failure.test.ts +18 -5
  116. package/test/files.test.ts +2 -2
  117. package/test/helpers/ledger.ts +18 -0
  118. package/test/host-executor.test.ts +80 -0
  119. package/test/inbox.test.ts +8 -10
  120. package/test/install-config.test.ts +18 -6
  121. package/test/install-tools.test.mjs +80 -7
  122. package/test/intake-relay.test.ts +58 -11
  123. package/test/message-history.test.ts +39 -47
  124. package/test/native-tasks.test.ts +1 -1
  125. package/test/plugin-broker.test.mjs +172 -0
  126. package/test/plugin-manager.test.mjs +143 -5
  127. package/test/reaction.test.ts +4 -0
  128. package/test/relay.test.ts +5 -7
  129. package/test/repair-policy.test.ts +1 -4
  130. package/test/schedule-cli.test.ts +14 -9
  131. package/test/scheduled-tasks.test.ts +29 -6
  132. package/test/scheduler-host.test.ts +5 -5
  133. package/test/scheduler-relay.test.ts +15 -9
  134. package/test/scheduler.test.ts +2 -2
  135. package/test/security.test.ts +6 -3
  136. package/test/software-status.test.ts +21 -3
  137. package/test/telegram-provisioning.test.ts +160 -0
  138. package/test/updates.test.mjs +102 -13
  139. package/test/workspace-confine.test.ts +47 -0
  140. package/test/workspace.test.ts +27 -11
  141. package/bin/ezenciel-agents-watch.mjs +0 -8
  142. package/compose.workforce-watch.yaml +0 -33
  143. package/docker/recovery.ts +0 -11
  144. package/docs/pagerduty.md +0 -42
  145. package/docs/workforce-watch.md +0 -101
  146. package/src/pagerduty.ts +0 -109
  147. package/src/reply-context.ts +0 -44
  148. package/src/update-attention.ts +0 -18
  149. package/src/workforce-watch-cli.ts +0 -14
  150. package/src/workforce-watch.ts +0 -155
  151. package/templates/agent/AGENTS.md +0 -15
  152. package/templates/agent/SOUL.md +0 -11
  153. package/templates/agent/USER.md +0 -5
  154. package/templates/agent-guidance.md +0 -114
  155. package/test/pagerduty.test.ts +0 -104
  156. package/test/update-attention.test.ts +0 -20
  157. package/test/workforce-watch.test.ts +0 -180
package/docker/run.ts CHANGED
@@ -4,7 +4,6 @@ import { executorEnvironment, executorInvocation } from '../src/executor.js'
4
4
  import { parseEnv } from 'node:util'
5
5
  import { writeFile, rm } from 'node:fs/promises'
6
6
  import { loadConfig } from '../src/config.js'
7
- import { recoverInterruptedRuns } from './recovery.js'
8
7
  import { createRelay } from '../src/index.js'
9
8
  import { packageVersion } from '../src/version.js'
10
9
 
@@ -21,18 +20,18 @@ try { privateEnv = parseEnv(readFileSync(3, 'utf8')) } catch (error) {
21
20
  if ((error as NodeJS.ErrnoException).code !== 'EINVAL' && (error as NodeJS.ErrnoException).code !== 'EBADF') throw error
22
21
  } finally { try { closeSync(3) } catch {} }
23
22
  for (const [key, value] of Object.entries(privateEnv)) {
24
- if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'EZ_CHANNEL_BACKEND_TOKEN', 'PAGERDUTY_ROUTING_KEY'].includes(key)) process.env[key] = value
23
+ if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'EZ_CHANNEL_BACKEND_TOKEN'].includes(key)) process.env[key] = value
25
24
  }
26
25
  privateEnv = {}
27
26
  const [command = 'start', ...args] = process.argv.slice(2)
28
27
  process.argv = [process.argv[0], '', ...args]
29
- if (['start', 'smoke'].includes(command)) await recoverInterruptedRuns(loadConfig().controlDir, Boolean(loadConfig().channelBackendUrl))
30
28
  if (command === 'start') {
31
- const relay = createRelay(loadConfig())
32
- const heartbeat = '/state/control/heartbeat.json'
29
+ const config = loadConfig()
30
+ const relay = createRelay(config)
31
+ const heartbeat = process.env.EZ_HEARTBEAT_FILE || '/tmp/ez-relay-heartbeat.json'
33
32
  await rm(heartbeat, { force: true })
34
33
  const timer = setInterval(() => {
35
- if (relay.isRunning()) void writeFile(heartbeat, JSON.stringify({ at: Date.now(), polling: Boolean(relay.bot?.isRunning()), applicationOnly: process.env.EZ_TELEGRAM_ENABLED === 'false', version: packageVersion }), { mode: 0o600 })
34
+ if (relay.isRunning()) void writeFile(heartbeat, JSON.stringify({ at: Date.now(), polling: Boolean(relay.bot?.isRunning()), applicationOnly: !relay.telegramEnabled, telegramConfigured: config.telegramBotToken !== '', version: packageVersion }), { mode: 0o600 })
36
35
  }, 5000)
37
36
  for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => { clearInterval(timer); void relay.stop() })
38
37
  try { await relay.start() } finally { clearInterval(timer); await rm(heartbeat, { force: true }) }
package/docker/smoke.mjs CHANGED
@@ -10,6 +10,7 @@ const volume = `${holder}-control`;
10
10
  const application = `${holder}-application`;
11
11
  const marker = 'qa-private-secret-never-in-executor';
12
12
  writeFileSync(join(dir, 'relay.env'), `TELEGRAM_BOT_TOKEN=${marker}\n`, { mode: 0o600 });
13
+ writeFileSync(join(dir, 'purpose.md'), 'Verify the packaged Ez runtime.\n', { mode: 0o644 });
13
14
  writeFileSync(join(dir, 'node'), `#!/bin/sh\nIFS= read -r value <&3\n[ "$value" = "TELEGRAM_BOT_TOKEN=${marker}" ] || exit 91\nexec /usr/local/bin/node "$@"\n`, { mode: 0o555 });
14
15
  const run = (args) => spawnSync('docker', args, { encoding: 'utf8', timeout: 60000 });
15
16
  try {
@@ -17,17 +18,19 @@ try {
17
18
  '--mount',`type=bind,src=${join(dir,'node')},dst=/qa/node,readonly`,'-e','PATH=/qa:/usr/local/bin:/usr/bin:/bin',image,'application','--help']);
18
19
  assert.equal(inherited.status,0,inherited.stderr);
19
20
  const nonroot = ['--user','20000:20000','--read-only','--cap-drop','ALL','--security-opt','no-new-privileges',
21
+ '--mount',`type=bind,src=${join(dir,'purpose.md')},dst=/run/agent-purpose.md,readonly`,'-e','EZ_AGENT_PURPOSE_FILE=/run/agent-purpose.md',
20
22
  '--tmpfs','/tmp:mode=1777','--tmpfs','/state/control:uid=20000,gid=20000,mode=700',
21
23
  '--tmpfs','/state/home:uid=20000,gid=20000,mode=700','--tmpfs','/workspace:uid=20000,gid=20000,mode=700',
22
- '-e','EZ_TELEGRAM_ENABLED=false','-e','EZ_APPLICATION_PORT=8110','-e','EZ_EXECUTOR_TRANSPORT=local','-e','EZ_EXECUTOR_CLI=codex'];
24
+ '-e','EZ_APPLICATION_PORT=8110','-e','EZ_ISOLATION=isolated','-e','EZ_EXECUTOR_TRANSPORT=local','-e','EZ_EXECUTOR_CLI=codex'];
23
25
  const help = run(['run','--rm',...nonroot,image,'application','--help']);
24
26
  assert.equal(help.status,0,help.stderr);
25
27
  assert.match(help.stdout,/ezenciel-agents-application/);
26
28
  const login = run(['run','--rm',...nonroot,'--entrypoint','/bin/bash',image,'-lc',
27
- 'command -v ezenciel-agents-application && command -v ezenciel-agents-message && ezenciel-agents-message --help']);
29
+ 'command -v ezenciel-agents-application && command -v ezenciel-agents-message && command -v codex && ezenciel-agents-message --help && codex --version']);
28
30
  assert.equal(login.status,0,login.stderr);
29
31
  assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-application/);
30
32
  assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-message/);
33
+ assert.match(login.stdout,/codex-cli 0\.153\.4/);
31
34
  const started = run(['run','-d','--name',application,...nonroot,image,'start']);
32
35
  assert.equal(started.status,0,started.stderr);
33
36
  let ready = false;
@@ -69,7 +72,23 @@ try {
69
72
  assert.equal(JSON.parse(custom.stdout).uid,20001);
70
73
  const failed = run(['run','--rm',image,'exec','node','-e','process.exit(23)']);
71
74
  assert.equal(failed.status, 23, failed.stderr);
72
- const held = run(['run','-d','--name',holder,'-v',`${volume}:/state/control`,image,'exec','node','-e',"require('fs').writeFileSync('/state/control/ready','yes');setInterval(()=>{},1000)"]);
75
+ // The live delivery socket is the single-relay guard (it replaced the kernel
76
+ // flock). This mirrors the relay's own start check: an answering address
77
+ // refuses the second owner; a stale file is removed before binding.
78
+ const guardScript = `
79
+ const net=require('net'),fs=require('fs'),stay=process.argv[1]==='hold',path='/state/control/delivery.sock';
80
+ const probe=net.createConnection(path);
81
+ const bind=()=>{
82
+ fs.rmSync(path,{force:true});
83
+ const s=net.createServer(()=>{});
84
+ s.on('error',()=>process.exit(1));
85
+ s.listen(path,()=>{if(stay){fs.writeFileSync('/state/control/ready','yes');setInterval(()=>{},1000)}else s.close(()=>process.exit(0))});
86
+ };
87
+ probe.on('connect',()=>{probe.destroy();process.exit(73)});
88
+ probe.on('error',bind);
89
+ probe.setTimeout(3000,()=>{probe.destroy();process.exit(1)});
90
+ `;
91
+ const held = run(['run','-d','--name',holder,'-v',`${volume}:/state/control`,image,'exec','node','-e',guardScript,'hold']);
73
92
  assert.equal(held.status,0,held.stderr);
74
93
  for (let n=0;n<30;n++) {
75
94
  const probe=run(['exec',holder,'test','-f','/state/control/ready']);
@@ -77,10 +96,16 @@ try {
77
96
  await new Promise(resolve=>setTimeout(resolve,100));
78
97
  }
79
98
  assert.equal(run(['exec',holder,'test','-f','/state/control/ready']).status,0);
80
- const duplicate=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e','process.exit(0)']);
99
+ const duplicate=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e',guardScript]);
81
100
  assert.equal(duplicate.status,73,duplicate.stderr);
82
101
  assert.equal(run(['kill',holder]).status,0);
83
- const recovered=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e','process.exit(0)']);
84
- assert.equal(recovered.status,0,recovered.stderr);
85
- console.log('Docker smoke passed: direct non-root application help/start/health/stop, inherited private descriptor, non-root executor, private secret isolation, literal argv, exit code, no Docker socket, duplicate writer rejection and crash lock release.');
102
+ let released=1;
103
+ for (let n=0;n<50;n++) {
104
+ const recovered=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e',guardScript]);
105
+ released=recovered.status;
106
+ if(released===0)break;
107
+ await new Promise(resolve=>setTimeout(resolve,100));
108
+ }
109
+ assert.equal(released,0,'a dead relay must release the delivery socket');
110
+ console.log('Docker smoke passed: direct non-root application help/start/health/stop, inherited private descriptor, non-root executor, isolated Codex on PATH, private secret isolation, literal argv, exit code, no Docker socket, live delivery-socket guard and crash release.');
86
111
  } finally { run(['rm','-f',holder,application]); run(['volume','rm',volume]); rmSync(dir, {recursive:true, force:true}); }
@@ -6,6 +6,10 @@ outbox. The application supplies domain tools and keeps its UI/content API; it
6
6
  can remove its own model runner, continuation loop and engine authentication.
7
7
  This is separate from the older outbound channel-backend integration.
8
8
 
9
+ Run admission, status and inbox receipts include the immutable `preset` captured
10
+ for that run so an application can label delivered messages with the engine choice
11
+ that actually produced them.
12
+
9
13
  ## Shared runtime controls
10
14
 
11
15
  Register with `--share-owner` (the existing `--share-telegram` spelling is an
@@ -267,17 +271,36 @@ separate CLI histories. A retry cannot change an admitted turn's AI choice.
267
271
 
268
272
  Register owner-chat channels with `--share-owner` and submit `followOwner:true`.
269
273
  Web, phone and Telegram then use the same selected native conversation and AI.
274
+ A later same-client provider or model change starts a new native conversation for the next
275
+ admitted turn; already-queued work keeps its original engine.
270
276
  This works without Telegram and requires no transcript replay or new runner.
271
277
  Scoped application conversations remain separate when the flag is omitted.
272
278
  Owner identity does not automatically merge histories or grant learner access.
273
279
 
274
- To link Telegram, enable its ordinary bot configuration, send a real DM, then
275
- approve the observed pending identity using `ezenciel-agents-owner approve ID`.
280
+ To link Telegram, enable its ordinary bot configuration, then use the
281
+ application-initiated handoff below or send a real DM and approve the observed
282
+ pending identity using `ezenciel-agents-owner approve ID`.
276
283
  `ezenciel-agents-owner unlink-telegram` removes that channel without removing the
277
284
  owner, application bindings or native sessions. Relinking does not authorize old
278
285
  Telegram deliveries. Linking other providers requires an authenticated adapter;
279
286
  registering the label `phone` does not install a phone service.
280
287
 
288
+ ### Application-initiated Telegram handoff
289
+
290
+ An already-authorized `--share-owner` application binding can make the ordinary
291
+ Telegram link easy without becoming a Telegram backend. `GET /v1/telegram`
292
+ returns the current `connected` state. `POST /v1/telegram/link` either returns
293
+ `{connected:true}` or a single-use, short-lived `https://t.me/BOT?start=TOKEN`
294
+ link. The bot accepts that token only from the matching application's current
295
+ owner, then persists the normal Telegram owner/channel record and consumes the
296
+ ticket. The raw token is never stored in control state.
297
+
298
+ This is a launch handoff, not a UI-owned connection: the application does not
299
+ store Telegram identity, bot credentials, or the ticket, and the Ez agent keeps
300
+ working when that application is unavailable. There is deliberately no
301
+ application disconnect endpoint. Administrative unlink remains the explicit
302
+ owner control above.
303
+
281
304
  The legacy `--share-telegram`, `--share-active` and `followTelegram:true` spellings remain supported.
282
305
 
283
306
  ### Existing scoped Telegram sharing
@@ -319,12 +342,17 @@ a generic HTTP error or revoked credential alone is not proof of termination.
319
342
 
320
343
  ## Application-only deployment (no Telegram bot)
321
344
 
322
- For a private per-user native runtime, set `EZ_TELEGRAM_ENABLED=false` and
345
+ For a private per-user native runtime, omit `TELEGRAM_BOT_TOKEN` and set
323
346
  `EZ_APPLICATION_PORT` (plus `EZ_APPLICATION_HOST` when other containers connect).
324
- No bot token is required or used. This mode runs the existing native executor,
347
+ This mode runs the existing native executor,
325
348
  application queue and application outbox without creating a Telegram client,
326
- starting Telegram sources/polling, or registering bot commands. Default deployments
327
- still enable Telegram and require its token.
349
+ starting Telegram sources/polling, or registering bot commands. Telegram becomes
350
+ active as soon as the agent receives its private bot token.
351
+
352
+ Upgrade note: the removed `EZ_TELEGRAM_ENABLED` flag is no longer read. A stale
353
+ flag that contradicts the token (disabled flag with a token present, or enabled
354
+ flag with no token) refuses to start — unset the flag, and remove any stale
355
+ `TELEGRAM_BOT_TOKEN` to stay application-only.
328
356
 
329
357
  The installing administrator can initialize empty control authority and register
330
358
  an application in one local command:
@@ -379,14 +407,14 @@ up to 10 MiB decoded; empty files and other types are rejected. Core detects fil
379
407
  content rather than trusting a client MIME label. This same limit and staging
380
408
  contract apply to Telegram photo/document input.
381
409
 
382
- Authentication precedes staging. Core stages the file privately in the ordinary
383
- workspace inbox and uses the same attachment metadata and literal comment as
384
- Telegram. The existing run queue, native executor, selected AI and native session
410
+ Authentication precedes staging. Core stages the file privately under the
411
+ deployment control directory and uses the same attachment metadata and literal
412
+ comment as Telegram. The existing run queue, native executor, selected AI and native session
385
413
  remain authoritative; applications must not extract, summarize, convert or build
386
414
  attachment prompts. Use `followOwner:true` for the shared owner conversation.
387
415
  Retry the same request ID, filename, bytes and literal comment; conflicts return
388
416
  409 and successful retries do not stage another file. A failed admission removes
389
- its staged file; a process crash may leave an unused inbox file, never a runnable
417
+ its staged file; a process crash may leave an unused staged file, never a runnable
390
418
  partial request. Separate owner runtimes must retain separate workspace mounts.
391
419
 
392
420
  This is inbound file support. Application replies currently deliver text only;
@@ -8,7 +8,9 @@ At intake each journal entry receives an immutable preset and conversation ID.
8
8
  Batches cannot cross that boundary. The run copies that choice and invokes the
9
9
  native adapter with its exact model/effort. A later menu change cannot reroute it.
10
10
  Old conversation metadata is retained only so accepted work can finish; selecting
11
- a different CLI never restores that CLI's old history.
11
+ a different CLI never restores that CLI's old history. Selecting a different provider
12
+ or model on the same client does not resume the previous selection's native thread; the next
13
+ admitted turn starts a new native conversation.
12
14
 
13
15
  Grok/Claude accept caller-selected native UUIDs. Codex/OpenCode generate IDs, so
14
16
  the adapter reads only their typed JSONL session metadata; stdout never becomes a
@@ -127,8 +127,8 @@ Native configuration reference: [Codex configuration](https://learn.chatgpt.com/
127
127
  Task notes live under protected `control/tasks/`, separate from the owner mind.
128
128
  Exact-contact task reports currently use the linked Telegram owner outbox and
129
129
  are visibly labelled; any-conversation runs have no report tool. Reports are not
130
- inserted as owner instructions or trusted memory. The owner mind
131
- keeps its existing `inbox/` and `work/` organization. No database or general memory
130
+ inserted as owner instructions or trusted memory. The owner mind is the workspace `AGENTS.md` and files the agent creates.
131
+ No prescribed `inbox/` or `work/` layout is required. No database or general memory
132
132
  index is introduced.
133
133
 
134
134
  ## Provider protocol
@@ -21,16 +21,17 @@ The response is `{status,reply}`. `queued` or `running` yields and resubmits the
21
21
  same event after four seconds. `complete` or `failed` supplies a final string
22
22
  reply, queued once with a deterministic outbox ID. Backend completion and
23
23
  Telegram delivery are separate; an uncertain send never replays the operation.
24
- Restart resubmits interrupted backend runs using the original ID. Permanent
24
+ A relay restart drops in-flight relay runs by design; the backend's resubmit of
25
+ the same event is the recovery path. Permanent
25
26
  4xx responses leave a failed relay run for operator inspection; transient
26
27
  failures back off. No response body or credential is logged.
27
28
 
28
29
  Native `/new` and AI settings belong to the application. `/stop` cannot cancel
29
30
  an application job and explicitly reports that limitation. `/cancel` removes
30
31
  pending relay work only; it does not undo work already accepted by a backend.
31
- External plugin/maintenance wakes do not dispatch through this channel backend.
32
+ External plugin wakes do not dispatch through this channel backend.
32
33
 
33
34
  Backend mode handles owner Telegram intake only. Existing native schedules,
34
- restricted messaging tasks, plugin events and maintenance prompts are not
35
+ restricted messaging tasks and plugin events are not
35
36
  forwarded to the application and cannot launch a fallback CLI. Their scheduling
36
37
  and authority remain separate from the application's own job lifecycle.
@@ -1,9 +1,9 @@
1
1
  # Docker runtime and shared host CLI
2
2
 
3
- Docker Compose owns each relay and executable plugin. The CLI the user installs
4
- Ez from stays on the host, with its existing authentication. All agents in that
5
- installation use that CLI; there is no second CLI installation or agent-specific
6
- CLI login. The relay image contains Node, relay dependencies and ffmpeg.
3
+ Docker Compose owns each relay and executable plugin. Isolated agents run the
4
+ native CLI from the relay image (pinned Codex). Host-capable agents reuse the
5
+ CLI the user installed Ez from, with its existing authentication. The relay
6
+ image contains Node, relay dependencies, ffmpeg and that pinned Codex CLI.
7
7
  The runtime image exposes Ez commands through the wrappers in `/app/bin`, including
8
8
  `ezenciel-agents-application`; application images do not need to link Ez themselves.
9
9
  The image also installs package-manifest commands in `/usr/local/bin`, so native
@@ -13,26 +13,56 @@ belongs to Ez packaging, not an application image or an engine-specific prompt.
13
13
  ## Agent binding
14
14
 
15
15
  Use [agent-led setup](setup.md). `ezenciel-agents-create` accepts a name, purpose,
16
+ optional `--isolation isolated|host-capable` (default `isolated`),
16
17
  a bot token through stdin; the CLI defaults to the one recorded at package
17
18
  installation by `--register-cli <current-cli>`. It creates a private deployment
18
19
  with a unique Compose project, mind, control directory and secret file. Names
19
20
  cannot overwrite existing agents. `installation.json` keeps subsequent agents
20
21
  on the same CLI. Each agent gets its own purpose and native conversation IDs.
22
+ `isolated` sets `EZ_EXECUTOR_TRANSPORT=local` and runs the native CLI in the
23
+ relay. `host-capable` sets `host` and reuses the installer UID; unlabeled
24
+ existing host transports stay host-capable so they are not flipped.
25
+
26
+ On Docker Desktop/OrbStack the control volume's Unix socket is not connectable
27
+ from the macOS host, so a host-capable agent also publishes an authenticated
28
+ loopback ledger endpoint (generated `ledger.compose.yaml` plus
29
+ `EZ_DELIVERY_TCP_PORT` in `docker.env`). The relay writes
30
+ `control/delivery-endpoint.json` (mode 600) with the port and per-start token;
31
+ host CLIs fall back to it when the socket is unreachable, and every request is
32
+ still re-authorized server-side. Isolated agents publish no endpoint. Existing
33
+ host-capable deployments created before this endpoint run
34
+ `ezenciel-agents-install migrate-ledger --deployment <absolute-directory>` once;
35
+ it is idempotent and does not restart services. If the chosen port is taken,
36
+ recreate the agent or edit `EZ_DELIVERY_TCP_PORT` and re-run Compose.
21
37
 
22
38
  `docker.env` contains explicit absolute paths, never token values:
23
39
 
24
40
  ```dotenv
25
41
  COMPOSE_PROJECT_NAME=ez-agent-family
26
42
  EZ_EXECUTOR_CLI=grok
43
+ EZ_ISOLATION=isolated
44
+ EZ_EXECUTOR_TRANSPORT=local
27
45
  EZ_AGENT_WORKSPACE=/absolute/private/agents/family/mind
28
46
  EZ_CONTROL_DIR=/absolute/private/agents/family/control
29
47
  EZ_RELAY_ENV_FILE=/absolute/private/agents/family/relay.env
30
48
  EZ_AGENT_PURPOSE_FILE=/absolute/private/agents/family/purpose.md
49
+ EZ_TOOLS_HOME=/absolute/private/agents/family/tools
50
+ EZ_PLUGIN_BROKER_SOCKET=/absolute/private/agents/family/control/plugin-broker.sock
51
+ EZ_PLUGIN_BROKER_HOST_CONFIG=/absolute/private/agents/family/host-executor.json
31
52
  ```
32
53
 
33
- The installing agent runs `ezenciel-agents-host` under the host's service manager
54
+ `ezenciel-agents-provision-telegram --config <private-config>` attaches Telegram
55
+ to an existing deployment. The private config names only the compose file,
56
+ project, secret and override paths; it never selects a relay image. The command
57
+ loads the deployment's own `docker.env`, so the relay restarts on its installed
58
+ image and version, then writes only the bot-token secret and a small override
59
+ that enables Telegram. That file must define `EZ_RELAY_IMAGE`; a shell export
60
+ never overrides it, and an incomplete record fails closed.
61
+
62
+ Host-capable agents run `ezenciel-agents-host` under the host's service manager
34
63
  with `EZ_DEPLOYMENT_DIR` bound to that deployment. This small transport invokes
35
- the existing CLI; it is not a second relay or model loop. It reads requests from
64
+ the existing CLI; it is not a second relay or model loop. Isolated agents do not
65
+ start it. It reads requests from
36
66
  the agent's control directory, fixes cwd/control/tool paths from its installation
37
67
  binding, strips environment secrets, forwards native output and exit status, and
38
68
  propagates cancellation. Requests cannot select another executable. No network
@@ -41,13 +71,18 @@ is required. Host Node 22+ and the package dependencies run this transport.
41
71
 
42
72
  Mount mind and control at identical absolute paths in Docker and on the host,
43
73
  so incoming files, message attachments and CLI outputs need no path translation.
44
- Only that agent's directories are mounted into its relay. The host CLI reuses the existing login. Codex receives a private per-agent
74
+ Only that agent's directories are mounted into its relay. Host-capable CLI reuses the existing login. Codex receives a private per-agent
45
75
  state home with linked authentication; global memory/configuration is excluded.
46
- This is not an OS security boundary between agents running as the same user.
47
- Plugin setup also binds `toolsHome` in the host configuration. Codex receives
48
- write access to that registry and network access for the Docker client; the host
49
- rejects registries belonging to a different workspace. Restart the host worker
50
- between jobs after adding a registry binding.
76
+ Host-capable is not an OS security boundary between agents running as the same user.
77
+ Isolated agents use the relay mounts; sibling host paths are not present.
78
+ Plugin setup also binds `toolsHome` in the host configuration. The isolated
79
+ runtime uses a separate `plugin-broker` Compose service: the relay receives only
80
+ the socket client and its normal mind/control mounts, while the broker receives
81
+ that agent's exact tools/workspace/control paths and the Docker socket. The
82
+ broker has no network, validates the host binding and registry owner, and is the
83
+ only service allowed to launch plugin containers. The relay never receives the
84
+ Docker socket or a tools registry mount. Host-capable agents keep the existing
85
+ host executor path instead.
51
86
 
52
87
  ## Operate and verify
53
88
 
@@ -78,19 +113,28 @@ bin/ezenciel-agents-docker up -d --wait
78
113
 
79
114
  The owner supplies a real DM; verify the pending numeric sender before pairing.
80
115
  Live smoke uses the same host CLI and requires an actual Telegram receipt.
81
- Restart preserves pairing and files. One kernel lock excludes relay/smoke
82
- writers; exit 73 means a writer is active. Do not delete its lock to bypass it.
116
+ Restart preserves pairing and files. On boot the relay binds the live delivery
117
+ socket as the single-relay guard: a second relay refuses to start while that
118
+ address answers. Do not remove a live socket to bypass the guard.
83
119
  Health requires recent polling and host-transport heartbeats, not just a process.
84
120
  Relay replacement retains the existing 30-second startup grace and deployment
85
121
  layout, so compatible releases remain eligible for agent-owned upgrades.
86
- An interrupted Telegram poller keeps authorized work and in-flight outbox writes
87
- alive while it retries intake. A `409 Conflict` still requires the operator to
88
- stop the competing poller; it remains unhealthy after the startup grace. Pending
89
- and uncertain deliveries keep their existing outbox/receipt semantics; executor
90
- stdout is not replayed as a reply.
122
+ An interrupted Telegram poller keeps authorized work and in-flight deliveries
123
+ alive while it retries intake. Run and delivery records are relay memory: a
124
+ relay process restart drops in-flight work by design, Telegram redelivery is the
125
+ intake replay path, and uncertain sends report unknown instead of success.
126
+ A `409 Conflict` still requires the operator to stop the competing poller; it
127
+ remains unhealthy after the startup grace. Pending and uncertain deliveries keep
128
+ their existing receipt semantics; executor stdout is not replayed as a reply.
91
129
  Signal and fatal-error paths share one shutdown; a secondary cleanup error is
92
130
  reported without replacing the original startup/polling failure.
93
131
 
132
+ For an isolated agent, `COMPOSE_PROFILES=isolated` starts both `relay` and
133
+ `plugin-broker`; the `up` command above must not be narrowed to `relay`. The
134
+ broker socket is private to the agent's control directory. Its per-run receipt
135
+ is written under `control/plugin-receipts/<run-id>/`; a plugin's stdout remains
136
+ the provider/API receipt and is not treated as a relay message.
137
+
94
138
  The separate `control-state.lock` serializes authority JSON updates across CLI
95
139
  processes. A forced kill or host crash can orphan this exclusive-create sentinel.
96
140
  It deliberately has no age/PID-based auto-reclamation: expiry cannot prove that a
@@ -118,6 +162,38 @@ and volume bindings. Never add standalone provider launchers or deployments.
118
162
  Read the installed skill from the registry before onboarding; do not re-pair
119
163
  an already connected account. Monitoring and sends require their own authority.
120
164
 
165
+ ### Isolated plugin commands
166
+
167
+ The relay-side `ez` client sends only declared aliases, literal argument arrays,
168
+ bounded stdin, and the active run ID to the broker. The broker re-reads owner
169
+ authority and the registry before every call, pins the discovered revision, and
170
+ passes only the installed plugin's `run.application.context.plugins[plugin-id]`
171
+ object as `EZ_PLUGIN_CONTEXT`. Missing, revoked, cross-agent, expired, or reused
172
+ admissions fail closed. Timeouts, cancellation and bounded output produce a
173
+ failed broker receipt rather than a retry.
174
+
175
+ Source-based `plugins inspect`, `catalog-add`, and `install` management calls are
176
+ accepted only for an existing path inside that agent's mounted workspace, with a
177
+ literal SHA-256 revision where the command requires one. The same management
178
+ surface permits data-preserving `plugins uninstall` and local candidate
179
+ `updates prepare/apply/recover`; candidate archives must also be inside the
180
+ workspace. Public update discovery stays with the host supervisor because the
181
+ broker has no network. The broker never turns an arbitrary host path into a
182
+ plugin mount.
183
+
184
+ Each agent has one registry and one broker binding. Registries, plugin source
185
+ snapshots, shared workers and Library/embedding state are not implicitly shared
186
+ between agents or organizations. A reviewed shared worker or an application
187
+ backend must be attached through its own explicit per-agent binding; it does not
188
+ grant the relay or Library containers Docker access.
189
+
190
+ An isolated agent created before this change has no `tools/` directory, broker
191
+ socket environment, or `COMPOSE_PROFILES` entry: recreate the agent (or backfill
192
+ `tools/`, `EZ_TOOLS_HOME`, `EZ_PLUGIN_BROKER_SOCKET`,
193
+ `EZ_PLUGIN_BROKER_HOST_CONFIG`, and the isolated profile) before its plugins
194
+ will run. Until then the broker refuses to start and health reports
195
+ `PLUGIN_BROKER_UNREADABLE` — fail-closed, never half-provisioned.
196
+
121
197
  For event intake, mount only the registered plugin project's socket/client
122
198
  exports into the relay, read-only. The event source stores a cursor and policy
123
199
  binding; it is not a second plugin installation. Provider credentials remain in
@@ -146,13 +222,22 @@ default. Existing queued jobs retain their captured execution choice.
146
222
 
147
223
  ## Codex context isolation
148
224
 
149
- The host Codex binary and existing login are reused, but each agent has its own
150
- `control/cli/codex` state directory. Only authentication is linked to the host
225
+ Isolated agents run the image Codex with `EZ_CODEX_SANDBOX=external`. Auth is a
226
+ real file at that agent's `control/cli/codex/auth.json`. Do not bind-mount the
227
+ operator `~/.codex` or `$HOME` into an isolated relay. Host-capable agents reuse
228
+ the host Codex binary and login, with a
229
+ `control/cli/codex` state directory. The native CLI is resolved from the host
230
+ PATH, not the package `binDir`, so a shared runtime or one agent's wrapper cannot
231
+ retarget another agent's `CODEX_HOME`. Only authentication is linked to the host
151
232
  login; global configuration, sessions and memories are not imported. Global
152
233
  memory and host skill discovery are disabled for relay jobs. A conversation
153
234
  that already received unrelated global context must be replaced with a fresh
154
235
  native conversation; disabling injection does not remove prior turn content.
155
236
  This prevents automatic context sharing, not adversarial access by the host user.
237
+ On macOS host jobs, Seatbelt also denies sibling directories of the bound
238
+ workspace (a tenant farm), while allowing explicitly shared and additional
239
+ workspace grants and leaving `$HOME` readable for the host CLI.
240
+ Codex then uses `danger-full-access` so it does not apply a nested Seatbelt.
156
241
 
157
242
  ## Chat latency
158
243
 
@@ -1,7 +1,8 @@
1
1
  # Host transport startup
2
2
 
3
- Register one service per deployment after initializing its tools. Substitute the
4
- exact absolute paths below. Use the existing authenticated host user and include
3
+ Register one service per **host-capable** deployment after initializing its tools.
4
+ Isolated agents run the native CLI in the relay and must not start this service.
5
+ Substitute the exact absolute paths below. Use the existing authenticated host user and include
5
6
  its Node 22+, pnpm (or Corepack), Docker and CLI directories in PATH. Resolve the
6
7
  actual installed launchers first; shell aliases and interactive shell startup
7
8
  files are not available to services. No bot token goes in these service files.
@@ -21,6 +22,7 @@ Description=Ez family host CLI transport
21
22
  Type=simple
22
23
  Environment=EZ_DEPLOYMENT_DIR=/absolute/private/agents/family
23
24
  Environment=PATH=/absolute/node/bin:/absolute/package-manager/bin:/absolute/cli/bin:/usr/local/bin:/usr/bin:/bin
25
+ EnvironmentFile=-%h/.config/ez/family-provider.env
24
26
  ExecStart=/absolute/ez-package/package/bin/ezenciel-agents-host
25
27
  Restart=on-failure
26
28
  RestartSec=5
@@ -29,6 +31,13 @@ RestartSec=5
29
31
  WantedBy=default.target
30
32
  ```
31
33
 
34
+ When the installation declares a custom Codex provider, create that private
35
+ environment file with mode `0600` and only the referenced key, for example
36
+ `OPENROUTER_API_KEY=...`. Provider and model selection do not belong in the
37
+ environment; they remain agent runtime state. The supervisor forwards only keys
38
+ explicitly named by the agent's provider binding, and the Codex subprocess still
39
+ receives none of the relay or plugin secrets.
40
+
32
41
  ```sh
33
42
  systemctl --user daemon-reload
34
43
  systemctl --user enable --now ez-family.service
@@ -81,10 +90,12 @@ main/plugin replacement. See [upgrade setup, tools and recovery](upgrades.md). E
81
90
 
82
91
  ## Shared application workspace
83
92
 
84
- Codex tasks inherit the agent's `control/cli/codex/auth.json` binding. Its default
85
- is a link to the host user's existing login. An operator may provision a private
86
- agent credential there during migration; new task homes link to that binding,
87
- preserving it rather than reverting to a different or expired host login.
93
+ Codex tasks inherit the agent's `control/cli/codex/auth.json` binding. Host-capable
94
+ default is a link to the host user's existing login. Isolated agents use a real
95
+ file in that directory and the image Codex; they do not link `~/.codex`. An
96
+ operator may provision a private agent credential there during migration; new
97
+ task homes link to that binding, preserving it rather than reverting to a
98
+ different or expired host login.
88
99
 
89
100
  An agent binding may set `sharedWorkspace` to an absolute canonical application
90
101
  repository. The host resolves its canonical path, adds that directory to Codex
package/docs/local-qa.md CHANGED
@@ -6,7 +6,7 @@ an immutable candidate in the PA's local tools directory. The agent reads its
6
6
  manifest and uses `ez updates prepare main --file ...` and `apply` on the owner's
7
7
  request. Automatic public update policy stays unchanged.
8
8
 
9
- Stage from a clean, reviewed feature checkout:
9
+ Commit, push and open a PR first — then stage only from that PR's exact commit. The staging script refuses dirty checkouts, so uncommitted work can never become a candidate. If QA finds issues, fix forward in the same PR (or a new PR) and stage a new versioned candidate; never leave a QA-only copy lingering outside the PR:
10
10
 
11
11
  ```sh
12
12
  node scripts/stage-qa.mjs --source /absolute/feature-checkout \
@@ -32,7 +32,7 @@ Add the catalog path and the following instructions to the PA's local policy fil
32
32
  > If no local entry exists, report that; do not silently substitute a public beta.
33
33
  > Follow an explicit request for a public npm release separately.
34
34
 
35
- For every feature, the developer handoff includes the beta label, exact private
35
+ For every feature, the developer handoff includes the PR URL, beta label, exact private
36
36
  version, source commit, checks completed and a short user-facing QA flow with
37
37
  expected results. Include any limitations. A prepared archive or healthy service
38
38
  does not prove the feature works; read back its result through the PA.