@jc_stack/ez-agents 0.1.0-beta.46 → 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 (91) hide show
  1. package/.dockerignore +2 -0
  2. package/.env.example +3 -0
  3. package/AGENTS.md +2 -2
  4. package/CHANGELOG.md +38 -0
  5. package/CONTRIBUTING.md +1 -1
  6. package/Dockerfile +18 -10
  7. package/THIRD_PARTY_NOTICES.md +6 -3
  8. package/bin/ez +7 -0
  9. package/docker/broker-readiness.mjs +54 -0
  10. package/docker/broker-update-smoke.mjs +86 -0
  11. package/docker/engine-pnpm-workspace.yaml +4 -0
  12. package/docker/run.ts +1 -1
  13. package/docker/smoke.mjs +1 -1
  14. package/docs/application-channel.md +32 -1
  15. package/docs/architecture/ai-selection.md +26 -5
  16. package/docs/architecture/authority-boundaries.md +2 -2
  17. package/docs/development-and-testing.md +1 -1
  18. package/docs/docker-runtime.md +39 -0
  19. package/docs/isolated-plugin-update-readiness.md +66 -0
  20. package/docs/plugin-guide.md +1 -1
  21. package/docs/plugins.md +1 -1
  22. package/docs/releasing.md +5 -1
  23. package/docs/scheduling.md +117 -9
  24. package/docs/selective-monitoring.md +13 -0
  25. package/docs/upgrades.md +38 -1
  26. package/docs/work-preflight.md +9 -0
  27. package/package.json +4 -3
  28. package/pnpm-workspace.yaml +7 -0
  29. package/scripts/smoke-scheduler.ts +4 -1
  30. package/src/ai.ts +41 -4
  31. package/src/application-channel.ts +43 -3
  32. package/src/application-cli.ts +7 -1
  33. package/src/application-origin.ts +2 -0
  34. package/src/audio.ts +37 -22
  35. package/src/client-defaults.ts +3 -2
  36. package/src/config.ts +9 -0
  37. package/src/control-state.ts +3 -1
  38. package/src/delivery-socket-client.d.mts +2 -0
  39. package/src/delivery-socket-client.mjs +2 -0
  40. package/src/delivery-socket.ts +38 -1
  41. package/src/event-sources.ts +2 -2
  42. package/src/executor.ts +72 -1
  43. package/src/files.ts +47 -3
  44. package/src/host-executor.ts +40 -26
  45. package/src/index.ts +42 -17
  46. package/src/install-tools.mjs +18 -9
  47. package/src/menu.ts +4 -2
  48. package/src/model-policy.ts +4 -0
  49. package/src/plugin-broker.mjs +47 -4
  50. package/src/plugins/manager.mjs +11 -4
  51. package/src/run-evidence.ts +78 -0
  52. package/src/runs.ts +16 -2
  53. package/src/schedule-cli.ts +80 -10
  54. package/src/schedule-preflight.ts +23 -0
  55. package/src/scheduled-tasks.ts +16 -12
  56. package/src/scheduler.ts +102 -16
  57. package/src/scripts.ts +167 -0
  58. package/src/setup.ts +2 -2
  59. package/src/task-capability.ts +17 -1
  60. package/src/task-executor.ts +11 -8
  61. package/src/task-mcp.ts +20 -8
  62. package/src/tasks.ts +89 -9
  63. package/src/updates/runtime.mjs +53 -11
  64. package/src/updates/supervisor.mjs +23 -13
  65. package/test/ai.test.ts +85 -5
  66. package/test/application-channel.test.ts +16 -1
  67. package/test/audio.test.ts +19 -0
  68. package/test/channel-owner.test.ts +2 -2
  69. package/test/config.test.ts +8 -0
  70. package/test/desktop-bridge.test.ts +2 -1
  71. package/test/executor.test.ts +37 -0
  72. package/test/failure.test.ts +4 -4
  73. package/test/files.test.ts +44 -0
  74. package/test/host-executor.test.ts +49 -6
  75. package/test/install-json.test.mjs +21 -0
  76. package/test/install-tools.test.mjs +6 -5
  77. package/test/native-tasks.test.ts +1 -1
  78. package/test/plugin-broker.test.mjs +64 -0
  79. package/test/plugin-manager.test.mjs +20 -0
  80. package/test/private-speech-config.test.ts +22 -0
  81. package/test/run-evidence.test.ts +78 -0
  82. package/test/schedule-cli.test.ts +47 -3
  83. package/test/schedule-preflight.test.ts +49 -0
  84. package/test/scheduled-tasks.test.ts +2 -2
  85. package/test/scheduler-host.test.ts +21 -3
  86. package/test/scheduler-relay.test.ts +84 -4
  87. package/test/scheduler.test.ts +21 -1
  88. package/test/script-schedules.test.ts +219 -0
  89. package/test/task-native.test.ts +50 -12
  90. package/test/tasks.test.ts +97 -4
  91. package/test/updates.test.mjs +77 -18
package/.dockerignore CHANGED
@@ -27,3 +27,5 @@
27
27
  !scripts/trusted-beta.mjs
28
28
  !scripts/generate-publish-caller.mjs
29
29
  !scripts/smoke-busy-reply.ts
30
+
31
+ !pnpm-workspace.yaml
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,43 @@
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
+
24
+ ## 0.1.0-beta.47
25
+
26
+ - Trigger an existing scheduled task manually with its saved instructions, model,
27
+ reasoning effort and delivery settings in a fresh session. Preserve recurring
28
+ timing and prevent overlapping schedule runs.
29
+ - Require explicit saved models for scheduled work; reject missing or mismatched
30
+ models before launch. Legacy definitions remain readable for explicit repair.
31
+ - Refresh Codex catalogs through the bound native CLI. Managed builds and upgrades
32
+ resolve latest Codex before cache lookup and supersede stale deployment pins.
33
+ Record the concrete native version; restricted correspondence uses audited
34
+ Codex 0.160.0 and fails closed on unaudited versions.
35
+ - Keep owner channels responsive while upgrades wait for active work to finish.
36
+ - Standardize dependency installs and runtime builds on pinned pnpm.
37
+
38
+ Beta limits: live fresh-host/reboot acceptance and the Daily Leverage VM smoke
39
+ remain pending. Publication does not establish an installed-agent upgrade.
40
+
3
41
  ## 0.1.0-beta.46
4
42
 
5
43
  - Allow private RC/QA candidates to graduate to their matching published beta
package/CONTRIBUTING.md CHANGED
@@ -12,7 +12,7 @@ repository. Public docs describe shipped behavior and explicit limitations.
12
12
  unnecessary behavior first, and explain why remaining code is needed. Change
13
13
  the code, user instructions and focused tests together. Authority,
14
14
  paths, credentials, cancellation and uncertain writes need negative tests.
15
- 4. Run `pnpm verify`, `npm run release:check` and `git diff --check`.
15
+ 4. Run `pnpm verify`, `pnpm run release:check` and `git diff --check`.
16
16
  Packaging/runtime changes also need the Docker checks in docs/releasing.md.
17
17
  5. Open a PR explaining the problem, resulting behavior, verification and limits.
18
18
  Include a short sanitized reproduction. State which tests were not run.
package/Dockerfile CHANGED
@@ -1,10 +1,14 @@
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
- RUN corepack enable && corepack prepare pnpm@10.30.3 --activate
7
- COPY package.json ./
6
+ ENV PNPM_HOME=/opt/pnpm COREPACK_HOME=/opt/corepack COREPACK_DEFAULT_TO_LATEST=0
7
+ ENV PATH=/opt/pnpm:$PATH
8
+ RUN corepack enable && corepack prepare pnpm@10.30.3 --activate && pnpm config set --global global-bin-dir /usr/local/bin
9
+ RUN mkdir -p /opt/pnpm/global/5
10
+ COPY docker/engine-pnpm-workspace.yaml /opt/pnpm/global/5/pnpm-workspace.yaml
11
+ COPY package.json pnpm-workspace.yaml ./
8
12
  COPY docker/pnpm-lock.yaml ./pnpm-lock.yaml
9
13
  RUN pnpm install --frozen-lockfile --ignore-scripts
10
14
  COPY . .
@@ -12,27 +16,31 @@ FROM dependencies AS test
12
16
  RUN pnpm verify
13
17
  FROM dependencies AS runtime
14
18
  # Isolated agents run this CLI in the relay; do not bind-mount the
15
- # operator's ~/.codex. Pin the reviewed CLI for repeatable release images.
16
- ARG CODEX_CLI_VERSION=0.153.4
17
- RUN npm install -g @openai/codex@${CODEX_CLI_VERSION} && command -v codex
19
+ # operator's ~/.codex. Managed builds resolve latest to a concrete cache key.
20
+ ARG CODEX_CLI_VERSION=latest
21
+ RUN pnpm add --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} && command -v codex && codex --version
18
22
  # Isolated agents run the selected CLI in the relay; host-capable agents reuse
19
23
  # the host installation instead. OpenCode carries no task-runner pin because
20
24
  # restricted messaging tasks stay on the audited Codex above.
21
25
  # Pin the reviewed CLI for repeatable release images.
22
26
  ARG OPENCODE_CLI_VERSION=1.18.32
23
- RUN npm install -g opencode-ai@${OPENCODE_CLI_VERSION} && command -v opencode && opencode --version
27
+ RUN pnpm add --global --allow-build=opencode-ai@${OPENCODE_CLI_VERSION} opencode-ai@${OPENCODE_CLI_VERSION} && command -v opencode && opencode --version
24
28
  # Isolated agents select pi from the relay menu; the binary must resolve on
25
29
  # the relay PATH just like codex/opencode above.
26
30
  # Pin the reviewed CLI for repeatable release images.
27
31
  ARG PI_CLI_VERSION=0.87.1
28
- RUN npm install -g @earendil-works/pi-coding-agent@${PI_CLI_VERSION} && command -v pi
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
29
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
30
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")'
31
39
  # Build identity for relay status (package version stays release-owned).
32
40
  ARG BUILD_TAG=""
33
41
  ARG BUILD_SHA=""
34
- RUN node -e 'require("node:fs").writeFileSync("/app/build.json",JSON.stringify({tag:process.env.BUILD_TAG||"",sha:process.env.BUILD_SHA||""})+"\n")'
35
- ENV HOME=/state/home EZ_AGENT_WORKSPACE=/workspace EZ_CONTROL_DIR=/state/control EZ_EXECUTOR_CLI=grok PATH=/app/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
42
+ RUN node -e 'require("node:fs").writeFileSync("/app/build.json",JSON.stringify({tag:process.env.BUILD_TAG||"",sha:process.env.BUILD_SHA||"",codexVersion:require("node:child_process").execFileSync("codex",["--version"],{encoding:"utf8"}).trim()})+"\n")'
43
+ ENV HOME=/state/home EZ_AGENT_WORKSPACE=/workspace EZ_CONTROL_DIR=/state/control EZ_EXECUTOR_CLI=grok PATH=/app/bin:/opt/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
36
44
  WORKDIR /workspace
37
45
  ENTRYPOINT ["/app/docker/entrypoint.sh"]
38
46
  CMD ["start"]
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ # Native CLI engines share the same install protections as project dependencies.
2
+ minimumReleaseAge: 10080
3
+ trustPolicy: no-downgrade
4
+ blockExoticSubdeps: true
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)
package/docker/smoke.mjs CHANGED
@@ -34,7 +34,7 @@ try {
34
34
  assert.equal(login.status,0,login.stderr);
35
35
  assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-application/);
36
36
  assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-message/);
37
- assert.match(login.stdout,/codex-cli 0\.153\.4/);
37
+ assert.match(login.stdout,/codex-cli \d+\.\d+\.\d+/);
38
38
  const started = run(['run','-d','--name',application,...nonroot,image,'start']);
39
39
  assert.equal(started.status,0,started.stderr);
40
40
  let ready = false;
@@ -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,13 +31,33 @@ 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)
37
47
  - [OpenCode native run events](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/run.ts)
38
48
 
39
- The catalog is local metadata, not an authentication or billing health check.
40
- Grok/Codex/OpenCode support explicit listed model/effort choices. Other installed clients
49
+ The catalog is native metadata, not an authentication or billing health check.
50
+ Codex refreshes through `codex debug models` with the whitelisted executor
51
+ environment and the execution-bound `CODEX_HOME`; discovery starts no session
52
+ or inference. Only visible model names and supported efforts are projected.
53
+ Unavailable discovery retains the native cache. The installed native version
54
+ still owns which models it advertises; Core never guesses newly released aliases.
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
41
61
  offer their own default only in this slice. OpenCode lists the installed `opencode models`
42
62
  catalog with the whitelisted executor environment, so only models the relay can actually
43
63
  run are offered; provider variants become selectable efforts (`--variant`).
@@ -46,7 +66,8 @@ Authenticated providers appear only when the agent-bound
46
66
  login); otherwise the free tier lists. `EZ_OPENCODE_PROVIDERS` optionally
47
67
  restricts the catalog to named providers (e.g. `opencode-go`); a set
48
68
  allowlist that matches nothing offers no OpenCode choice rather than falling
49
- back outside it. Refresh by opening the native client;
69
+ back outside it. Codex discovery refreshes directly; for other cache-only
70
+ catalogs, refresh by opening the native client;
50
71
  the relay does not install models, manage subscriptions or guess aliases.
51
72
 
52
73
  Setup initialization and relay startup seed one default choice per installed client.
@@ -58,8 +79,8 @@ Refresh available AIs repeats default discovery. Active/default
58
79
  presets and queued snapshots are preserved; discovery only refreshes unused
59
80
  detected entries.
60
81
  Codex uses its native `config/read` interface; Grok reads its documented user
61
- model/effort settings (or `models` for the default model). Claude reads user and
62
- 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
63
84
  opaque wrappers remain explicitly “client default”. No credentials are stored,
64
85
  no inference runs, no new dependency, and no cross-CLI session transfer.
65
86
 
@@ -109,12 +109,12 @@ after service outages; core revocation blocks sends immediately.
109
109
 
110
110
  ## Native execution and core tools
111
111
 
112
- V1 uses audited Codex CLI **0.153.4** for task work, regardless of the owner's
112
+ V1 uses audited Codex CLI **0.160.0** for task work, regardless of the owner's
113
113
  selected executor. Missing or different versions fail closed; upgrading this pin
114
114
  requires repeating the native tool inventory test. Owner work retains its normal
115
115
  executor. The task runner creates a fresh ephemeral home/session, skips user
116
116
  config, rules and ancestor project instructions, and disables shell, file/image,
117
- browser, apps, hooks, memory and agent spawning tools. A native permissions
117
+ browser, apps, hooks, memory, goals and agent spawning tools. A native permissions
118
118
  profile denies general filesystem access and tool network access. No owner
119
119
  workspace or conversation is passed to this runner. The runner uses the pinned
120
120
  CLI's bundled model catalog with task-specific tool defaults: direct MCP calls,
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use Node 22+ and pnpm 10.30.3: `pnpm install --frozen-lockfile`, then
4
4
  `pnpm verify`. Tests use temporary synthetic state; TypeScript checks source and
5
- tests. Use `npm run release:check` for package contents. See CONTRIBUTING.md for
5
+ tests. Use `pnpm run release:check` for package contents. See CONTRIBUTING.md for
6
6
  PR expectations and [release checks](releasing.md) for Docker and clean-host QA.
7
7
 
8
8
  For the cross-repository fixture, place the reviewed WhatsApp source beside this
@@ -162,6 +162,16 @@ control state, then remove only the verified orphaned `control-state.lock` and
162
162
  restart the single relay. Never remove a lock while a writer might still be live.
163
163
  Model catalog metadata is exported from the selected host CLI without credentials.
164
164
  The AI menu stays within that CLI. No automatic executor fallback is performed.
165
+ Codex discovery invokes `codex debug models` in the agent's bound `CODEX_HOME`
166
+ and projects only visible model names and supported reasoning efforts. Older
167
+ clients without that command or unavailable discovery retain the native cache.
168
+ The native CLI owns availability: a new model may require a CLI upgrade as well
169
+ as a catalog refresh. Managed builds and upgrades resolve npm Codex `latest` before cache lookup and
170
+ record the concrete version in the build/update receipt and image identity. Old
171
+ deployment Codex pins are superseded. Host installations remain operator-owned.
172
+ Restricted correspondence retains its audited native version gate and fails closed
173
+ when the normal CLI is newer than the audited version. Direct Docker builds use
174
+ `latest`; use `--no-cache` to refresh native acquisition on a reused builder.
165
175
 
166
176
  ## Secrets and plugins
167
177
 
@@ -262,6 +272,29 @@ workspace (a tenant farm), while allowing explicitly shared and additional
262
272
  workspace grants and leaving `$HOME` readable for the host CLI.
263
273
  Codex then uses `danger-full-access` so it does not apply a nested Seatbelt.
264
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
+
265
298
  ## OpenCode auth
266
299
 
267
300
  Isolated agents run the image OpenCode with `XDG_DATA_HOME` set to the
@@ -309,3 +342,9 @@ restart after repair. Existing authorized work and outbox draining remain active
309
342
  polling health remains unhealthy. Transient HTTP, rate-limit and server errors
310
343
  may reconnect. grammY still owns reconnect behavior inside its poller; ez does
311
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.