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