@jc_stack/ez-agents 0.1.0-beta.34 → 0.1.0-beta.37

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 (142) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +5 -16
  3. package/AGENTS.md +17 -9
  4. package/CHANGELOG.md +96 -0
  5. package/Dockerfile +20 -11
  6. package/README.md +10 -13
  7. package/THIRD_PARTY_NOTICES.md +7 -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 +41 -4
  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 +28 -6
  17. package/docs/application-channel.md +32 -10
  18. package/docs/architecture/ai-selection.md +10 -2
  19. package/docs/architecture/authority-boundaries.md +2 -2
  20. package/docs/channel-backend.md +4 -3
  21. package/docs/docker-runtime.md +98 -11
  22. package/docs/local-qa.md +2 -2
  23. package/docs/plugin-guide.md +202 -0
  24. package/docs/plugins.md +48 -2
  25. package/docs/releasing.md +13 -0
  26. package/docs/responsive-channels.md +1 -1
  27. package/docs/scheduling.md +4 -4
  28. package/docs/setup.md +11 -5
  29. package/docs/upgrades.md +46 -23
  30. package/package.json +5 -5
  31. package/scripts/smoke-scheduler.ts +10 -3
  32. package/scripts/smoke.ts +14 -11
  33. package/src/agent-guidance.ts +6 -9
  34. package/src/agent-install.ts +28 -7
  35. package/src/ai-cli.ts +1 -1
  36. package/src/ai.ts +131 -5
  37. package/src/application-channel.ts +78 -7
  38. package/src/application-cli.ts +2 -2
  39. package/src/approval-cli.ts +14 -7
  40. package/src/approval.ts +27 -37
  41. package/src/channel-backend.ts +2 -3
  42. package/src/config.ts +12 -27
  43. package/src/control-state.ts +175 -8
  44. package/src/conversation-menu.ts +6 -17
  45. package/src/delivery-socket-client.d.mts +4 -0
  46. package/src/delivery-socket-client.mjs +104 -0
  47. package/src/delivery-socket.ts +354 -0
  48. package/src/execution-authority.ts +21 -5
  49. package/src/executor.ts +81 -24
  50. package/src/files.ts +38 -11
  51. package/src/host-executor.ts +33 -17
  52. package/src/inbox.ts +15 -49
  53. package/src/index.ts +98 -54
  54. package/src/install-tools.mjs +97 -22
  55. package/src/ledger-endpoint.d.mts +2 -0
  56. package/src/ledger-endpoint.mjs +17 -0
  57. package/src/menu.ts +8 -3
  58. package/src/message-history.ts +13 -18
  59. package/src/message.ts +35 -18
  60. package/src/plugin-broker.mjs +396 -0
  61. package/src/plugins/connection.mjs +2 -2
  62. package/src/plugins/manager.mjs +131 -23
  63. package/src/plugins/native-tasks.mjs +2 -2
  64. package/src/prompt-suffix.ts +12 -0
  65. package/src/react.ts +6 -4
  66. package/src/runs.ts +156 -243
  67. package/src/schedule-cli.ts +29 -10
  68. package/src/scheduled-tasks.ts +53 -10
  69. package/src/scheduler.ts +20 -7
  70. package/src/service.ts +3 -3
  71. package/src/setup.ts +1 -0
  72. package/src/software-status.ts +36 -4
  73. package/src/task-executor.ts +2 -2
  74. package/src/task-workspace.ts +4 -3
  75. package/src/tasks.ts +1 -1
  76. package/src/telegram-provisioning-cli.ts +22 -0
  77. package/src/telegram-provisioning.ts +214 -0
  78. package/src/updates/control.mjs +77 -7
  79. package/src/updates/runtime.mjs +28 -8
  80. package/src/updates/status.mjs +25 -3
  81. package/src/updates/supervisor.mjs +15 -16
  82. package/src/version.ts +20 -0
  83. package/src/workspace.ts +3 -9
  84. package/templates/failure-review.md +1 -1
  85. package/templates/maintainer-purpose.md +1 -1
  86. package/test/agent-guidance.test.ts +14 -80
  87. package/test/agent-install.test.ts +14 -0
  88. package/test/ai.test.ts +220 -1
  89. package/test/application-channel.test.ts +61 -13
  90. package/test/application-only.test.ts +14 -13
  91. package/test/channel-backend.test.ts +3 -7
  92. package/test/channel-delivery.test.ts +4 -1
  93. package/test/channel-owner.test.ts +6 -3
  94. package/test/codex-context.test.ts +2 -2
  95. package/test/config.test.ts +38 -43
  96. package/test/control-state.test.ts +83 -4
  97. package/test/conversation-menu.test.ts +2 -4
  98. package/test/delivery-transport.test.ts +72 -0
  99. package/test/docker-runtime.test.ts +27 -14
  100. package/test/engine-handoff.test.ts +7 -6
  101. package/test/event-sources.test.ts +2 -2
  102. package/test/execution-authority.test.ts +9 -9
  103. package/test/executor.test.ts +120 -12
  104. package/test/failure.test.ts +12 -5
  105. package/test/files.test.ts +2 -2
  106. package/test/helpers/ledger.ts +18 -0
  107. package/test/host-executor.test.ts +87 -3
  108. package/test/inbox.test.ts +8 -10
  109. package/test/install-config.test.ts +1 -0
  110. package/test/install-tools.test.mjs +74 -7
  111. package/test/intake-relay.test.ts +58 -11
  112. package/test/message-history.test.ts +39 -47
  113. package/test/native-tasks.test.ts +1 -1
  114. package/test/plugin-broker.test.mjs +172 -0
  115. package/test/plugin-manager.test.mjs +136 -2
  116. package/test/reaction.test.ts +4 -0
  117. package/test/relay.test.ts +5 -7
  118. package/test/repair-policy.test.ts +1 -4
  119. package/test/schedule-cli.test.ts +14 -9
  120. package/test/scheduled-tasks.test.ts +29 -6
  121. package/test/scheduler-host.test.ts +5 -5
  122. package/test/scheduler-relay.test.ts +15 -9
  123. package/test/security.test.ts +6 -3
  124. package/test/software-status.test.ts +21 -3
  125. package/test/telegram-provisioning.test.ts +160 -0
  126. package/test/updates.test.mjs +93 -13
  127. package/test/workspace.test.ts +4 -1
  128. package/bin/ezenciel-agents-watch.mjs +0 -8
  129. package/compose.workforce-watch.yaml +0 -33
  130. package/docker/recovery.ts +0 -11
  131. package/docs/pagerduty.md +0 -42
  132. package/docs/workforce-watch.md +0 -101
  133. package/src/pagerduty.ts +0 -109
  134. package/src/reply-context.ts +0 -43
  135. package/src/update-attention.ts +0 -18
  136. package/src/workforce-watch-cli.ts +0 -14
  137. package/src/workforce-watch.ts +0 -155
  138. package/templates/agent/AGENTS.md +0 -5
  139. package/templates/agent-guidance.md +0 -17
  140. package/test/pagerduty.test.ts +0 -104
  141. package/test/update-attention.test.ts +0 -20
  142. package/test/workforce-watch.test.ts +0 -180
package/docker/smoke.mjs CHANGED
@@ -21,7 +21,7 @@ try {
21
21
  '--mount',`type=bind,src=${join(dir,'purpose.md')},dst=/run/agent-purpose.md,readonly`,'-e','EZ_AGENT_PURPOSE_FILE=/run/agent-purpose.md',
22
22
  '--tmpfs','/tmp:mode=1777','--tmpfs','/state/control:uid=20000,gid=20000,mode=700',
23
23
  '--tmpfs','/state/home:uid=20000,gid=20000,mode=700','--tmpfs','/workspace:uid=20000,gid=20000,mode=700',
24
- '-e','EZ_TELEGRAM_ENABLED=false','-e','EZ_APPLICATION_PORT=8110','-e','EZ_ISOLATION=isolated','-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'];
25
25
  const help = run(['run','--rm',...nonroot,image,'application','--help']);
26
26
  assert.equal(help.status,0,help.stderr);
27
27
  assert.match(help.stdout,/ezenciel-agents-application/);
@@ -72,7 +72,23 @@ try {
72
72
  assert.equal(JSON.parse(custom.stdout).uid,20001);
73
73
  const failed = run(['run','--rm',image,'exec','node','-e','process.exit(23)']);
74
74
  assert.equal(failed.status, 23, failed.stderr);
75
- 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']);
76
92
  assert.equal(held.status,0,held.stderr);
77
93
  for (let n=0;n<30;n++) {
78
94
  const probe=run(['exec',holder,'test','-f','/state/control/ready']);
@@ -80,10 +96,16 @@ try {
80
96
  await new Promise(resolve=>setTimeout(resolve,100));
81
97
  }
82
98
  assert.equal(run(['exec',holder,'test','-f','/state/control/ready']).status,0);
83
- 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]);
84
100
  assert.equal(duplicate.status,73,duplicate.stderr);
85
101
  assert.equal(run(['kill',holder]).status,0);
86
- const recovered=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e','process.exit(0)']);
87
- assert.equal(recovered.status,0,recovered.stderr);
88
- 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, 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.');
89
111
  } finally { run(['rm','-f',holder,application]); run(['volume','rm',volume]); rmSync(dir, {recursive:true, force:true}); }
@@ -277,13 +277,30 @@ This works without Telegram and requires no transcript replay or new runner.
277
277
  Scoped application conversations remain separate when the flag is omitted.
278
278
  Owner identity does not automatically merge histories or grant learner access.
279
279
 
280
- To link Telegram, enable its ordinary bot configuration, send a real DM, then
281
- 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`.
282
283
  `ezenciel-agents-owner unlink-telegram` removes that channel without removing the
283
284
  owner, application bindings or native sessions. Relinking does not authorize old
284
285
  Telegram deliveries. Linking other providers requires an authenticated adapter;
285
286
  registering the label `phone` does not install a phone service.
286
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
+
287
304
  The legacy `--share-telegram`, `--share-active` and `followTelegram:true` spellings remain supported.
288
305
 
289
306
  ### Existing scoped Telegram sharing
@@ -325,12 +342,17 @@ a generic HTTP error or revoked credential alone is not proof of termination.
325
342
 
326
343
  ## Application-only deployment (no Telegram bot)
327
344
 
328
- 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
329
346
  `EZ_APPLICATION_PORT` (plus `EZ_APPLICATION_HOST` when other containers connect).
330
- No bot token is required or used. This mode runs the existing native executor,
347
+ This mode runs the existing native executor,
331
348
  application queue and application outbox without creating a Telegram client,
332
- starting Telegram sources/polling, or registering bot commands. Default deployments
333
- 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.
334
356
 
335
357
  The installing administrator can initialize empty control authority and register
336
358
  an application in one local command:
@@ -385,14 +407,14 @@ up to 10 MiB decoded; empty files and other types are rejected. Core detects fil
385
407
  content rather than trusting a client MIME label. This same limit and staging
386
408
  contract apply to Telegram photo/document input.
387
409
 
388
- Authentication precedes staging. Core stages the file privately in the ordinary
389
- workspace inbox and uses the same attachment metadata and literal comment as
390
- 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
391
413
  remain authoritative; applications must not extract, summarize, convert or build
392
414
  attachment prompts. Use `followOwner:true` for the shared owner conversation.
393
415
  Retry the same request ID, filename, bytes and literal comment; conflicts return
394
416
  409 and successful retries do not stage another file. A failed admission removes
395
- 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
396
418
  partial request. Separate owner runtimes must retain separate workspace mounts.
397
419
 
398
420
  This is inbound file support. Application replies currently deliver text only;
@@ -34,8 +34,16 @@ References used for the adapter contract:
34
34
  - [OpenCode native run events](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/run.ts)
35
35
 
36
36
  The catalog is local metadata, not an authentication or billing health check.
37
- Grok/Codex support explicit listed model/effort choices. Other installed clients
38
- offer their own default only in this slice. Refresh by opening the native client;
37
+ Grok/Codex/OpenCode support explicit listed model/effort choices. Other installed clients
38
+ offer their own default only in this slice. OpenCode lists the installed `opencode models`
39
+ catalog with the whitelisted executor environment, so only models the relay can actually
40
+ run are offered; provider variants become selectable efforts (`--variant`).
41
+ Authenticated providers appear only when the agent-bound
42
+ `control/cli/opencode/auth.json` binding exists (host runs keep the installer
43
+ login); otherwise the free tier lists. `EZ_OPENCODE_PROVIDERS` optionally
44
+ restricts the catalog to named providers (e.g. `opencode-go`); a set
45
+ allowlist that matches nothing offers no OpenCode choice rather than falling
46
+ back outside it. Refresh by opening the native client;
39
47
  the relay does not install models, manage subscriptions or guess aliases.
40
48
 
41
49
  Setup initialization and relay startup seed one default choice per installed client.
@@ -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.
@@ -23,6 +23,18 @@ on the same CLI. Each agent gets its own purpose and native conversation IDs.
23
23
  relay. `host-capable` sets `host` and reuses the installer UID; unlabeled
24
24
  existing host transports stay host-capable so they are not flipped.
25
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.
37
+
26
38
  `docker.env` contains explicit absolute paths, never token values:
27
39
 
28
40
  ```dotenv
@@ -34,8 +46,19 @@ EZ_AGENT_WORKSPACE=/absolute/private/agents/family/mind
34
46
  EZ_CONTROL_DIR=/absolute/private/agents/family/control
35
47
  EZ_RELAY_ENV_FILE=/absolute/private/agents/family/relay.env
36
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
37
52
  ```
38
53
 
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
+
39
62
  Host-capable agents run `ezenciel-agents-host` under the host's service manager
40
63
  with `EZ_DEPLOYMENT_DIR` bound to that deployment. This small transport invokes
41
64
  the existing CLI; it is not a second relay or model loop. Isolated agents do not
@@ -52,10 +75,14 @@ Only that agent's directories are mounted into its relay. Host-capable CLI reuse
52
75
  state home with linked authentication; global memory/configuration is excluded.
53
76
  Host-capable is not an OS security boundary between agents running as the same user.
54
77
  Isolated agents use the relay mounts; sibling host paths are not present.
55
- Plugin setup also binds `toolsHome` in the host configuration. Codex receives
56
- write access to that registry and network access for the Docker client; the host
57
- rejects registries belonging to a different workspace. Restart the host worker
58
- between jobs after adding a registry binding.
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.
59
86
 
60
87
  ## Operate and verify
61
88
 
@@ -86,19 +113,28 @@ bin/ezenciel-agents-docker up -d --wait
86
113
 
87
114
  The owner supplies a real DM; verify the pending numeric sender before pairing.
88
115
  Live smoke uses the same host CLI and requires an actual Telegram receipt.
89
- Restart preserves pairing and files. One kernel lock excludes relay/smoke
90
- 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.
91
119
  Health requires recent polling and host-transport heartbeats, not just a process.
92
120
  Relay replacement retains the existing 30-second startup grace and deployment
93
121
  layout, so compatible releases remain eligible for agent-owned upgrades.
94
- An interrupted Telegram poller keeps authorized work and in-flight outbox writes
95
- alive while it retries intake. A `409 Conflict` still requires the operator to
96
- stop the competing poller; it remains unhealthy after the startup grace. Pending
97
- and uncertain deliveries keep their existing outbox/receipt semantics; executor
98
- 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.
99
129
  Signal and fatal-error paths share one shutdown; a secondary cleanup error is
100
130
  reported without replacing the original startup/polling failure.
101
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
+
102
138
  The separate `control-state.lock` serializes authority JSON updates across CLI
103
139
  processes. A forced kill or host crash can orphan this exclusive-create sentinel.
104
140
  It deliberately has no age/PID-based auto-reclamation: expiry cannot prove that a
@@ -126,6 +162,38 @@ and volume bindings. Never add standalone provider launchers or deployments.
126
162
  Read the installed skill from the registry before onboarding; do not re-pair
127
163
  an already connected account. Monitoring and sends require their own authority.
128
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
+
129
197
  For event intake, mount only the registered plugin project's socket/client
130
198
  exports into the relay, read-only. The event source stores a cursor and policy
131
199
  binding; it is not a second plugin installation. Provider credentials remain in
@@ -171,6 +239,25 @@ workspace (a tenant farm), while allowing explicitly shared and additional
171
239
  workspace grants and leaving `$HOME` readable for the host CLI.
172
240
  Codex then uses `danger-full-access` so it does not apply a nested Seatbelt.
173
241
 
242
+ ## OpenCode auth
243
+
244
+ Isolated agents run the image OpenCode with `XDG_DATA_HOME` set to the
245
+ agent-bound `control/cli` directory only when the owner provisioned
246
+ `control/cli/opencode/auth.json` there (mode `600`, copied once from the host
247
+ `opencode auth login`). Authenticated providers then list and run; otherwise
248
+ only the free tier is offered and paid turns fail closed with the provider
249
+ error. Only the directory path crosses into the executor environment, never a
250
+ secret. Host-capable agents reuse the host login and ignore the binding.
251
+
252
+ `EZ_OPENCODE_PROVIDERS` optionally restricts the catalog to named providers,
253
+ e.g. `opencode-go` for Go credentials only. Isolated relays read it from
254
+ their environment (`docker.env` through the relay `environment`); the host
255
+ transport is spawned with a scrubbed environment, so host-capable agents set
256
+ `opencodeProviders` on the agent's `host-executor.json` binding instead
257
+ (validated the same way, unknown fields ignored by older releases). Unset or
258
+ absent means unfiltered; a set scope that matches nothing offers no OpenCode
259
+ choice rather than falling back outside it.
260
+
174
261
  ## Chat latency
175
262
 
176
263
  Interactive Codex CLI turns resume the existing native session and use native
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.
@@ -0,0 +1,202 @@
1
+ # Build, install, and publish an Ez plugin
2
+
3
+ This is the shareable guide for open-source Ez plugin authors. It covers the
4
+ smallest path from a plugin checkout to a working private installation, and
5
+ the separate path for publishing and listing a public product.
6
+
7
+ The detailed documents linked below remain the normative references for release
8
+ and security edge cases. This page is the front door.
9
+
10
+ ## The important distinction
11
+
12
+ An Ez plugin can be useful before it is published. These are separate states:
13
+
14
+ | State | What it means | What is required |
15
+ | --- | --- | --- |
16
+ | Local-only | You install your own reviewed checkout or tarball on your agent. | A valid package, an inspected content hash, and real executor verification. No npm publication or catalog entry. |
17
+ | Catalog-listed | Ez publicly records the plugin as a product capability. | A catalog PR with the canonical repository, package identity, capability, and release links. Registration does not prove that a release exists. |
18
+ | Released | Other users can install an immutable package from npm/GitHub. | Release checks, exact artifact/hash readback, a tagged GitHub release, registry publication, and clean-host verification. |
19
+
20
+ Do not treat a catalog entry, a GitHub workflow, or a package name as proof that
21
+ an artifact was published. Read back the actual npm and GitHub release records.
22
+
23
+ ## 1. Define the plugin boundary
24
+
25
+ Use a plugin when the capability needs provider or domain authentication,
26
+ deterministic validation, private state, or provider receipts. The native AI
27
+ engine owns inference, sessions, context, reasoning, tool choice, and
28
+ continuation. Ez core owns authorization, transport, scheduling, cancellation,
29
+ session binding, and delivery receipts.
30
+
31
+ Keep provider behavior out of the relay and do not add another agent runner,
32
+ model router, conversation engine, prompt-history store, or competing queue.
33
+ General plugins live in the Ez plugin repositories; a product-specific plugin
34
+ stays with the product that owns its canonical data and policy. See the
35
+ [plugin architecture](../../planning/docs/architecture-plugins.md).
36
+
37
+ ## 2. Build the smallest valid package
38
+
39
+ At minimum, provide:
40
+
41
+ - `package.json` with a unique scoped package name, SemVer version, repository
42
+ URLs, `files` allowlist, and supported Node/package-manager versions.
43
+ - A frozen lockfile, `LICENSE`, and `THIRD_PARTY_NOTICES.md`.
44
+ - `README.md` explaining purpose, requirements, exact install/start/onboarding
45
+ commands, one working read example, identity/account binding, limitations,
46
+ and troubleshooting.
47
+ - `Dockerfile` and `.dockerignore` when the plugin has a managed service.
48
+ - `ez-plugin.json` with the stable plugin id, matching version, commands, and
49
+ skill paths. Declare command exposure for external content, external sends,
50
+ record changes, and requested review.
51
+ - `ez-deployment.json` when the plugin has managed services, volumes,
52
+ healthchecks, exports, or deployment dependencies.
53
+ - A skill such as `skills/<plugin>/SKILL.md` covering required inputs, private
54
+ credential handling, external handoffs, resumption/recovery, and readiness
55
+ evidence.
56
+ - Offline contract/negative tests, `--help`, a read-only doctor, bounded
57
+ machine-readable commands, stable exit codes, and readback for writes.
58
+
59
+ The manager snapshots the package according to `package.json.files` plus the
60
+ required manifests and Docker inputs. Keep one canonical checkout; do not keep
61
+ permanent packaged copies or a second registry. See the full
62
+ [plugin contribution requirements](plugin-contributions.md) and the
63
+ [plugin manager contract](plugins.md#deployment-descriptors).
64
+
65
+ A typical package layout is:
66
+
67
+ ```text
68
+ plugin/
69
+ ├── bin/ # executable entry points
70
+ ├── skills/<plugin>/SKILL.md
71
+ ├── src/
72
+ ├── test/
73
+ ├── Dockerfile # if a service is managed
74
+ ├── ez-plugin.json
75
+ ├── ez-deployment.json # if a service is managed
76
+ ├── package.json
77
+ ├── pnpm-lock.yaml
78
+ ├── README.md
79
+ ├── LICENSE
80
+ └── THIRD_PARTY_NOTICES.md
81
+ ```
82
+
83
+ ## 3. Test and install it before publishing
84
+
85
+ From the plugin checkout, run the package's documented checks. The standard
86
+ baseline is:
87
+
88
+ ```sh
89
+ pnpm install --frozen-lockfile
90
+ pnpm verify
91
+ npm run release:check
92
+ git diff --check
93
+ ```
94
+
95
+ Run the plugin's Docker/manager smoke test when it has a managed service.
96
+ Verify installation, start/stop, the registered command, restart persistence,
97
+ and non-destructive uninstall using synthetic data. Then run one authorized,
98
+ supported operation through the actual bound executor and verify its canonical
99
+ result or provider receipt.
100
+
101
+ ### Install from local source or an unpublished tarball
102
+
103
+ Publication is not required. Inspect the exact source first:
104
+
105
+ ```sh
106
+ ez plugins inspect <id> --source /absolute/plugin
107
+ ```
108
+
109
+ Use the returned revision for either a one-off install:
110
+
111
+ ```sh
112
+ ez plugins install <id> --source /absolute/plugin --revision sha256:<inspected-hash>
113
+ ez plugins start <id>
114
+ ```
115
+
116
+ Or save the reviewed source in the agent's local catalog, then install it by
117
+ the catalog entry:
118
+
119
+ ```sh
120
+ ez plugins catalog-add <id> --source /absolute/plugin --revision sha256:<inspected-hash>
121
+ ez plugins install <id>
122
+ ez plugins start <id>
123
+ ```
124
+
125
+ After startup, verify the installed command from the actual executor:
126
+
127
+ ```sh
128
+ ez plugins status <id>
129
+ ez tools list
130
+ ez <command> doctor --json
131
+ ```
132
+
133
+ Use the plugin's real supported operation as the acceptance test. Health,
134
+ registration, or a successful `--help` command alone is not completion. A
135
+ changed source must be inspected again; never install a stale catalog hash.
136
+
137
+ ## 4. Prepare a public release
138
+
139
+ Use an isolated release worktree and reviewed PR. Then:
140
+
141
+ 1. Bump the package and plugin manifest to the same new SemVer version. Add
142
+ changelog and migration notes for breaking CLI or state changes.
143
+ 2. Run frozen dependency installation, package tests, release checks, Docker
144
+ checks, `git diff --check`, dependency/license review, and the actual manager
145
+ smoke test.
146
+ 3. Create the candidate with `npm pack --ignore-scripts`. Inspect its file list,
147
+ compute and record its SHA-256, extract it into a fresh directory, reinstall
148
+ from the artifact, and repeat the CLI/runtime checks there.
149
+ 4. For a first release or onboarding change, install that exact candidate on a
150
+ disposable clean host and verify the real executor, restart/boot behavior,
151
+ account binding, and first supported operation.
152
+ 5. With explicit release authorization, tag the reviewed source, publish the
153
+ exact tested artifact, create the matching GitHub prerelease/release, and
154
+ read back npm metadata, the downloaded tarball hash, release assets, and
155
+ source/tag identity.
156
+ 6. Install the published version on a clean host and repeat the relevant
157
+ verification before calling the release complete.
158
+
159
+ For the complete release checklist, use [Releasing](releasing.md). For
160
+ unattended beta publication, use [Verified beta publication](trusted-publishing.md);
161
+ it defines the immutable GitHub asset, sanitized receipt, OIDC trust, dispatch,
162
+ and failure-reconciliation rules. Do not put npm tokens in CI or publish merely
163
+ to test authentication.
164
+
165
+ ## 5. List it as a public product
166
+
167
+ Catalog registration is a discovery and product step, not installation
168
+ authority. Open a PR updating [Available plugins](plugin-catalog.md) with:
169
+
170
+ - the product name and one concrete capability description;
171
+ - the canonical public source repository;
172
+ - the exact npm package identity; and
173
+ - canonical npm and GitHub release links.
174
+
175
+ Only advertise a version as installable after verifying that it exists in the
176
+ registry and in the matching GitHub release. A public catalog entry may exist
177
+ while its first release is still pending; label that state clearly and do not
178
+ present it as an installable version. Keep local/private plugins out of the
179
+ public catalog; they can still be installed by reviewed source and an inspected
180
+ hash.
181
+
182
+ ## The short checklist
183
+
184
+ ### Own agent / unpublished
185
+
186
+ - [ ] Package, manifests, skill, README, license/notices, and tests exist.
187
+ - [ ] `pnpm verify`, release checks, and the manager/Docker smoke pass.
188
+ - [ ] Source or tarball is inspected and installed with its returned hash.
189
+ - [ ] The actual bound executor performs a real supported operation.
190
+
191
+ ### Public product
192
+
193
+ - [ ] The exact candidate is packed, inspected, hashed, and clean-host tested.
194
+ - [ ] Release authorization, tag, GitHub asset, npm publication, and readback
195
+ all refer to the same source and bytes.
196
+ - [ ] A clean installation of the published version is verified.
197
+ - [ ] The public catalog is updated only with verified repository/package/release
198
+ records.
199
+
200
+ For contribution, security, and removal details, follow the links above and the
201
+ plugin's own `CONTRIBUTING.md`, `SECURITY.md`, and README.
202
+
package/docs/plugins.md CHANGED
@@ -48,6 +48,31 @@ Job requests cannot override this host binding. Filesystem access remains
48
48
  `workspace-write`; other agents' directories are not added. Docker access is
49
49
  host administration under the existing trusted-host model, not OS isolation.
50
50
 
51
+ For an isolated Codex agent, this binding is consumed by the per-agent
52
+ `plugin-broker` service, not by a host executor. Start the deployment only after
53
+ initialization with `COMPOSE_PROFILES=isolated`; Compose then starts the relay
54
+ and broker together. The relay-side `ez` command has no tools-home or Docker
55
+ socket mount. It sends a declared alias, literal arguments, bounded stdin and
56
+ the active run ID over the private control-directory Unix socket. The broker
57
+ revalidates the exact registry/host/workspace ownership, pinned revision and
58
+ owner-authorized run before launching the plugin container.
59
+
60
+ The broker writes bounded invocation metadata to
61
+ `control/plugin-receipts/<run-id>/`. Plugin stdout/stderr and exit status remain
62
+ available to the native command, including provider/API receipts. Application
63
+ context is narrowed to the installed plugin ID and forwarded as opaque
64
+ `EZ_PLUGIN_CONTEXT`; it is absent from ordinary native commands. No registry,
65
+ plugin container, shared worker, Library index or embedding store is shared
66
+ across agents unless an operator adds an explicit reviewed binding.
67
+
68
+ The same broker accepts source-based `plugins inspect`, `catalog-add`, and
69
+ `install` only when the source is an existing path under the bound workspace and
70
+ the revision is literal and pinned. It also exposes data-preserving
71
+ `plugins uninstall` and the local-file `updates prepare/apply/recover` lifecycle.
72
+ Candidate archives must be staged inside the bound workspace; public registry
73
+ discovery remains host-supervisor work because the broker has no network. Paths
74
+ outside that workspace are rejected.
75
+
51
76
  ## Installation completion contract
52
77
 
53
78
  For initial Ez onboarding, plugin requests happen in the working Telegram
@@ -255,6 +280,26 @@ never retries a provider operation. Service startup/restart is explicit; command
255
280
  never implicitly start a stopped provider service. Plugin data does not enter the
256
281
  relay. The manager sends only a whitelist of Docker client environment variables.
257
282
 
283
+ ### Application plugin context
284
+
285
+ An authenticated application can attach opaque domain data under
286
+ `context.plugins.<plugin-id>` when it submits a run. During that active,
287
+ authorized application run, the matching plugin command container receives only
288
+ that object as JSON in `EZ_PLUGIN_CONTEXT`. No other plugin context, core control
289
+ state, owner identity, or application context is forwarded. Ordinary native runs
290
+ and commands launched outside an application run receive no such environment
291
+ variable. The manager authorizes the active run and application binding at command
292
+ admission (prepare time) and rejects revoked authority there; revocation between
293
+ admission and container start is a documented residual window. Transport is a
294
+ `0o600` one-shot compose override consumed via `--file` that sets the command
295
+ service environment directly, never process arguments; the value remains
296
+ visible in `docker inspect` while the container runs.
297
+
298
+ Use this for a short-lived, domain-scoped capability or an application service
299
+ address. It is not prompt text, a general secret channel, or an environment
300
+ override. Plugin code must validate the object, avoid printing it, and send it
301
+ only to its authoritative service over the reviewed transport.
302
+
258
303
  ## Verification
259
304
 
260
305
  `npm run verify` runs offline negative/contract tests. Explicit Docker integration:
@@ -343,8 +388,9 @@ on the next read, without hooks, LLM calls or a cached inventory file.
343
388
 
344
389
  `ez tools list` retains its alias mapping for existing clients. Native AGENTS.md
345
390
  contains only the agent-bound discovery shortcut. New workspaces do not seed
346
- TOOLS.md; upgrades preserve legacy notes without rewriting them. Keep owner/account
347
- policies in agent instructions or linked policy files, separate from plugin metadata.
391
+ TOOLS.md, SOUL.md, USER.md, or an `inbox/`/`work/` layout; upgrades preserve
392
+ legacy notes without rewriting them. Keep owner/account policies in agent
393
+ instructions or linked policy files, separate from plugin metadata.
348
394
 
349
395
  Folder bindings default to read-only. For an explicitly authorized plugin that
350
396
  updates the existing source, add `--writable` to `folder-bind` while the plugin