@jc_stack/ez-agents 0.1.0-beta.47 → 0.1.0-beta.48

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 (72) hide show
  1. package/.env.example +3 -0
  2. package/AGENTS.md +2 -2
  3. package/CHANGELOG.md +21 -0
  4. package/Dockerfile +5 -1
  5. package/THIRD_PARTY_NOTICES.md +6 -3
  6. package/bin/ez +7 -0
  7. package/docker/broker-readiness.mjs +54 -0
  8. package/docker/broker-update-smoke.mjs +86 -0
  9. package/docker/run.ts +1 -1
  10. package/docs/application-channel.md +32 -1
  11. package/docs/architecture/ai-selection.md +18 -3
  12. package/docs/docker-runtime.md +29 -0
  13. package/docs/isolated-plugin-update-readiness.md +66 -0
  14. package/docs/releasing.md +4 -0
  15. package/docs/scheduling.md +100 -6
  16. package/docs/selective-monitoring.md +13 -0
  17. package/docs/upgrades.md +35 -0
  18. package/docs/work-preflight.md +9 -0
  19. package/package.json +1 -1
  20. package/src/ai.ts +22 -3
  21. package/src/application-channel.ts +43 -3
  22. package/src/application-cli.ts +7 -1
  23. package/src/application-origin.ts +2 -0
  24. package/src/audio.ts +37 -22
  25. package/src/client-defaults.ts +3 -2
  26. package/src/config.ts +9 -0
  27. package/src/control-state.ts +3 -1
  28. package/src/delivery-socket-client.d.mts +2 -0
  29. package/src/delivery-socket-client.mjs +2 -0
  30. package/src/delivery-socket.ts +24 -1
  31. package/src/event-sources.ts +2 -2
  32. package/src/executor.ts +72 -1
  33. package/src/files.ts +47 -3
  34. package/src/host-executor.ts +38 -29
  35. package/src/index.ts +41 -18
  36. package/src/install-tools.mjs +8 -5
  37. package/src/menu.ts +4 -2
  38. package/src/plugin-broker.mjs +47 -4
  39. package/src/plugins/manager.mjs +11 -4
  40. package/src/run-evidence.ts +78 -0
  41. package/src/runs.ts +12 -1
  42. package/src/schedule-cli.ts +69 -8
  43. package/src/schedule-preflight.ts +23 -0
  44. package/src/scheduled-tasks.ts +16 -12
  45. package/src/scheduler.ts +79 -18
  46. package/src/scripts.ts +167 -0
  47. package/src/setup.ts +2 -2
  48. package/src/task-capability.ts +17 -1
  49. package/src/task-executor.ts +10 -7
  50. package/src/task-mcp.ts +20 -8
  51. package/src/tasks.ts +89 -9
  52. package/src/updates/runtime.mjs +46 -7
  53. package/test/ai.test.ts +40 -0
  54. package/test/application-channel.test.ts +16 -1
  55. package/test/audio.test.ts +19 -0
  56. package/test/config.test.ts +8 -0
  57. package/test/executor.test.ts +37 -0
  58. package/test/files.test.ts +44 -0
  59. package/test/host-executor.test.ts +45 -2
  60. package/test/install-json.test.mjs +21 -0
  61. package/test/plugin-broker.test.mjs +64 -0
  62. package/test/plugin-manager.test.mjs +20 -0
  63. package/test/private-speech-config.test.ts +22 -0
  64. package/test/run-evidence.test.ts +78 -0
  65. package/test/schedule-preflight.test.ts +49 -0
  66. package/test/scheduler-host.test.ts +7 -0
  67. package/test/scheduler-relay.test.ts +38 -3
  68. package/test/scheduler.test.ts +1 -1
  69. package/test/script-schedules.test.ts +219 -0
  70. package/test/task-native.test.ts +50 -12
  71. package/test/tasks.test.ts +97 -4
  72. package/test/updates.test.mjs +45 -9
package/.env.example CHANGED
@@ -13,6 +13,9 @@ EZ_AGENT_WORKSPACE=./agent
13
13
  # Optional. Default is 300.
14
14
  # Agent execution has no wall-clock timeout. Use /stop or schedule cancel.
15
15
 
16
+ # Maximum concurrent owner-scheduled jobs; foreground chat keeps a separate serial lane.
17
+ # EZ_SCHEDULED_CONCURRENCY=6
18
+
16
19
  # CLI Executor adapter. Supported: codex (default), codex-gui, claude, grok, opencode, pi
17
20
  # Switch easily with: pnpm run setup <executor-name>
18
21
  EZ_EXECUTOR_CLI=codex
package/AGENTS.md CHANGED
@@ -120,8 +120,8 @@ round merely to seek more criticism, tests or hypothetical edge cases.
120
120
  - **Rate limits:** Avoid spamming bubbles to respect Telegram’s 1 message/sec per-chat rate limit.
121
121
 
122
122
  ## 6. Concurrency & Workspace Invariants
123
- - **Responsive owner channels:** One foreground turn and one owner-scheduled turn may run concurrently; each stream stays serial. Separate native sessions isolate history and engine state, while the agent-owned Markdown workspace is shared. The engine coordinates shared file updates and plugins guard canonical record writes. Do not add a second reply agent, scheduler or execution queue.
124
- - The delivery socket carries every cross-process enqueue/receipt, never control/ files. Host plugin workspace invocations remain excluded until both native turns finish. Scheduled jobs keep separate native CLI sessions and control-side task state. Restricted contact tasks retain their separate authorization boundary. Delegation decisions and goal persistence belong to the agent/executor; there is no automatic planner or canned chat ACK. Production executor runs have no wall-clock timeout; cancellation is explicit.
123
+ - **Responsive owner channels:** One foreground turn and up to `EZ_SCHEDULED_CONCURRENCY` owner-scheduled turns may run concurrently (default six); foreground stays serial. Separate native sessions isolate history and engine state, while the agent-owned Markdown workspace is shared. The engine coordinates shared file updates and plugins guard canonical record writes. Do not add a second reply agent, scheduler or execution queue.
124
+ - The delivery socket carries every cross-process enqueue/receipt, never control/ files. Host plugin workspace invocations remain excluded until all native turns finish. Scheduled jobs keep separate native CLI sessions and control-side task state. Restricted contact tasks retain their separate authorization boundary. Delegation decisions and goal persistence belong to the agent/executor; there is no automatic planner or canned chat ACK. Production executor runs have no wall-clock timeout; cancellation is explicit.
125
125
 
126
126
  ## 7. Fail-Closed Authority (Channel Access ≠ Execution)
127
127
  - Incoming messages outside the approved owner binding must **never** spawn the executor. A first DM or group message records a pending request only. Explicitly approved owner groups grant owner access to all human members in that exact chat; bots and anonymous posts are ignored.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.48
4
+
5
+ - Skip empty or unchanged scheduled work through owner-configured installed read-only plugin preflight. Invalid or unavailable reads fail closed; saved execution settings and recurring timing remain intact. The bound plugin broker is required.
6
+ - Run explicitly registered workspace scripts through the existing native scheduler without starting a model. Preserve registration revision/hash, cancellation, timeout, isolated environment and run receipts.
7
+ - Run Claude Code with an agent-bound configuration and native model/effort discovery. Older Claude sessions need `/new`; host login sharing supports OAuth only, and isolated agents log in separately.
8
+ - Keep owner channels responsive with configurable scheduled concurrency, default six.
9
+ - Expose bounded, sanitized owner run and delivery evidence through native controls.
10
+ - Repair transient plugin admission and isolated broker readiness after atomic binding changes; include isolated executor utilities.
11
+ - Keep Codex version discovery JSON separate from stderr notices.
12
+ - Validate Docker CI against audited Codex 0.160.1 instead of a moving registry tag; managed native version selection is unchanged.
13
+ - Read scoped TXT/PDF and JPEG/PNG/WebP attachments through the shared native MCP path. MP4 attachments supply bounded sampled frames to native vision; speech remains in the provider-sourced incoming transcript.
14
+
15
+ - Add short-lived browser conversation links for explicitly granted restricted discussions, retaining each task’s context and tool permissions.
16
+ - Preserve the existing runtime file owner when an administrator replaces discussion application grants, keeping private mode 0600 and preventing restricted task startup failures.
17
+ - Forward only the existing broker and delivery socket paths to the trusted task MCP subprocess, allowing scoped browser launch through the isolated installed launcher without exposing a tool registry.
18
+ - Audit restricted native tools on Codex 0.160.1 and retain the exact version gate.
19
+ - Extend the existing speech renderer with OpenRouter TTS and Ogg Opus/WAV encoding.
20
+ - Allow authorized task replies to use core-rendered voice on supporting channels, with post-synthesis authorization and unchanged uncertain-send protection.
21
+
22
+ Beta limits: live fresh-host/reboot acceptance remains pending. Claude host OAuth login sharing and human voice/media exchanges retain their documented live-QA limitations. Publication does not establish an installed-agent upgrade or activate new schedule gates.
23
+
3
24
  ## 0.1.0-beta.47
4
25
 
5
26
  - Trigger an existing scheduled task manually with its saved instructions, model,
package/Dockerfile CHANGED
@@ -1,7 +1,7 @@
1
1
  FROM docker/compose-bin:v5.5.1@sha256:14162382692aae43977f79effa64a499d61de19d4645461881daaf88773f7e74 AS compose-bin
2
2
  FROM node:22.22.0-bookworm-slim@sha256:dd9d21971ec4395903fa6143c2b9267d048ae01ca6d3ea96f16cb30df6187d94 AS dependencies
3
3
  WORKDIR /app
4
- RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg docker.io && rm -rf /var/lib/apt/lists/*
4
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg poppler-utils docker.io jq python3 python-is-python3 && rm -rf /var/lib/apt/lists/*
5
5
  COPY --from=compose-bin /docker-compose /usr/local/bin/docker-compose
6
6
  ENV PNPM_HOME=/opt/pnpm COREPACK_HOME=/opt/corepack COREPACK_DEFAULT_TO_LATEST=0
7
7
  ENV PATH=/opt/pnpm:$PATH
@@ -30,6 +30,10 @@ RUN pnpm add --global --allow-build=opencode-ai@${OPENCODE_CLI_VERSION} opencode
30
30
  # Pin the reviewed CLI for repeatable release images.
31
31
  ARG PI_CLI_VERSION=0.87.1
32
32
  RUN pnpm add --global --ignore-scripts @earendil-works/pi-coding-agent@${PI_CLI_VERSION} && command -v pi
33
+ # Isolated agents select Claude Code from the relay menu with an agent-bound
34
+ # CLAUDE_CONFIG_DIR login (control/cli/claude). Pin the reviewed CLI.
35
+ ARG CLAUDE_CLI_VERSION=2.1.286
36
+ RUN pnpm add --global --allow-build=@anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} @anthropic-ai/claude-code@${CLAUDE_CLI_VERSION} && command -v claude && claude --version
33
37
  RUN chmod +x docker/entrypoint.sh bin/ez bin/ezenciel-agents* && mkdir -p /state/control /state/home /workspace && chown node:node /state/control /state/home /workspace
34
38
  RUN node -e 'for (const [name, target] of Object.entries(require("./package.json").bin)) require("node:fs").symlinkSync("/app/" + target, "/usr/local/bin/" + name); require("node:fs").symlinkSync("/app/bin/ez", "/usr/local/bin/ez")'
35
39
  # Build identity for relay status (package version stays release-owned).
@@ -8,9 +8,12 @@ The container is built locally; no bundled third-party binary image is promised.
8
8
 
9
9
  Direct runtime dependencies: grammy (MIT), tsx (MIT). Development dependencies
10
10
  include TypeScript (Apache-2.0) and Node type definitions (MIT). The runtime
11
- image installs ffmpeg, Debian packages, and these reviewed CLI versions:
12
- `@openai/codex` 0.153.4 (Apache-2.0), `opencode-ai` 1.18.32 (MIT), and
13
- `@earendil-works/pi-coding-agent` 0.87.1 (MIT). Redistributing the image
11
+ image installs ffmpeg, Poppler (`poppler-utils`, GPL-2.0-or-later), Debian packages, and these reviewed CLI versions:
12
+ `@openai/codex` (Apache-2.0; the build resolves and records the selected version),
13
+ `opencode-ai` 1.18.32 (MIT), `@earendil-works/pi-coding-agent` 0.87.1 (MIT), and
14
+ `@anthropic-ai/claude-code` 2.1.286 (proprietary; Anthropic's legal agreements,
15
+ as referenced by its installed LICENSE.md, govern use). Preserve its installed
16
+ license and README; it is not covered by this repository's MIT license. Redistributing the image
14
17
  requires review of its exact package build, notices and corresponding-source
15
18
  obligations. Docker and
16
19
  host-capable AI CLIs remain separately installed products, not covered by this
package/bin/ez CHANGED
@@ -91,6 +91,13 @@ const main = async () => {
91
91
  process.stdout.write(JSON.stringify({commands:['tools list [--details]','tools skill <plugin> <declared-path>','<registered CLI> ...'],native:await installedNativeHelp(fileURLToPath(new URL('.',import.meta.url)))})+'\n');
92
92
  return;
93
93
  }
94
+ if(args[0]==='--task-application') {
95
+ if(args.length!==1 || !runId || !RUN_ID.test(runId))throw Error('Active task run required')
96
+ const result=await request({version:1,id:randomUUID(),operation:'task-launch',runId,stdin:await readStdin()})
97
+ if(!result?.ok)throw Error(result?.error || 'Task launch failed')
98
+ if(result.stdout)process.stdout.write(result.stdout)
99
+ process.exitCode=result.code;return
100
+ }
94
101
  const stdin = ['plugins', 'tools', 'updates', 'status'].includes(args[0]) ? '' : await readStdin();
95
102
  if (['plugins', 'tools', 'updates', 'status'].includes(args[0])) await manager(args);
96
103
  else await invoke(args, stdin);
@@ -0,0 +1,54 @@
1
+ // Trusted operator diagnostic, executed inside the installed broker. Never a
2
+ // socket operation or native-run substitute; no command execution or writes.
3
+ import * as fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { createHash } from 'node:crypto';
6
+ import { loadPluginBrokerBinding } from '/app/src/plugin-broker.mjs';
7
+ import { registry, snapshot, checkFolders, hostNetworkBindings } from '/app/src/plugins/manager.mjs';
8
+
9
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
10
+ export async function brokerReadiness(input, expected) {
11
+ if (!Array.isArray(expected) || expected.length !== 3 || expected.some(value => !/^[a-f0-9]{64}$/.test(value)))
12
+ throw Error('Expected host, registry and configuration fingerprints');
13
+ const binding = await loadPluginBrokerBinding(input);
14
+ const files = [binding.hostConfig, path.join(binding.home, 'registry.json'), path.join(binding.home, 'config.json')];
15
+ const verify = async () => {
16
+ for (let i = 0; i < files.length; i++) if (hash(await fs.readFile(files[i])) !== expected[i])
17
+ throw Error('Isolated broker configuration fingerprint mismatch');
18
+ };
19
+ await verify();
20
+ if (!(await fs.stat(binding.socket)).isSocket()) throw Error('Isolated broker socket unavailable');
21
+ const config = JSON.parse(await fs.readFile(files[2], 'utf8'));
22
+ const r = await registry(binding.home), plugins = [];
23
+ for (const [id, record] of Object.entries(r.plugins)) {
24
+ const trusted = await snapshot(record.source);
25
+ if (trusted.manifest.id !== id || record.source !== trusted.source || record.revision !== trusted.revision ||
26
+ JSON.stringify(record.manifest) !== JSON.stringify(trusted.manifest) ||
27
+ JSON.stringify(record.deployment) !== JSON.stringify(trusted.deployment) ||
28
+ JSON.stringify(record.sharedRevisions || {}) !== JSON.stringify(trusted.sharedRevisions))
29
+ throw Error('Registry plugin identity is not the reviewed source');
30
+ for (const alias of Object.keys(record.manifest.commands))
31
+ if (r.commands[alias] !== id || !record.deployment.commands[alias]) throw Error('Corrupt command registry');
32
+ await checkFolders(config, record, binding.home);
33
+ await hostNetworkBindings(config, record, binding.home);
34
+ plugins.push({ id, revision: record.revision });
35
+ }
36
+ await verify();
37
+ return { version: 1, ready: true, hostSha256: expected[0], registrySha256: expected[1], configSha256: expected[2], plugins };
38
+ }
39
+ {
40
+ const args = process.argv.slice(process.argv[1]?.endsWith('/broker-readiness.mjs') ? 2 : 1);
41
+ try {
42
+ const result = await brokerReadiness({
43
+ home: process.env.EZ_PLUGIN_BROKER_HOME, workspace: process.env.EZ_PLUGIN_BROKER_WORKSPACE,
44
+ controlDir: process.env.EZ_PLUGIN_BROKER_CONTROL_DIR, socket: process.env.EZ_PLUGIN_BROKER_SOCKET,
45
+ hostConfig: process.env.EZ_PLUGIN_BROKER_HOST_CONFIG,
46
+ }, args);
47
+ console.log(JSON.stringify(result));
48
+ } catch {
49
+ // Validation exceptions can contain private paths. Keep operator/public
50
+ // readback content-free; updater still retains failure and rollback state.
51
+ console.error('EZ_BROKER_STRUCTURAL_READINESS_FAILED');
52
+ process.exitCode = 1;
53
+ }
54
+ }
@@ -0,0 +1,86 @@
1
+ // Explicit synthetic Linux mount regression. No credentials, native identities,
2
+ // provider calls or existing deployments. Run against the exact reviewed image:
3
+ // EZ_RELAY_IMAGE=<immutable image ID> node docker/broker-update-smoke.mjs
4
+ import * as fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import assert from 'node:assert/strict';
7
+ import { execFileSync } from 'node:child_process';
8
+ import { createHash } from 'node:crypto';
9
+ import { atomic, snapshot } from '../src/plugins/manager.mjs';
10
+ import { execute, refreshBroker } from '../src/updates/runtime.mjs';
11
+ const docker=(...args)=>execFileSync('docker',args,{encoding:'utf8',stdio:['ignore','pipe','pipe'],timeout:120000}).trim();
12
+ const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
13
+ if(process.argv[2]!=='--inside') {
14
+ const image=process.env.EZ_RELAY_IMAGE;
15
+ if(!/^sha256:[a-f0-9]{64}$/.test(image||''))throw Error('Supply the exact reviewed runtime image ID');
16
+ const volume=`e245-${process.pid}`;
17
+ try {
18
+ docker('volume','create',volume);
19
+ const root=JSON.parse(docker('volume','inspect',volume))[0].Mountpoint;
20
+ const output=docker('run','--rm','--network','none','--mount',`type=volume,source=${volume},target=${root}`,
21
+ '--mount','type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock',
22
+ '--env','EZ_DOCKER_COMPOSE=standalone','--entrypoint','node',image,'--import','/app/node_modules/tsx/dist/loader.mjs',
23
+ '/app/docker/broker-update-smoke.mjs','--inside',root,image);
24
+ console.log(output);
25
+ }finally {docker('volume','rm',volume);}
26
+ } else {
27
+ const root=process.argv[3],image=process.argv[4],home=path.join(root,'tools'),workspace=path.join(root,'mind'),controlDir=path.join(root,'control');
28
+ const hostConfig=path.join(root,'host-executor.json'),socket=path.join(controlDir,'b.sock'),project=`e245-${hash(root).slice(0,12)}`;
29
+ const config={schemaVersion:1,workspace,hostConfig,deploymentDir:root};
30
+ const args=['compose','--env-file',path.join(root,'docker.env')];
31
+ const call=(tail)=>execute('docker',[...args,...tail],{stdoutOnly:true,timeout:120000});
32
+ const container=async name=>(await call(['ps','-q',name])).trim();
33
+ const inside=async script=>call(['exec','-T','plugin-broker','node','--input-type=module','-e',script]);
34
+ const prepare=()=>inside(`import {prepareCommand} from '/app/src/plugins/manager.mjs'; await prepareCommand(${JSON.stringify(home)},'sample',[]); console.log('prepared');`);
35
+ const mounted=()=>inside(`import fs from 'node:fs'; import {createHash} from 'node:crypto'; console.log(createHash('sha256').update(fs.readFileSync(${JSON.stringify(hostConfig)})).digest('hex'));`);
36
+ const fingerprints=async()=>Promise.all([hostConfig,path.join(home,'registry.json'),path.join(home,'config.json')].map(async f=>hash(await fs.readFile(f))));
37
+ const probe=async()=>call(['exec','-T','plugin-broker','node','--import','/app/node_modules/tsx/dist/loader.mjs','/app/docker/broker-readiness.mjs',...await fingerprints()]);
38
+ const agent={name:'synthetic',toolsHome:home,workspace,controlDir};
39
+ const host=(revisions)=>({isolation:'isolated',agents:[{...agent,pluginNetworkBindings:{sample:{revisions,bindings:[{service:'sample',network:'synthetic-reviewed-network'}]}}}]});
40
+ const records=[];
41
+ for(const directory of [home,workspace,controlDir])await fs.mkdir(directory,{recursive:true});
42
+ for(const version of ['1.0.0','1.0.1']) {
43
+ const source=path.join(home,'packages','sample',version);
44
+ await fs.mkdir(path.join(source,'bin'),{recursive:true});
45
+ const manifest={schemaVersion:1,id:'sample',version,commands:{sample:{executable:'bin/example.mjs',args:[]}},skills:[]};
46
+ const deployment={schemaVersion:1,services:{sample:{buildTarget:'runtime',volumes:{},healthcheck:['node','--version']}},commands:{sample:{service:'sample',argv:['node','/app/bin/example.mjs']}},exports:{}};
47
+ for(const [name,body] of Object.entries({'package.json':JSON.stringify({name:'synthetic-plugin',version,files:['bin']}),'ez-plugin.json':JSON.stringify(manifest),'ez-deployment.json':JSON.stringify(deployment),'Dockerfile':'FROM scratch AS runtime','.dockerignore':'','bin/example.mjs':'console.log("synthetic")'}))await fs.writeFile(path.join(source,name),body);
48
+ const s=await snapshot(source);
49
+ records.push({source,revision:s.revision,manifest:s.manifest,deployment:s.deployment,sharedRevisions:s.sharedRevisions,project:`ezp-${hash(home).slice(0,16)}-sample`,compose:path.join(home,'packages','sample','compose.json')});
50
+ }
51
+ const [a,b]=records;
52
+ const publish=record=>atomic(path.join(home,'registry.json'),{schemaVersion:1,owner:home,plugins:{sample:record},commands:{sample:'sample'}});
53
+ await atomic(path.join(home,'config.json'),config);await atomic(hostConfig,host([a.revision]));await publish(a);
54
+ const brokerEnv={EZ_PLUGIN_BROKER_HOME:home,EZ_PLUGIN_BROKER_WORKSPACE:workspace,EZ_PLUGIN_BROKER_CONTROL_DIR:controlDir,EZ_PLUGIN_BROKER_HOST_CONFIG:hostConfig,EZ_PLUGIN_BROKER_SOCKET:socket,EZ_DOCKER_COMPOSE:'standalone'};
55
+ const sleep=['node','-e','setInterval(()=>{},1000)'];
56
+ const compose={services:{relay:{image,network_mode:'none',entrypoint:sleep},'plugin-broker':{image,network_mode:'none',entrypoint:['node','--import','/app/node_modules/tsx/dist/loader.mjs','/app/src/plugin-broker.mjs'],environment:brokerEnv,volumes:[`${home}:${home}`,`${workspace}:${workspace}:ro`,`${controlDir}:${controlDir}`,`${hostConfig}:${hostConfig}:ro`,'/var/run/docker.sock:/var/run/docker.sock'],healthcheck:{test:['CMD','node','-e',`process.exit(require('fs').statSync(${JSON.stringify(socket)}).isSocket()?0:1)`],interval:'1s',timeout:'5s',retries:15}}}};
57
+ await atomic(path.join(root,'compose.json'),compose);
58
+ await fs.writeFile(path.join(root,'docker.env'),`COMPOSE_PROJECT_NAME=${project}\nCOMPOSE_FILE=${path.join(root,'compose.json')}\nEZ_EXECUTOR_TRANSPORT=local\n`);
59
+ try {
60
+ await call(['up','-d','--no-build','--pull','never','--wait','--wait-timeout','30']);
61
+ const relay=await container('relay'),oldBroker=await container('plugin-broker'),oldHash=(await mounted()).trim();
62
+ assert.equal(JSON.parse(await probe()).ready,true);assert.equal((await prepare()).trim(),'prepared');
63
+ await atomic(hostConfig,host([a.revision,b.revision]));await publish(b);
64
+ assert.equal((await mounted()).trim(),oldHash);assert.notEqual(oldHash,(await fingerprints())[0]);
65
+ await assert.rejects(prepare(),/not pinned/);await assert.rejects(probe(),/STRUCTURAL_READINESS_FAILED/);
66
+ console.log('PASS stale inode: live socket; mounted A; registry B rejected; structural probe rejected');
67
+ const ready=await refreshBroker(config);
68
+ const newBroker=await container('plugin-broker');assert.notEqual(newBroker,oldBroker);assert.equal(await container('relay'),relay);
69
+ assert.equal((await mounted()).trim(),ready.hostSha256);assert.equal((await prepare()).trim(),'prepared');
70
+ assert.equal(ready.plugins[0].revision,b.revision);
71
+ console.log(JSON.stringify({stage:'activation',oldBroker,newBroker,relay,hostSha256:ready.hostSha256,registrySha256:ready.registrySha256,revision:b.revision}));
72
+ await atomic(hostConfig,host([a.revision]));
73
+ await assert.rejects(refreshBroker(config),/STRUCTURAL_READINESS_FAILED/);await assert.rejects(prepare(),/not pinned/);
74
+ console.log('PASS unapproved B remains rejected after recreation');
75
+ await publish(a);const rolledBack=await refreshBroker(config);
76
+ assert.equal((await mounted()).trim(),rolledBack.hostSha256);assert.equal(rolledBack.plugins[0].revision,a.revision);assert.equal((await prepare()).trim(),'prepared');assert.equal(await container('relay'),relay);
77
+ console.log(JSON.stringify({stage:'rollback',broker:await container('plugin-broker'),relay,hostSha256:rolledBack.hostSha256,registrySha256:rolledBack.registrySha256,revision:a.revision}));
78
+ // Read-only probe must neither repair a tampered snapshot nor create leases.
79
+ await fs.appendFile(path.join(a.source,'bin/example.mjs'),'\n// unreviewed');await assert.rejects(probe(),/STRUCTURAL_READINESS_FAILED/);
80
+ await fs.writeFile(path.join(a.source,'bin/example.mjs'),'console.log("synthetic")');
81
+ await assert.rejects(call(['exec','-T','--env',`EZ_PLUGIN_BROKER_HOST_CONFIG=${root}/missing.json`,'plugin-broker','node','--import','/app/node_modules/tsx/dist/loader.mjs','/app/docker/broker-readiness.mjs',...await fingerprints()]),/STRUCTURAL_READINESS_FAILED/);
82
+ await fs.rm(hostConfig);await assert.rejects(probe(),{code:'ENOENT'});
83
+ await atomic(hostConfig,host([a.revision]));await refreshBroker(config);
84
+ console.log('PASS source tamper and missing host file fail; no provider command or native run identity used');
85
+ }finally {await call(['down','--volumes','--remove-orphans']);}
86
+ }
package/docker/run.ts CHANGED
@@ -20,7 +20,7 @@ try { privateEnv = parseEnv(readFileSync(3, 'utf8')) } catch (error) {
20
20
  if ((error as NodeJS.ErrnoException).code !== 'EINVAL' && (error as NodeJS.ErrnoException).code !== 'EBADF') throw error
21
21
  } finally { try { closeSync(3) } catch {} }
22
22
  for (const [key, value] of Object.entries(privateEnv)) {
23
- if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'EZ_CHANNEL_BACKEND_TOKEN'].includes(key)) process.env[key] = value
23
+ if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY', 'EZ_SPEECH_PROVIDER', 'EZ_SPEECH_MODEL', 'EZ_SPEECH_VOICE_EN', 'EZ_SPEECH_VOICE_ES', 'EZ_CHANNEL_BACKEND_TOKEN', 'EZ_SCHEDULED_CONCURRENCY'].includes(key)) process.env[key] = value
24
24
  }
25
25
  privateEnv = {}
26
26
  const [command = 'start', ...args] = process.argv.slice(2)
@@ -20,7 +20,7 @@ unfinished run returns 409, missing speech configuration 503, and provider
20
20
  failure 502. Depleted Gemini credits return 503 with
21
21
  `code: speech_credits_depleted`. Audio responses have `Cache-Control: no-store`.
22
22
 
23
- Configure `GEMINI_API_KEY` on the relay. The existing speech renderer uses
23
+ Configure `GEMINI_API_KEY` on the relay. Alternatively select `EZ_SPEECH_PROVIDER=openrouter`, supply `OPENROUTER_API_KEY` privately on the relay, and set `EZ_SPEECH_MODEL` (default `fish-audio/s2.1-pro-free:free`). The same renderer converts provider MP3 to the channel’s WAV or Ogg Opus format; provider failures do not select another provider. The existing speech renderer uses
24
24
  `gemini-3.8-flash-lite-tts`; Telegram still receives Ogg Opus. Applications should
25
25
  send `{"language":"en"}` or `{"language":"es"}` to select
26
26
  `EZ_SPEECH_VOICE_EN` or `EZ_SPEECH_VOICE_ES` (both default to Kore). Telegram
@@ -451,3 +451,34 @@ outgoing file downloads remain unsupported by this HTTP channel.
451
451
 
452
452
  Private scope model selections are stored on that scope’s native-session binding.
453
453
  They do not add entries to the owner’s saved AI menu or consume its preset limit.
454
+
455
+ ## Restricted discussion browser handoff
456
+
457
+ A trusted application may host a browser conversation for existing exact-contact
458
+ tasks without inheriting the owner’s workspace or general tools. Outside native
459
+ turns, the installing administrator explicitly grants an installed launch command:
460
+
461
+ ```sh
462
+ ezenciel-agents-application --id browser --task-launch-command voice \
463
+ --task-launch-args '["launch","--task"]' --task-launch-tasks '["task_0123456789abcdef0123456789abcdef"]'
464
+ ```
465
+
466
+ Use `--task-launch-tasks '"all"'` only with owner approval for all active
467
+ exact-contact discussions. `--disable-task-launch` revokes this handoff while
468
+ keeping the application binding. Wildcard public tasks cannot launch links.
469
+ The restricted native model sees `browser_link` with the approved application
470
+ names; it cannot choose another task, command arguments or identity. The trusted
471
+ broker executes the installed command with a 20-minute task bearer through private
472
+ stdin. The model receives only the resulting short-lived HTTPS browser link.
473
+ This is an explicit mutating capability, separate from read-only channel queries.
474
+
475
+ The backend checks its task bearer with `POST /v1/registration` and
476
+ `{taskToken}`. It submits `{requestId,scope:"task:TASK_ID",text,taskToken}` to
477
+ `/v1/runs`. Core derives the exact task and contact, binds the original grant and
478
+ application, rechecks owner/source/account/expiry/revocation, and uses the same
479
+ restricted native task executor and task notes. Task requests cannot select
480
+ owner history, AI, files or native sessions. `send` replies to the application
481
+ inbox as text; it does not send an additional channel message. The task bearer
482
+ is hashed in core storage, stays in the trusted backend and expires no later
483
+ than the task. Changed retries and cross-task/application use fail closed.
484
+ No contributor becomes an installation owner.
@@ -31,6 +31,16 @@ cross-turn, command, file and unrelated requests are declined rather than being
31
31
  guessed or forwarded to a hidden task UI.
32
32
  Legacy unbound sessions require the owner's explicit `/new`.
33
33
 
34
+ Like Codex's `CODEX_HOME`, Claude runs with an agent-bound `CLAUDE_CONFIG_DIR`
35
+ (`control/cli/claude`): settings, memory, plugins and sessions belong to the
36
+ agent, never the operator's `~/.claude`. Host-capable agents share only the
37
+ installer's OAuth/subscription login by setting `CLAUDE_SECURESTORAGE_CONFIG_DIR`
38
+ empty, so Claude reads and refreshes its default credential store in place. API
39
+ key, `apiKeyHelper` or provider `env` setups in the operator's `~/.claude` are
40
+ not shared; put them in the agent-bound directory instead. Isolated agents log in
41
+ inside their own config directory. Claude conversations started before this
42
+ binding live in the operator's `~/.claude` and need `/new`.
43
+
34
44
  References used for the adapter contract:
35
45
 
36
46
  - [Codex non-interactive execution](https://learn.chatgpt.com/docs/non-interactive-mode)
@@ -42,7 +52,12 @@ environment and the execution-bound `CODEX_HOME`; discovery starts no session
42
52
  or inference. Only visible model names and supported efforts are projected.
43
53
  Unavailable discovery retains the native cache. The installed native version
44
54
  still owns which models it advertises; Core never guesses newly released aliases.
45
- Grok/Codex/OpenCode support explicit listed model/effort choices. Other installed clients
55
+ Grok/Codex/OpenCode/Claude support explicit listed model/effort choices. Claude has
56
+ no catalog command, so the installed `claude --help` is its native metadata: the
57
+ documented model aliases and `--effort` levels are listed alongside the client
58
+ default; unparseable help keeps the client default only. A versioned model name (for example
59
+ `claude-sonnet-5-5`) is offered by adding a `claude` entry to the agent's curated
60
+ `control/ai-models.json`; the alias `sonnet` always means the client's current Sonnet. Other installed clients
46
61
  offer their own default only in this slice. OpenCode lists the installed `opencode models`
47
62
  catalog with the whitelisted executor environment, so only models the relay can actually
48
63
  run are offered; provider variants become selectable efforts (`--variant`).
@@ -64,8 +79,8 @@ Refresh available AIs repeats default discovery. Active/default
64
79
  presets and queued snapshots are preserved; discovery only refreshes unused
65
80
  detected entries.
66
81
  Codex uses its native `config/read` interface; Grok reads its documented user
67
- model/effort settings (or `models` for the default model). Claude reads user and
68
- workspace JSON settings; OpenCode reports resolved config. Unknown defaults and
82
+ model/effort settings (or `models` for the default model). Claude reads the agent-bound
83
+ user settings and workspace JSON settings; OpenCode reports resolved config. Unknown defaults and
69
84
  opaque wrappers remain explicitly “client default”. No credentials are stored,
70
85
  no inference runs, no new dependency, and no cross-CLI session transfer.
71
86
 
@@ -272,6 +272,29 @@ workspace (a tenant farm), while allowing explicitly shared and additional
272
272
  workspace grants and leaving `$HOME` readable for the host CLI.
273
273
  Codex then uses `danger-full-access` so it does not apply a nested Seatbelt.
274
274
 
275
+ ## Claude Code auth
276
+
277
+ Isolated agents run the image Claude Code with `CLAUDE_CONFIG_DIR` set to the
278
+ agent-bound `control/cli/claude`; settings, memory, plugins and sessions stay
279
+ there. Log the agent in once inside its relay, which writes its own
280
+ `.credentials.json` there and never shares or copies the operator login:
281
+
282
+ ```sh
283
+ docker compose exec -it -u "${EZ_RUNTIME_UID:-1000}:${EZ_RUNTIME_GID:-1000}" relay \
284
+ sh -c 'CLAUDE_CONFIG_DIR="$EZ_CONTROL_DIR/cli/claude" claude auth login'
285
+ ```
286
+
287
+ To share one login across many agents without logging in one by one, create a
288
+ long-lived token once with `claude setup-token` and copy it to each agent's
289
+ `control/cli/claude/oauth-token` (mode `600`). It does not refresh, so agents
290
+ cannot invalidate each other's session the way copied `.credentials.json`
291
+ files can. The file's contents reach only the Claude process as
292
+ `CLAUDE_CODE_OAUTH_TOKEN`; a malformed file fails the run closed.
293
+
294
+ Without any login, Claude turns fail closed with the native "not logged in"
295
+ error. Host-capable agents use the same agent-bound directory but share the
296
+ installer login in place (`CLAUDE_SECURESTORAGE_CONFIG_DIR` empty).
297
+
275
298
  ## OpenCode auth
276
299
 
277
300
  Isolated agents run the image OpenCode with `XDG_DATA_HOME` set to the
@@ -319,3 +342,9 @@ restart after repair. Existing authorized work and outbox draining remain active
319
342
  polling health remains unhealthy. Transient HTTP, rate-limit and server errors
320
343
  may reconnect. grammY still owns reconnect behavior inside its poller; ez does
321
344
  not override private library methods or implement a replacement poller.
345
+
346
+ The isolated executor image includes Node, jq and Python 3 (`python` and
347
+ `python3`) for task-local JSON/text processing. Domain calculations and writes
348
+ remain behind installed plugins. Registered command admission waits up to five
349
+ seconds for the short registry lock; lifecycle changes still fail closed, and
350
+ no caller removes an existing lock. Serialize calls when a plugin requires it.
@@ -0,0 +1,66 @@
1
+ # Isolated plugin update readiness
2
+ Status: proposed acceptance contract; not a claim of installed behavior.
3
+ Source baseline: f192d57d8b1ea2748fe31544ffdb968c827b891e.
4
+ Scope: one causal repair to isolated broker activation and verification.
5
+
6
+ ## Objective and boundaries
7
+ An authorized plugin update must leave the isolated native execution path able to resolve and prepare the exact reviewed plugin revision, or report failure with the existing recovery path. A successful host-side command, running container or socket is insufficient.
8
+
9
+ Constraints:
10
+ 1. Preserve reviewed-revision, host-network, folder, owner and per-run authorization checks. Never authorize a revision merely to make a probe pass.
11
+ 2. Keep one registry, broker and updater; no new daemon, retry controller, scheduler or financial workflow.
12
+ 3. Preserve the relay, native sessions, unrelated plugins and persistent state during plugin-only refresh.
13
+ 4. No credentials, customer data or production run identities in tests or public receipts.
14
+ 5. Existing update serialization, active-work admission and uncertain-write reconciliation remain binding.
15
+
16
+ Owner: updater/broker boundary, principally src/updates/runtime.mjs and existing plugin validation. This does not change domain procedures or models.
17
+
18
+ ## Observed mechanism and confidence
19
+ compose.yaml bind-mounts the host configuration as a single read-only file into plugin-broker. Configuration writers use temporary-file plus rename. A single-file bind mount can retain the prior inode after host-side atomic replacement. The plugin registry can therefore select the new reviewed revision while the broker still sees the old revision list. hostNetworkBindings in src/plugins/manager.mjs correctly rejects that mismatch.
20
+
21
+ refreshBroker currently invokes Compose restart after plugin registry activation and rollback. That does not explicitly recreate the container/mount. The current broker health check only verifies socket existence. The existing updater regression test confirms restart and inventory version, not mounted configuration identity or executable readiness.
22
+
23
+ Production diagnosis observed different host and mounted revision authorization, and targeted broker recreation restored the failing boundary. This supports the stale-mount mechanism; the exact historical writer/commit that first replaced the file is not yet established. Do not invent that provenance or attribute every domain deferral to this incident.
24
+
25
+ ## Smallest implementation
26
+ First reproduce the failure with a synthetic deployment. Prefer fixing lifecycle of the existing narrowly scoped mount rather than broadening the mount or making authorization mutable inside the broker.
27
+
28
+ 1. After approved activation changes and registry publication, refresh the isolated broker using the deployment's existing Compose identity and exact installed image. Recreate only plugin-broker, without dependencies/build/pull or relay replacement, and wait for readiness. Use supported equivalent Compose flags rather than hardcoding another project or image.
29
+ 2. Apply the same mount-refresh rule to rollback. Restore the old registry/config state owned by the existing transaction, then verify the old revision is again usable. Never restore provider journals or silently erase subsequent unrelated state.
30
+ 3. Ensure every updater path that replaces a relevant host file and retains an unchanged broker image cannot skip mount refresh merely because Compose sees the same image/configuration. Include main-update activation/rollback where relevant; do not gratuitously recreate healthy relay processes.
31
+ 4. Before completed, compare broker-visible configuration identity with the intended host configuration and verify installed registry/source revision consistency and existing host-network/folder validation. The check must execute inside the actual isolated broker environment, be read-only and bounded, and expose no configuration contents or secrets.
32
+ 5. Separate structural readiness from real native invocation evidence. A structural probe must not fabricate EZ_RUN_ID, borrow another run, create permanent authorization, or invoke an arbitrary plugin command that might mutate. Prefer existing prepare/validation primitives with a narrowly scoped trusted diagnostic if no supported read-only probe exists. An actual harmless command through a genuine authorized native run is a separate acceptance step.
33
+ 6. A mismatched pin, missing mounted file, failed recreate or failed verification must prevent completed. Use current failed/rollback/recovery-required states and preserve error evidence. Do not swallow probe failures.
34
+ 7. Keep socket liveness distinguishable from functional readiness in existing status/diagnostics. Do not build an expensive provider poll into a high-frequency health check or restart endlessly. Document exactly what each level proves.
35
+
36
+ If code inspection finds a simpler existing supported mechanism meeting all these requirements, use it and explain the equivalence in the PR. Do not bypass pinning, rewrite authorized revisions, mount the entire private deployment directory, or switch execution transport.
37
+
38
+ ## Regression evidence
39
+ Use synthetic, isolated state and real Docker bind-mount behavior where required:
40
+ - Start with approved revision A and a single-file read-only host binding.
41
+ - Atomically replace host configuration with independently approved revision B and activate registry B; reproduce old mounted bytes and rejection in the uncorrected harness.
42
+ - Apply corrected refresh; prove broker sees exact new file identity/content hash and prepares B successfully. Record container identity change and unchanged relay identity.
43
+ - Prove a still-unapproved B remains rejected. Recreate is not authorization.
44
+ - Cover rollback B to A, including mounted bytes, registry identity and readiness.
45
+ - Recreate failure and readback/probe failure must not return completed.
46
+ - A socket that exists while configuration is stale or invalid must not count as functional-ready.
47
+ - Host-capable updates should not start an isolated broker.
48
+ - Preserve existing invalid owner/run, canceled/revoked admission, folder/network mismatch and secret-isolation tests relevant to changed code.
49
+ - Respect active work through existing updater admission. Do not manufacture a second drain/queue mechanism.
50
+
51
+ Update the existing restart-only test to assert the meaningful behavior. Mocked Compose argument assertions alone cannot prove Linux bind-mount recovery. No test should require customer credentials or invoke a financial command.
52
+
53
+ ## Verification and review
54
+ Run required repository checks on final source: pnpm verify, pnpm run release:check, git diff --check and applicable Docker/packed-artifact checks in docs/releasing.md. Identify test counts, skipped/unrun checks and exact commit/artifact hash honestly. Keep one focused PR and do not include unrelated media work.
55
+
56
+ The implementation author supplies the final diff, root-cause reproduction, before/after evidence and limits. The assigned reviewer personally checks architecture, correctness, negative cases and exact-head CI, then merges using expected-head protection. Changes after review require affected revalidation. This specification is authored by the reviewer; its authorship is disclosed, and implementation verification must not be described as independent review of the reviewer's own design.
57
+
58
+ ## Direct VM acceptance after merge
59
+ Use the deployment's supported updater on the reviewed artifact. Before activation, record non-secret installed revision/image identities, native active-work state and baseline read-only access; preserve ongoing work and use the updater's normal drain. Never force a restart during an active call.
60
+
61
+ Verify exact installed commit/artifact and broker/relay identities, mounted configuration fingerprint, reviewed registry identity and structural readiness. Then inspect a genuine native invocation using the same bound launcher, with a harmless help/manifest read and its real run receipt. Do not trigger scores, leverage, trades or other investment workflows as a smoke test. If no authorized diagnostic invocation exists, retain native proof pending the next natural invocation rather than inventing identity.
62
+
63
+ Success: the corrected lifecycle demonstrably handles atomic binding replacement and rollback, and the installed isolated path accepts the already authorized revision without operator recreation. Source merge, service health and a host-side doctor alone are not success.
64
+
65
+ ## Separate subsequent work
66
+ Outage-to-engineering acknowledgement, recovery/resumption ownership, large-file archive transfer and shared daily-ledger contention are separate causes. They remain follow-up investigations; do not add automatic domain replay, broaden stdin limits, change schedules or rewrite record ownership in this PR.
package/docs/releasing.md CHANGED
@@ -17,6 +17,10 @@ separate release bot is not required.
17
17
  3. Build both Docker targets from a clean checkout:
18
18
  `docker build --target test -t ez-release-tests .` and
19
19
  `docker build --target runtime -t ez-release-runtime .`.
20
+ Docker CI pins the reviewed Codex baseline (`TASK_CODEX_VERSION`) with the
21
+ existing `CODEX_CLI_VERSION` build argument; update that pin when the native
22
+ audit advances. Baseline CI does not validate a newly published native version.
23
+ Managed installs and upgrades keep their existing native version selection.
20
24
  Main relay: `EZ_RELAY_IMAGE=ez-release-runtime node docker/smoke.mjs`.
21
25
  The WhatsApp Compose overlay has no separate relay image; verify its
22
26
  provider integration with the synthetic plugin smoke in `docs/plugins.md`.
@@ -55,6 +55,71 @@ Paused, stale-owner, overlapping or held tasks and unmet conditions cannot be
55
55
  triggered. Historical deferred tasks keep their source context
56
56
  available through `ezenciel-agents-schedule context`.
57
57
 
58
+ ## Registered script schedules
59
+
60
+ A schedule has one execution type. **Agent** schedules run the saved prompt with
61
+ their saved engine, model and effort, as described below. **Script** schedules run
62
+ a registered, agent-owned script directly: no model, effort, prompt or native
63
+ session is involved.
64
+
65
+ ```sh
66
+ ezenciel-agents-schedule script register book-freshness \
67
+ --file scripts/book-freshness.mjs --interpreter node [--arg=VALUE ...] [--timeout-seconds 900]
68
+ ezenciel-agents-schedule create actual-book-freshness-guard \
69
+ --script book-freshness [--arg=VALUE ...] --cron '15 * * * *' --timezone Asia/Dubai
70
+ ezenciel-agents-schedule script list | show ID | update ID [...] | remove ID
71
+ ```
72
+
73
+ The agent writes, tests and maintains the script; core owns registration,
74
+ scheduling, authorization, process execution, cancellation and run receipts.
75
+ A registration records the owner binding, a workspace-relative entry point, an
76
+ installed interpreter (command name on the executor PATH or absolute path),
77
+ literal arguments, a timeout and the entry point's SHA-256 under a new revision.
78
+ It references the workspace file; core keeps no source copy. Paths outside the
79
+ workspace (including symlink escapes) and shell command strings are rejected.
80
+ Core invokes `interpreter entry registrationArgs... scheduleArgs...` directly,
81
+ without a shell. Missing interpreters fail clearly; nothing is installed.
82
+
83
+ When an occurrence is queued, the run captures the current registration
84
+ revision and hash. Immediately before spawning, the executor requires that
85
+ revision to be current and the entry-point bytes to match. Changed bytes fail
86
+ the run until the agent explicitly runs `script update`. The hash identifies the
87
+ entry-point file only; it does not certify imported modules or other files.
88
+
89
+ Script runs use the existing scheduler, admission queue, scheduled concurrency
90
+ limit, one-occurrence-per-schedule rule, run records, delivery binding, manual
91
+ `trigger`, pause/resume/edit/remove and `cancel`. They run in the agent's
92
+ isolated environment (relay or host transport) under its normal identity, with
93
+ the executor environment allowlist plus `EZ_RUN_ID`, `EZ_SCHEDULE_ID`,
94
+ `EZ_SCHEDULE_REVISION`, `EZ_DUE_AT`, `EZ_SCRIPT_ID`, `EZ_SCRIPT_REVISION` and
95
+ `EZ_SCRIPT_SHA256`. Use the run/schedule/due identifiers to form a stable
96
+ occurrence identity. External operations go through the bound plugin CLI; a
97
+ script must not read private databases, call providers directly or acquire
98
+ credentials. A script run can read schedule and script state but cannot change
99
+ registrations or schedules.
100
+
101
+ A script runs with the agent's runtime identity and file access, but not inside
102
+ a native engine's own sandbox (for example host Codex `workspace-write`); register
103
+ only code the agent would be allowed to run itself. An interpreter inside the
104
+ workspace is rejected because it would be unhashed code.
105
+
106
+ Each registration has a bounded timeout (default 900 seconds, maximum 21600)
107
+ that does not affect agent runs. Timeout and `cancel` stop the process and its
108
+ children. Failed, timed-out or interrupted script runs are not retried, do not
109
+ fall back to an LLM and do not start a debugging agent. Inspect them with
110
+ `runs`, `run RUN_ID`, `failures` and `evidence`: the run shows the script ID,
111
+ revision and hash, queue/start/finish clocks, exit code, cancellation/timeout,
112
+ failure reason and a 4 KiB redacted tail of stdout/stderr. Those records follow
113
+ the normal relay-memory retention. Stdout is diagnostic only. Process success
114
+ means the script exited 0; domain success still requires the owning plugin's
115
+ validation, receipt and canonical readback, which the script performs and
116
+ records. Notifications use `ezenciel-agents-message` under the normal policy;
117
+ routine unchanged runs should stay quiet.
118
+
119
+ Editing a schedule replaces it completely, so it may switch execution type.
120
+ When replacing an agent schedule with a script, move any judgment the prompt
121
+ performed to its owning workflow first; the script schedule does not carry it.
122
+
58
123
  ## Execution and authority
59
124
 
60
125
  Due occurrences enter the durable queue with stable IDs and literal task text.
@@ -63,17 +128,20 @@ No identity files or role instructions are generated. Existing workspace Markdow
63
128
  provides context, including native `AGENTS.md` discovery and relative links; the
64
129
  engine chooses what to read. Per-run native state stays under `control/`.
65
130
 
66
- One foreground turn and one owner-scheduled turn can run at the same time.
67
- Foreground inputs queue behind foreground work; scheduled turns queue behind
68
- scheduled work. A long scheduled goal therefore does not block chat. Both streams
131
+ One foreground turn and up to six owner-scheduled turns can run at the same time.
132
+ Set `EZ_SCHEDULED_CONCURRENCY` to a positive integer to change the scheduled-job
133
+ limit; it defaults to six. For Docker deployments, put this setting in the
134
+ existing private relay environment file (`EZ_RELAY_ENV_FILE`); no Compose
135
+ layout migration is needed. Foreground inputs queue behind foreground work;
136
+ scheduled turns queue only when their limit is occupied. A long scheduled goal therefore does not block chat. Both streams
69
137
  use the existing admission and executor path, with separate native sessions.
70
138
  They share the agent-owned Markdown workspace: the agent coordinates shared file
71
139
  updates, and plugins enforce canonical record-write guards. The host keeps plugin
72
- workspace invocations excluded until both native turns finish.
140
+ workspace invocations excluded until all native turns finish.
73
141
  The agent decides when to send through the message CLI.
74
142
  A recurring schedule has at most one pending or active occurrence. Shared
75
- provider resources still need writer coordination. The former four background
76
- slots are no longer used for owner schedules.
143
+ provider resources still need writer coordination. This is admission to the existing native executor, not a second queue or
144
+ workflow controller.
77
145
 
78
146
  Production relay/host execution has no wall-clock timeout. The old
79
147
  `EZ_EXECUTOR_TIMEOUT_SECONDS` setting is ignored. Individual network/tool waits
@@ -170,6 +238,32 @@ Create a normal recurring schedule with `--every-seconds 900 --when unreviewed-f
170
238
 
171
239
  `failures [--all] [--limit N]` returns failedAt, reason, exit code, native session, captured error and runtime versions. Capture keeps at most 4 KiB of redacted stderr, including a failed native Codex turn's error message when supplied. A failure without error details remains explicitly unknown; historical failures are not backfilled. `run RUN_ID` reads an owned run. `review RUN_ID --failed-at ISO --status resolved|attention --diagnosis TEXT --recovery TEXT --outcome TEXT` records the investigation without rewriting execution history. A stale timestamp is rejected; a later failure needs a new review. Restricted reply, external and isolated-task callers cannot review failures. An attention review is handed off, not repeatedly relaunched; another new failure wakes the next review.
172
240
 
241
+ For a shareable operational projection, use `evidence [RUN_ID] --offset 0 --limit 100`.
242
+ This reads the live relay through its existing authenticated socket, under the
243
+ bound operator or an active unrestricted Telegram owner run. Application/channel,
244
+ external and isolated-task callers cannot use it. It exposes only current-owner
245
+ Telegram foreground/scheduled runs, stable IDs, clocks/status/presets, failure
246
+ metadata without prose, and retained run-associated outbox metadata. No native
247
+ session, prompt, reasoning, tool arguments/results, message text or channel identity
248
+ is returned. Failure text is represented by its SHA-256, not copied or diagnosed.
249
+
250
+ The response carries exact `readStartedAt`/`observedAt`, `snapshotSha256`, and
251
+ coverage total/offset/limit/returned/nextOffset. Follow `nextOffset` with the same
252
+ `--expected SNAPSHOT_SHA256`; changed retained evidence fails closed and requires
253
+ a fresh traversal. Each page recomputes the projection; no snapshot store or
254
+ background capture is added. History resets on relay restart and is bounded to
255
+ 2000 terminal runs and 2000 terminal outbox items; full historical coverage cannot
256
+ be inferred. Unassociated deliveries and excluded scopes remain unknown.
257
+
258
+ Delivery records identify queued/sent/failed/unknown state and whether a retained
259
+ Telegram receipt contains a valid clock and returned message IDs (only their count
260
+ is exported). The submitted UTF-8 text/voice-text hash can be compared to an exact
261
+ canonical source. It does not prove rendered or current recipient-side content;
262
+ attachments are not exported. Empty receipt lists never prove that no delivery
263
+ occurred. The relay does not retain native tool/delegation events, so that coverage
264
+ is explicitly unavailable. This command does not mark failures reviewed, send
265
+ messages, read private sessions, trigger work or certify owning results.
266
+
173
267
  The prompt controls diagnosis, authorized recovery and quiet notification behavior. Inspect prior effects and receipts before retrying anything. A failed review run itself remains visible as a new failure for the next occurrence.
174
268
 
175
269
  The Telegram Scheduled tasks menu lists enabled schedules that still have a pending