@jc_stack/ez-agents 0.1.0-beta.27 → 0.1.0-beta.29

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 (75) hide show
  1. package/.env.example +9 -0
  2. package/AGENTS.md +25 -1
  3. package/CHANGELOG.md +29 -0
  4. package/CONTRIBUTING.md +28 -0
  5. package/Dockerfile +1 -0
  6. package/README.md +79 -8
  7. package/bin/ezenciel-agents-application +2 -0
  8. package/bin/ezenciel-agents-application.mjs +16 -0
  9. package/compose.yaml +8 -0
  10. package/docker/entrypoint.sh +20 -2
  11. package/docker/healthcheck.mjs +1 -1
  12. package/docker/run.ts +3 -3
  13. package/docker/smoke.mjs +41 -2
  14. package/docs/application-channel.md +366 -0
  15. package/docs/docker-runtime.md +20 -0
  16. package/docs/managed-applications.md +68 -0
  17. package/docs/plugin-catalog.md +1 -0
  18. package/docs/plugin-connection.md +76 -0
  19. package/docs/plugins.md +40 -4
  20. package/docs/upgrades.md +11 -1
  21. package/package.json +7 -2
  22. package/src/application-channel.ts +308 -0
  23. package/src/application-cli.ts +41 -0
  24. package/src/application-client.mjs +87 -0
  25. package/src/application-origin.ts +15 -0
  26. package/src/codex-session.ts +5 -3
  27. package/src/config.ts +20 -2
  28. package/src/control-state.ts +256 -15
  29. package/src/conversation-menu.ts +89 -0
  30. package/src/delivery-context.d.mts +5 -0
  31. package/src/delivery-context.mjs +25 -0
  32. package/src/event-sources.ts +2 -2
  33. package/src/execution-authority.ts +2 -0
  34. package/src/executor.ts +10 -4
  35. package/src/host-executor.ts +7 -1
  36. package/src/identity.ts +11 -3
  37. package/src/index.ts +149 -54
  38. package/src/menu.ts +26 -9
  39. package/src/message-history.ts +52 -0
  40. package/src/message.ts +48 -7
  41. package/src/owner.ts +7 -1
  42. package/src/plugins/connection-artifacts.mjs +31 -0
  43. package/src/plugins/connection.mjs +124 -0
  44. package/src/plugins/manager.mjs +63 -18
  45. package/src/plugins/native-tasks.d.mts +4 -0
  46. package/src/plugins/native-tasks.mjs +66 -0
  47. package/src/plugins/workspace-lease.d.mts +3 -0
  48. package/src/plugins/workspace-lease.mjs +44 -0
  49. package/src/runs.ts +67 -9
  50. package/src/schedule-cli.ts +11 -6
  51. package/src/scheduler.ts +17 -7
  52. package/src/updates/control.mjs +4 -0
  53. package/src/web-launcher.ts +19 -0
  54. package/templates/agent-guidance.md +58 -2
  55. package/templates/deployments.md +24 -0
  56. package/test/application-channel.test.ts +283 -0
  57. package/test/application-client.test.mjs +84 -0
  58. package/test/application-controls.test.ts +224 -0
  59. package/test/application-only.test.ts +100 -0
  60. package/test/channel-delivery.test.ts +63 -0
  61. package/test/channel-owner.test.ts +161 -0
  62. package/test/codex-session.test.ts +8 -5
  63. package/test/config.test.ts +15 -0
  64. package/test/connection-artifacts.test.mjs +32 -0
  65. package/test/conversation-menu.test.ts +67 -0
  66. package/test/conversations.test.ts +84 -0
  67. package/test/executor.test.ts +56 -0
  68. package/test/host-executor.test.ts +28 -0
  69. package/test/intake-relay.test.ts +126 -5
  70. package/test/message-history.test.ts +127 -0
  71. package/test/native-tasks.test.ts +36 -0
  72. package/test/plugin-connection.test.mjs +124 -0
  73. package/test/plugin-manager.test.mjs +34 -0
  74. package/test/runtime-identity.test.mjs +18 -0
  75. package/test/updates.test.mjs +39 -0
@@ -0,0 +1,366 @@
1
+ # Application input to the standard agent
2
+
3
+ An installed application can submit work to the ordinary Ez agent. Ez uses its
4
+ existing run queue, native executor, session store, cancellation and message
5
+ outbox. The application supplies domain tools and keeps its UI/content API; it
6
+ can remove its own model runner, continuation loop and engine authentication.
7
+ This is separate from the older outbound channel-backend integration.
8
+
9
+ ## Shared runtime controls
10
+
11
+ Register with `--share-owner` (the existing `--share-telegram` spelling is an
12
+ alias) to grant access to the runtime's active conversation and standard AI
13
+ settings. This explicit grant works with or without a Telegram bot. It grants
14
+ control of the same active state Telegram uses when enabled; it is not a
15
+ scope-only permission. Ordinary application bindings cannot read or change it.
16
+
17
+ `GET /v1/control` returns the existing Ez AI presets, installed native model
18
+ catalog, active control-session ID and conversations visible through `/chats`.
19
+ Native engine session IDs and private application scopes are omitted. The token
20
+ stays in the authenticated application backend, never in browser code.
21
+
22
+ `POST /v1/control` accepts one action and the `expectedSession` returned by the
23
+ last read (null before a conversation exists):
24
+
25
+ | Action | Additional fields | Existing Ez operation |
26
+ | --- | --- | --- |
27
+ | `new` | None | `/new`, using the default AI |
28
+ | `switch` | `sessionId` from the visible conversation list | `/chats` selection |
29
+ | `select` | `presetId` from `ai.presets` | Select a saved AI |
30
+ | `model` | `cli`, optional `model` and `effort`, from `models` | Save/select the same native choice as `/ai` |
31
+
32
+ Engine changes start a fresh conversation through the same selection operation
33
+ as Telegram. A changed active conversation rejects a stale mutation; refresh
34
+ controls before another attempt. Model changes within one conversation retain
35
+ the ordinary last-selection-wins behavior. Already-admitted runs keep their
36
+ captured conversation and AI. Uncertain control responses require a fresh read,
37
+ not blind repetition of `/new`.
38
+
39
+ For private application conversations, `GET /v1/scope-control?scope=<encoded-scope>` returns
40
+ the same catalog and the current scope's public control ID and preset. The scope
41
+ is the application's original admission scope, resolved under its authenticated
42
+ binding. `POST` accepts `new`, `select` and `model` with that scope's
43
+ `expectedSession`. It does not require shared-control permission. Shared scopes
44
+ must use `/v1/control` instead.
45
+
46
+ A scope reset uses the configured default AI. A client change starts a fresh
47
+ private conversation; a model change within the same client retains it. Retired
48
+ private conversations stay hidden from `/chats`; admitted work can finish in its
49
+ original native session. Neither operation changes the shared active selection.
50
+ Control mutations are never marked retryable by the public client: read back
51
+ after uncertainty before deciding on another change.
52
+
53
+ Current HTTP gaps: rename/archive, scheduling
54
+ administration and shared-chat stop-all are not exposed here. Per-run application
55
+ cancellation remains available. The native agent can use the standard scheduler
56
+ from an application turn; its replies retain that channel binding.
57
+
58
+ ## Install and authorize
59
+
60
+ An installation has one owner, independent of its channels. As the installing
61
+ administrator, outside an agent turn, generate a random 32-byte base64url token
62
+ into a private file and register the first channel with a verified opaque owner ID:
63
+
64
+ ```sh
65
+ ezenciel-agents-application --owner-id verified-account --id web --token-file /private/web-token --share-owner
66
+ ezenciel-agents-application --id phone --token-file /private/phone-token --share-owner
67
+ ```
68
+
69
+ The application backend receives that token through its secret store. Core stores
70
+ only its SHA-256 digest, bound to the installation owner. Channel IDs are arbitrary
71
+ labels, not a predefined provider list. The trusted adapter verifies its provider's
72
+ identity (for example Privy); a browser-supplied owner ID is never authentication.
73
+ For an existing Telegram owner, omit `--owner-id`: the new channel attaches to that
74
+ same owner. `GET /v1/registration` returns the owner ID and binding ID.
75
+ Revoke a channel with `--id web --revoke`. Rotate with `--id web --token-file
76
+ /private/new-token --rotate`: the binding ID, retries and native sessions remain
77
+ unchanged. Revoking the installation owner invalidates every channel.
78
+ Revocation blocks admission, results and delivery and stops an active app run on
79
+ the existing queue tick. `--list` exposes IDs, never tokens or hashes.
80
+
81
+ This grant lets a **trusted application backend act for the paired owner**. It is
82
+ not a multi-tenant filesystem sandbox. The application authenticates its users,
83
+ derives their allowed scope, and enforces domain tool permissions. Independent
84
+ owners or untrusted applications need separate agents/workspaces. Scoped sessions
85
+ separate conversation history; they share the ordinary agent's filesystem and
86
+ tool authority. Do not give this bearer token to browser JavaScript or models.
87
+
88
+ Enable the optional listener in the relay environment:
89
+
90
+ ```dotenv
91
+ EZ_APPLICATION_PORT=8787
92
+ EZ_APPLICATION_HOST=0.0.0.0
93
+ ```
94
+
95
+ The default bind address is loopback. In Docker, put the relay and backend on a
96
+ private shared network and use `http://<relay-service>:8787`; do not publish a host
97
+ port. Cross-host use requires HTTPS or an authenticated private tunnel. There is
98
+ no listener unless a port is configured. This mode cannot be combined with the
99
+ older channel-backend URL. Install core normally: its bin manifest makes the
100
+ application administration command available through the standard tools setup.
101
+
102
+ ## Request and result
103
+
104
+ All endpoints require `Authorization: Bearer <token>`.
105
+
106
+ ```text
107
+ POST /v1/runs
108
+ {"requestId":"job-123","scope":"principal:program","text":"Prepare our next lesson","context":{"reference":"lesson-1"}}
109
+
110
+ GET /v1/runs/<id>
111
+ GET /v1/runs
112
+ GET /v1/registration
113
+ POST /v1/runs/<id>/cancel
114
+ ```
115
+
116
+ Submission returns HTTP 202; reads/cancellation return 200. Each response is:
117
+
118
+ ```json
119
+ {"id":"r_app_...","scope":"principal:program","status":"queued","messages":[]}
120
+ ```
121
+
122
+ Status is `queued`, `running`, `completed`, `failed` or `cancelled`. Messages are
123
+ `{id,text}` records sent by the native agent through the existing message CLI.
124
+ The backend polls and renders them; no raw native JSONL or Telegram messages are
125
+ exposed. A delivery receipt means durable availability in the application inbox,
126
+ not that a human read it. Failure adds a generic `error`; detailed logs remain
127
+ local to core. Cancellation uses the existing child termination boundary and may
128
+ return `running` until the process exits.
129
+
130
+ `requestId` and `scope` accept 1–200 ASCII letters, digits, `_:.-`. Text is at most
131
+ 16,000 characters. Context is an optional JSON object (48 KiB); the complete HTTP
132
+ body is bounded to 64 KiB. Unknown input fields are rejected. Use a stable job ID:
133
+ retries with the same ID, scope and text return the original run and original
134
+ context, even if the caller supplies refreshed context. Changed scope/text is a
135
+ 409 conflict. Persist the submitted context/capability for recovery, or poll the
136
+ original run; retries never grant replacement authority to an admitted run.
137
+
138
+ Context is opaque domain-tool data, not prompt text or an environment override.
139
+ The existing `ezenciel-agents-schedule context` exposes it as
140
+ `run.application.context` only to the current native run. Domain tools and their
141
+ filesystem mounts are installed using ordinary Ez plugin/runtime setup. No API
142
+ field selects a native session, working directory, process environment or owner.
143
+ This first version transports text only, without file delivery, reactions or
144
+ approval UI.
145
+
146
+ ## Conversation continuity and migration
147
+
148
+ Each application binding plus scope gets an existing core session. Its first run
149
+ captures the selected engine/preset; later runs resume that same native session.
150
+ Application sessions are excluded from the Telegram session selector. Engines
151
+ that only resume an implicit latest session (`agy`) cannot serve isolated scopes.
152
+
153
+ Before cutting over an existing application, stop its old runner, copy its native
154
+ transcripts into the ordinary engine's private state, and import each authoritative
155
+ scope pointer as the administrator:
156
+
157
+ ```sh
158
+ ezenciel-agents-application --id myapp --import-scope principal:program \
159
+ --native-session NATIVE_SESSION_ID --cli codex
160
+ ```
161
+
162
+ Do this before submitting work for that scope. Import does not copy transcripts,
163
+ change the model preset, or merge histories. Verify the next real native turn
164
+ recalls the intended history and the app renders its reply before removing the
165
+ old deployment. Keep exactly one executor owner throughout the cutover.
166
+ For an owner-chat channel registered with `--share-owner`, add `--share-owner`
167
+ to the import command to select that imported conversation as the owner's current
168
+ chat. Without it, the import remains scoped and does not switch owner chat.
169
+
170
+ ## Reviewed deployment migration
171
+
172
+ The first upgrade adding the two optional application listener environment lines
173
+ changes `compose.yaml`. The existing updater deliberately rejects that runtime
174
+ shape change with “Runtime deployment changed; a separately reviewed migration is
175
+ required.” Keep this guard. Installation requires a separately reviewed deployment
176
+ migration before subsequent ordinary updates can use the new baseline.
177
+
178
+ The operator verifies the immutable archive's SHA-256, extracts it into a new
179
+ versioned package directory, copies `docker/pnpm-lock.yaml` to `pnpm-lock.yaml`,
180
+ and installs frozen dependencies **inside that package root**. Verify the new
181
+ application CLI and root-local `tsx` resolve. Build its runtime image without
182
+ stopping or changing the existing agent. Then the authorized deployment owner
183
+ switches the saved package/image binding using the standard host/update setup,
184
+ preserving the agent's existing workspace, control, native engine state, owner
185
+ pairing and plugin registry. Do not copy another agent's state or disable the
186
+ updater's deployment compatibility check.
187
+
188
+ For Docker applications, a deployment-owned overlay can attach the ordinary relay
189
+ to an explicitly created private network shared with the application backend:
190
+
191
+ ```yaml
192
+ services:
193
+ relay:
194
+ environment:
195
+ EZ_APPLICATION_PORT: "8787"
196
+ EZ_APPLICATION_HOST: "0.0.0.0"
197
+ networks:
198
+ default: {}
199
+ application:
200
+ aliases: [standard-agent]
201
+ networks:
202
+ application:
203
+ external: true
204
+ name: ${EZ_APPLICATION_NETWORK:?Set the deployment-owned application network}
205
+ ```
206
+
207
+ Create that network once through normal Docker administration; attach the backend
208
+ to the same external network in its own deployment overlay. Preserve the default
209
+ network. The backend connects to `http://standard-agent:8787`; no `ports` entry or
210
+ host listener is required. Keep the overlay outside the package, owned by the
211
+ deployment, and include it in that deployment's saved Compose invocation. This is
212
+ an explicit network configuration, not a second runtime or automatic discovery
213
+ service. Read back the loaded package/image and verify a real authenticated app
214
+ request after switching; preparation and image build alone are not rollout.
215
+
216
+ ## Shared backend client and private principals
217
+
218
+ Node backends can import `applicationBinding`, `applicationCall`, and
219
+ `runApplication` from `@jc_stack/ez-agents/application-client`. This is a small
220
+ HTTP client; core remains the sole owner of agent execution. Pin the package
221
+ revision in the application's lockfile. Do not copy this client into each app.
222
+
223
+ `applicationBinding(file, principalId)` reads a backend-private registry:
224
+
225
+ ```json
226
+ {"version":1,"bindings":[
227
+ {"principalId":"verified-person-one","url":"http://person-one-agent:8787","tokenFile":"/run/secrets/person-one"},
228
+ {"principalId":"verified-person-two","url":"http://person-two-agent:8787","tokenFile":"/run/secrets/person-two"}
229
+ ]}
230
+ ```
231
+
232
+ Resolve identity in the app before lookup. Unknown and revoked principals fail
233
+ closed. Set `revoked:true` to remove a mapping from new lookup and revoke its
234
+ core application grant to stop admitted work. Do not use a default owner's
235
+ connection. Each endpoint must be a separate ordinary Ez deployment with private
236
+ workspace, native CLI state, OS/container access and tools. The client rejects
237
+ reused endpoint origins, but cannot prove that two DNS aliases name different
238
+ containers. Provision isolation explicitly; this registry does not create it.
239
+ The backend's registry and service credentials must not be mounted in an agent.
240
+
241
+ The app keeps its existing user and coach/tutor grants. A delegated principal
242
+ must be distinct from the learner's personal principal, and every domain tool
243
+ must recheck the current grant. A coach can edit permitted learner records
244
+ without inheriting the learner's private agent conversation. No new role system
245
+ is required in Ez.
246
+
247
+ Use `runApplication({requestId,scope,text,context}, connection)`. It returns the
248
+ completed snapshot plus `reply`, the last nonempty message. An interrupted or
249
+ invalid HTTP response reports a retryable transport error; the caller reconnects
250
+ using the same persisted job ID and authority. The client does not resubmit by
251
+ itself. `applicationCall('/v1/runs/<id>/cancel', {}, connection)` uses normal
252
+ core cancellation. Aborting a local poll does not cancel admitted work.
253
+
254
+ Snapshots also expose `sessionId`, `nativeSessionId` when known, and `cli`.
255
+ An optional `expectedNativeSessionId` on submission is an assertion, never a
256
+ session selector. It rejects a missing or different imported history before
257
+ execution. Import old histories administratively before cutover. An optional
258
+ `ai:{cli,model,effort}` uses the existing preset/effort contract; a scope keeps its
259
+ CLI, while model/effort can change for later turns. Use distinct scopes for
260
+ separate CLI histories. A retry cannot change an admitted turn's AI choice.
261
+
262
+ ## One owner across channels
263
+
264
+ Register owner-chat channels with `--share-owner` and submit `followOwner:true`.
265
+ Web, phone and Telegram then use the same selected native conversation and AI.
266
+ This works without Telegram and requires no transcript replay or new runner.
267
+ Scoped application conversations remain separate when the flag is omitted.
268
+ Owner identity does not automatically merge histories or grant learner access.
269
+
270
+ To link Telegram, enable its ordinary bot configuration, send a real DM, then
271
+ approve the observed pending identity using `ezenciel-agents-owner approve ID`.
272
+ `ezenciel-agents-owner unlink-telegram` removes that channel without removing the
273
+ owner, application bindings or native sessions. Relinking does not authorize old
274
+ Telegram deliveries. Linking other providers requires an authenticated adapter;
275
+ registering the label `phone` does not install a phone service.
276
+
277
+ The legacy `--share-telegram`, `--share-active` and `followTelegram:true` spellings remain supported.
278
+
279
+ ### Existing scoped Telegram sharing
280
+
281
+ For an application using the ordinary agent's Telegram channel, the administrator
282
+ may register its grant with `--share-telegram`. An ordinary application turn can
283
+ then send `activateTelegram:true`: its scoped session becomes the owner's current
284
+ Telegram conversation. Both inputs resume the same native history. Shared scopes
285
+ appear in the existing `/chats` selector. Omit activation for temporary selection,
286
+ extraction, and delegated work; those must not switch the personal conversation.
287
+ Without the administrator's flag, application requests cannot switch Telegram.
288
+
289
+ For a main chat that should follow the owner's current Telegram conversation,
290
+ submit `followTelegram:true` under the same administrator-approved sharing grant.
291
+ This uses the normal selected conversation and AI at admission, including after
292
+ `/new`, `/chats`, or `/ai`. Do not also send `activateTelegram`, `ai`, or a native
293
+ session assertion. Retries remain pinned to their originally admitted run even
294
+ if the owner has since switched conversations. Keep exercise/detail scopes on
295
+ the ordinary scoped path by omitting this flag.
296
+
297
+ This shares native context, not an application's transcript database. Existing
298
+ Telegram-to-backend integrations are a legacy limitation. Drain/reconcile their
299
+ actual admitted runs before moving Telegram onto ordinary Ez transport and app
300
+ admission onto this path. Do not introduce an application execution queue or
301
+ route a channel-backend job into its own occupied execution queue.
302
+
303
+ For backends holding a data lock across a native turn, `reconnect:true` keeps
304
+ retrying transient transport failures with the same admitted request/GET until
305
+ core reports the outcome. It never changes a job or capability. Do not apply a
306
+ wall-clock abort that releases the data lock while remote execution continues.
307
+ Persist a restart barrier before admission if backend restart could otherwise
308
+ allow conflicting domain writes. Core remains the execution owner.
309
+
310
+ A rejected submission includes `admitted:false` only when core can verify that
311
+ no run exists for that binding/request ID. An existing conflicting run produces
312
+ `admitted:true` and `runId`. Authentication or unreadable state may leave admission
313
+ unknown. Backends may release a pending data barrier on explicit non-admission;
314
+ a generic HTTP error or revoked credential alone is not proof of termination.
315
+
316
+ ## Application-only deployment (no Telegram bot)
317
+
318
+ For a private per-user native runtime, set `EZ_TELEGRAM_ENABLED=false` and
319
+ `EZ_APPLICATION_PORT` (plus `EZ_APPLICATION_HOST` when other containers connect).
320
+ No bot token is required or used. This mode runs the existing native executor,
321
+ application queue and application outbox without creating a Telegram client,
322
+ starting Telegram sources/polling, or registering bot commands. Default deployments
323
+ still enable Telegram and require its token.
324
+
325
+ The installing administrator can initialize empty control authority and register
326
+ an application in one local command:
327
+
328
+ ```sh
329
+ ezenciel-agents-application --id web --token-file /run/private/application-token --owner-id VERIFIED_ACCOUNT_ID --share-owner
330
+ ```
331
+
332
+ No Telegram ID is required or fabricated. Account identity remains server-resolved
333
+ by the app and bound to its isolated runtime. Registration is unavailable inside
334
+ agent turns and cannot replace an existing owner or adopt orphaned session state.
335
+ Existing owners require no bootstrap. Tokens remain private; normal binding and
336
+ run authorization checks still apply. The old numeric `--owner` option is retained
337
+ only for compatibility, not recommended for new installations.
338
+
339
+ Each learner/coach principal requires its own workspace, CLI state and isolated
340
+ runtime. No separate bot is required. A channel is an authenticated route to its
341
+ existing owner, not a new owner, scheduler or native execution engine.
342
+
343
+ The standard scheduler, task controls and native delegation remain available.
344
+ Schedules created during an application turn retain its binding and reply scope;
345
+ scheduled replies use the normal outbox and appear in `GET /v1/runs` (100 channel
346
+ runs per page), with their `originRunId`. Follow `nextCursor` through
347
+ `GET /v1/runs?before=RUN_ID` until null to catch up after a disconnect; receipts
348
+ from older origins must not be limited to the UI's current page. The backend projects these receipts into
349
+ its UI; it does not schedule or execute work. Revoked channels cannot launch or
350
+ receive scheduled work. Token rotation retains delivery. Telegram-specific intake
351
+ and delivery require Telegram; pending Telegram work is never rerouted to web.
352
+
353
+ When a private container supplies the isolation boundary and
354
+ Codex cannot create its nested sandbox, its installing administrator may set
355
+ `EZ_CODEX_SANDBOX=external` together with `EZ_EXECUTOR_TRANSPORT=local`.
356
+ Local owner-authorized Codex app and Telegram turns use
357
+ `--sandbox danger-full-access`. Native scheduled owner sessions use Codex's
358
+ `externalSandbox` turn policy, with network access supplied by the container.
359
+ The default remains `workspace-write`.
360
+ Keep the container's private mounts, non-root UID, dropped capabilities and
361
+ no-new-privileges policy; the agent can access everything mounted into it.
362
+ Use standard root-start relay privilege separation when Telegram/provider
363
+ secrets are present; do not put those secrets in a same-UID process environment
364
+ or readable mount. This setting is rejected for host/backend execution and
365
+ restricted delegated tasks, and cannot be selected by an application request.
366
+ It is not forwarded to the host executor.
@@ -4,6 +4,11 @@ Docker Compose owns each relay and executable plugin. The CLI the user installs
4
4
  Ez from stays on the host, with its existing authentication. All agents in that
5
5
  installation use that CLI; there is no second CLI installation or agent-specific
6
6
  CLI login. The relay image contains Node, relay dependencies and ffmpeg.
7
+ The runtime image exposes Ez commands through the wrappers in `/app/bin`, including
8
+ `ezenciel-agents-application`; application images do not need to link Ez themselves.
9
+ The image also installs package-manifest commands in `/usr/local/bin`, so native
10
+ engine login shells can resolve them after resetting PATH. Command installation
11
+ belongs to Ez packaging, not an application image or an engine-specific prompt.
7
12
 
8
13
  ## Agent binding
9
14
 
@@ -46,6 +51,21 @@ between jobs after adding a registry binding.
46
51
 
47
52
  ## Operate and verify
48
53
 
54
+ Root-started containers default to executor UID/GID `1000:1000` and relay real
55
+ UID `1001`. To preserve an existing private workspace's ownership, set
56
+ `EZ_RUNTIME_UID`, `EZ_RUNTIME_GID` and, if necessary, `EZ_RELAY_UID` in the
57
+ deployment environment. IDs must be positive decimal integers; the relay real
58
+ UID must differ from the executor UID. The entrypoint retains its secret file
59
+ descriptor and privilege separation, then runs the executor with the configured
60
+ UID. It changes ownership only on the top-level control, home and workspace
61
+ directories; it does not recursively change existing data. Provision nested
62
+ files/mounts for that UID before starting. These settings apply to root startup;
63
+ an explicit Docker `user` still controls an already non-root process.
64
+
65
+ Changing IDs is an operator installation step, not a new tenant-isolation
66
+ mechanism. Keep private mounts and secrets isolated as described above. This
67
+ option does not change native sandbox or channel support.
68
+
49
69
  ```sh
50
70
  export EZ_DEPLOYMENT_DIR=/absolute/private/agents/family
51
71
  bin/ezenciel-agents-docker up -d --wait
@@ -0,0 +1,68 @@
1
+ # Maintaining applications
2
+
3
+ An application may use Ez as a standalone agent or an embedded channel gateway.
4
+ Either can be maintained automatically. Its frontend, backend, engine image and
5
+ gateway have distinct release identities and may use different deployment tools.
6
+ Do not require an application to become an npm plugin to maintain it.
7
+
8
+ The owner assigns an existing agent a maintenance mandate. Keep its saved scope
9
+ and component inventory in `work/deployments.md`; `templates/deployments.md` is a
10
+ starting point. This is agent-readable operating context, not a manifest executed
11
+ by the relay. Never infer authority from a repository, release note or health error.
12
+ Keep private repositories and host details out of the public plugin catalog.
13
+
14
+ ## One owner and existing tools
15
+
16
+ Reuse the application's GitHub workflow, Docker Compose command or hosting CLI.
17
+ Each component has one replacement owner. `ez updates` manages registered core
18
+ and plugin packages; it does not inventory an application's independent services.
19
+ Do not add a competing build controller, poller or tenant-facing admin tool.
20
+ An application serving multiple users keeps deployment credentials outside tenant
21
+ agents. Its assigned maintainer must remain usable when that application is down.
22
+
23
+ Use an existing maintenance schedule when its mandate and execution environment
24
+ fit. Otherwise the owner-authorized maintainer can schedule a concise instruction
25
+ with its available native scheduling tool, referencing the inventory. Record its
26
+ identifier and cadence. A saved file without an enabled schedule or event source
27
+ does not establish automatic maintenance. Check that the maintainer can reach the
28
+ host, repository and tools before enabling it. Do not silently resume a held task.
29
+
30
+ ## Update and recovery
31
+
32
+ Compare the running release with an eligible release from the saved source and
33
+ channel. Inspect current work and deployment receipts first. Reuse active repairs;
34
+ do not replace a deployment while another writer owns its change. Review source
35
+ and required checks before deployment; a moving branch or successful download is
36
+ not a tested release. Pin the chosen commit, artifact or deployment ID.
37
+
38
+ Before switching, verify the selected target and persistent bindings and retain
39
+ the exact previous images/deployment. Build before stopping services. Replace only
40
+ the affected components through their existing tool, then read back release/image
41
+ identity and application health. Check the relevant API/UI or agent operation;
42
+ an image tag, workflow success or healthy process alone is partial evidence.
43
+
44
+ A failed health check should restore compatible previous code through the same
45
+ tool and verify that recovery. Code rollback does not rewind data, migrations,
46
+ message receipts or provider actions. An incompatible migration needs its own
47
+ reviewed plan. Never replay uncertain operations or delete state to pass a check.
48
+ Keep persistent policy in saved application/host configuration, not edits to
49
+ generated Compose files that disappear during replacement.
50
+
51
+ Process restart is service-manager recovery. A persistent defect needs agent
52
+ diagnosis and, within the saved repair mandate, an isolated PR, independent review,
53
+ required tests and verified deployment. A failed candidate is not a reason to keep
54
+ retrying it: retain its receipt and resume only with changed evidence or an explicit
55
+ intervention. Keep a working runtime while repair proceeds where possible.
56
+
57
+ Record candidate, previous release, check results and final running identity in a
58
+ private receipt. Keep maintenance quiet when unchanged. Report a verified change,
59
+ failed recovery or actionable blocker once; retain the next trigger for blocked
60
+ work. Do not manufacture releases just to exercise the maintenance loop.
61
+
62
+ ## Acceptance
63
+
64
+ Before calling a deployment self-maintaining, verify the enabled schedule/event,
65
+ maintainer access, exact-version deployment and readback, configuration retention,
66
+ and failed-health rollback in an isolated environment. Reconcile the live target
67
+ after rollout. State separately which live API/UI or delivery paths were exercised
68
+ and which still need evidence. Never test rollback by breaking a user's live app.
@@ -12,6 +12,7 @@ has been published. Install only released plugins needed for the owner's request
12
12
  | [GitHub](https://github.com/jdorado/ez_github) | Create repositories, commit and push through native Git/gh with a private per-agent profile. Requires GitHub browser consent and Ez 0.1.0-beta.13 or newer. | `@jc_stack/ez-github` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-github?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez_github/releases) |
13
13
  | [Library](https://github.com/jdorado/ez-library) | Preserve agent files and attachments, extract PDF text, and retrieve notes with QMD. Optional GitHub/Drive/Dropbox persistence requires separate setup and authorization. | `@jc_stack/ez-library` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-library?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez-library/releases) |
14
14
  | [Resend](https://github.com/jdorado/ez-resend) | Receive Resend email into private idempotent receipts for agent-owned routing. Requires a Resend API key and an already-configured receiving domain. | `@jc_stack/ez-resend` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-resend?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez-resend/releases) |
15
+ | [Voice](https://github.com/jdorado/ez-voice) | Talk with the agent through an authenticated realtime voice web client, with direct access to installed Ez tools. Requires an OpenAI API key, microphone access and Ez 0.1.0-beta.28 or newer. | `@jc_stack/ez-voice` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-voice?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez-voice/releases) |
15
16
 
16
17
  Release links are live records, not a guarantee that every registered package
17
18
  has a published version. Before installing, read npm metadata and the matching
@@ -0,0 +1,76 @@
1
+ # Persistent plugin connection
2
+
3
+ `ez tools connect <installed-alias> <literal arguments...>` attaches one Compose
4
+ command container for a local, agent-bound client. It releases the registry lock
5
+ after admission. Ordinary JSON objects travel as JSONL between client and plugin;
6
+ this transport owns no inference, conversation history or plugin-specific tools.
7
+ The local owning connection uses the agent's installed plugin permissions, just
8
+ like its bound CLI. A remote or web adapter must authenticate its owner before
9
+ connecting and protect its stdin. Plugin commands retain their own authorization;
10
+ the connection adds no per-command permission prompt.
11
+
12
+ Plugin requests use `{"coreRequest":{"id":"r1","method":"tools.list","params":{}}}`.
13
+ Core answers `{"coreResponse":{"id":"r1","result":...}}` or `error` (a string).
14
+
15
+ | Method | Parameters | Result |
16
+ | --- | --- | --- |
17
+ | `tools.list` | none | Array of alias, plugin, description, skillCount, revision |
18
+ | `tools.help` | alias | CLI `--help` result: code, stdout, stderr |
19
+ | `tools.skill` | alias, index, optional line | Declared skill text and nextLine; index starts at zero, line at one |
20
+ | `tools.invoke` | alias, args (literal string array), optional stdin and output filename | CLI result: code, stdout, stderr; optional artifact path, bytes, sha256 |
21
+ | `tools.native.list` | none | Core command descriptions and availability |
22
+ | `tools.native` | command, args (literal argument array) | Native CLI result: code, stdout, stderr |
23
+
24
+ `tools.native` invokes only the shipped `schedule` or `message` CLI, using the
25
+ owning host's verified workspace/control binding and a whitelisted environment.
26
+ Read `--help` for its command contract; this connection requires inline `--text`
27
+ and rejects `--text-file` so it cannot read arbitrary host files. `create --now` submits standard
28
+ asynchronous native work; it does not launch a second runner or hold the workspace
29
+ lease while waiting. The native agent may retrieve a Library original, inspect it
30
+ with its existing image/PDF tools, and save source-linked searchable text.
31
+ Scheduler owner checks, selected engine, cancellation and receipts remain native.
32
+ No synthetic run context is injected. Standalone bindings cannot use this method;
33
+ task creation without a Telegram owner or authenticated originating channel still
34
+ fails under the scheduler's existing delivery rule. Results can be inspected with
35
+ `runs` and `run RUN_ID`; a completed run alone is not proof of indexed content.
36
+
37
+ Message delivery captures the paired owner and pairing epoch when the connection
38
+ opens. Core injects that context; tool arguments cannot select a recipient or
39
+ replace it. The message CLI enqueues the existing outbox without creating an agent
40
+ run, and the relay rechecks the owner before sending. Revocation or re-pairing
41
+ invalidates the connection. Inline text and workspace documents use the same
42
+ Telegram delivery and receipt path as native runs. Text-file input is unavailable.
43
+ History still requires a native run; `message receipt OUTBOX_ID` reconciles a
44
+ channel send against the current owner's outbox. Sends return a queued ID then
45
+ wait up to 20 seconds for delivery; queued or unknown never means sent, and must
46
+ not be retried without receipt readback. An accepted send may finish after the
47
+ voice connection closes. Omitting command retains schedule compatibility.
48
+
49
+ For binary plugin output, supply a simple `output` filename. Core saves at most
50
+ 20 MiB of stdout in a unique private workspace artifact and returns its relative
51
+ path, byte count and SHA-256 instead of binary model text. Only successful command
52
+ output is saved. The message CLI can send that path with `--document`; both host
53
+ admission and relay delivery enforce workspace containment.
54
+
55
+ The registry is read on each request. The connected plugin is excluded, including
56
+ its other aliases. Skill reads stay within its declared source directory and
57
+ return at most 100 lines. Invocations execute once through the registered binding;
58
+ a changed plugin revision requires rediscovery. Client stdin cannot forge core
59
+ requests or responses. The plugin may send `coreCancel: {id}` to cancel its own
60
+ pending request. Request IDs cannot be reused within a connection.
61
+
62
+ Commands use the existing bound Compose dispatcher, literal argv and whitelisted
63
+ environment. Calls have a 30-second timeout and 256 KiB combined output limit;
64
+ connection frames are limited to 1 MiB. Connection closure aborts pending calls
65
+ and removes their exact command containers. There is no automatic retry.
66
+
67
+ Arbitrary calls take the same fail-fast workspace lease as non-scheduled native
68
+ jobs. Native work stays pending while a call runs; calls refuse pending/running
69
+ native work. Ordinary CLI calls inside an admitted native job do not reacquire
70
+ the lease. Isolated scheduled work keeps its existing workspace behavior. A stale
71
+ native `workspace-writer.lock` is recovered at host startup only after its host
72
+ owner is dead and startup has checked previous native processes. A stale plugin
73
+ lease stops admission with an explicit error: an operator must verify its command
74
+ containers stopped before removing the lock. A dead bridge alone is insufficient.
75
+ This guard requires the updated host worker. Direct external writers are outside
76
+ the managed admission boundary.
package/docs/plugins.md CHANGED
@@ -263,10 +263,10 @@ Shared workers have a hard Docker CPU quota of half a core by default, across al
263
263
 
264
264
  ## Existing local folders
265
265
 
266
- Use operator-owned read-only folder bindings when a plugin needs files that
266
+ Use operator-owned folder bindings when a plugin needs files that
267
267
  already exist on the host. Keep indexes and writable metadata in the plugin's
268
268
  normal volume. This uses Docker bind mounts; it copies no source bytes and
269
- starts no provider sync. File edits remain with the host's existing tools.
269
+ starts no provider sync. Read-only is the default.
270
270
 
271
271
  Stop the plugin before changing a binding:
272
272
 
@@ -288,8 +288,8 @@ volume contents; inspect those before restarting to avoid using stale files.
288
288
  Never enable another sync writer for an already synchronized host folder.
289
289
 
290
290
  For Library, create/select the library name first and retain its QMD state while
291
- binding the host tree at that library's `files` directory. Keep provider bindings
292
- disabled for that source. Enable the normal shared embedding worker through
291
+ binding the host tree at that library's `files` directory. Enable provider bindings
292
+ only when that plugin supports the selected host folder and owns its sync. Enable the normal shared embedding worker through
293
293
  `plugins shared-enable library embeddings`. Verify `library sources`, real search,
294
294
  and original readback from the actual executor. Document any differences between
295
295
  indexed snapshots and current originals; do not replace Library with private
@@ -307,3 +307,39 @@ on the next read, without hooks, LLM calls or a cached inventory file.
307
307
  contains only the agent-bound discovery shortcut. New workspaces do not seed
308
308
  TOOLS.md; upgrades preserve legacy notes without rewriting them. Keep owner/account
309
309
  policies in agent instructions or linked policy files, separate from plugin metadata.
310
+
311
+ Folder bindings default to read-only. For an explicitly authorized plugin that
312
+ updates the existing source, add `--writable` to `folder-bind` while the plugin
313
+ is stopped. The grant applies only to that folder and survives compatible
314
+ upgrades. Rebind without `--writable` to return it to read-only. Package
315
+ descriptors cannot request this grant. Keep one synchronization owner for each
316
+ source; a writable mount alone does not configure synchronization.
317
+
318
+ ## Browser endpoints for connected plugins
319
+
320
+ `ez tools serve HOST_PORT:CONTAINER_PORT ALIAS ARGS...` runs a plugin's web command
321
+ inside its command container while core handles the existing persistent tool
322
+ protocol. Both ports must be 1024–65535. Publication is always on host 127.0.0.1;
323
+ no plugin manifest can request public ingress. Run the foreground command under
324
+ the host's normal service supervisor if it must survive terminal closure. SIGINT
325
+ or SIGTERM cancels the connection and removes its command container.
326
+
327
+ The plugin owns HTTP, browser authentication, sessions and static assets. HTTPS
328
+ termination, DNS and forwarding are explicit operator configuration. Plugins may
329
+ use the read-only `tools.owner` core request to check the current paired private
330
+ Telegram user and opaque pairing epoch. This is identity data, not an access grant;
331
+ the plugin must authenticate the requester and recheck identity on protected requests.
332
+ No owner returns null, and no bot token is exposed. Standard plugin CLI operations
333
+ still enforce their normal permissions. See the Voice plugin's README for a client.
334
+
335
+ To add an optional launcher without replacing Telegram's command menu, set the
336
+ relay Compose environment or `.env` (then recreate the relay container):
337
+
338
+ ```dotenv
339
+ EZ_TELEGRAM_WEB_APP={"command":"voice","label":"Voice","url":"https://voice.example.com/"}
340
+ ```
341
+
342
+ The command returns a Mini App button to the authenticated owner in private chat;
343
+ `/menu` includes the same button. Reserved commands cannot be replaced. The HTTPS
344
+ URL must not contain credentials, query parameters or a fragment. This setting
345
+ only registers a launcher; it does not expose a port or authenticate web requests.
package/docs/upgrades.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Agent-owned software upgrades
2
2
 
3
+ For an application's independently deployed frontend, backend or embedded gateway,
4
+ see [managed applications](managed-applications.md). They use their existing
5
+ deployment tools and a saved maintenance mandate; this package updater inventories
6
+ only core and registered plugins.
7
+
3
8
  Available in this beta. Earlier main upgrade/rollback VM QA passed; final-release
4
9
  fresh-host/reboot and live plugin upgrade acceptance remain pending. npm
5
10
  publication is not required to test this feature. The beta channel is the default
@@ -14,7 +19,12 @@ runs the normal CLI transport. It checks npm every six hours while running and
14
19
  queues an owner-bound maintenance turn only when an automatic channel changes.
15
20
  No owner means no maintenance executor. Normal user work and maintenance share
16
21
  one serial queue. Checks use the installed scoped npm identity; failures are
17
- visible in `updates check` and private `tools/updates/available.json`.
22
+ visible in `updates check` and private `tools/updates/available.json`. Plugins marked
23
+ `private: true` in their package metadata are reported as local-source updates
24
+ only, without querying public npm; this does not mean they are up to date.
25
+ Explicit local-file updates retain the existing release-contract checks; plugins
26
+ without that contract use their reviewed local-source installation procedure.
27
+ Public packages still receive discovery checks under a manual policy.
18
28
 
19
29
  ## Installation and scope
20
30