@jc_stack/ez-agents 0.1.0-beta.33 → 0.1.0-beta.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +9 -16
  3. package/AGENTS.md +17 -9
  4. package/CHANGELOG.md +92 -0
  5. package/Dockerfile +11 -9
  6. package/README.md +32 -27
  7. package/THIRD_PARTY_NOTICES.md +4 -4
  8. package/bin/ez +97 -0
  9. package/bin/ezenciel-agents-plugin-broker.mjs +16 -0
  10. package/bin/ezenciel-agents-provision-telegram.mjs +13 -0
  11. package/bin/ezenciel-agents.mjs +1 -1
  12. package/compose.yaml +42 -5
  13. package/docker/entrypoint.sh +1 -1
  14. package/docker/healthcheck.mjs +12 -4
  15. package/docker/run.ts +5 -6
  16. package/docker/smoke.mjs +32 -7
  17. package/docs/application-channel.md +38 -10
  18. package/docs/architecture/ai-selection.md +3 -1
  19. package/docs/architecture/authority-boundaries.md +2 -2
  20. package/docs/channel-backend.md +4 -3
  21. package/docs/docker-runtime.md +106 -21
  22. package/docs/host-service.md +17 -6
  23. package/docs/local-qa.md +2 -2
  24. package/docs/plugin-guide.md +202 -0
  25. package/docs/plugins.md +63 -2
  26. package/docs/releasing.md +13 -0
  27. package/docs/repair.md +1 -1
  28. package/docs/responsive-channels.md +1 -1
  29. package/docs/scheduling.md +4 -4
  30. package/docs/setup.md +96 -16
  31. package/docs/standalone-cli.md +6 -5
  32. package/docs/upgrades.md +46 -23
  33. package/package.json +5 -5
  34. package/scripts/smoke-scheduler.ts +12 -4
  35. package/scripts/smoke.ts +14 -11
  36. package/src/agent-guidance.ts +6 -9
  37. package/src/agent-install.ts +44 -9
  38. package/src/ai-cli.ts +9 -7
  39. package/src/ai.ts +7 -7
  40. package/src/application-channel.ts +88 -17
  41. package/src/application-cli.ts +2 -2
  42. package/src/approval-cli.ts +14 -7
  43. package/src/approval.ts +27 -37
  44. package/src/channel-backend.ts +2 -3
  45. package/src/codex-session.ts +5 -3
  46. package/src/config.ts +18 -28
  47. package/src/control-state.ts +187 -9
  48. package/src/conversation-menu.ts +6 -17
  49. package/src/delivery-socket-client.d.mts +4 -0
  50. package/src/delivery-socket-client.mjs +104 -0
  51. package/src/delivery-socket.ts +354 -0
  52. package/src/execution-authority.ts +21 -5
  53. package/src/executor.ts +110 -31
  54. package/src/files.ts +38 -11
  55. package/src/host-executor.ts +50 -17
  56. package/src/inbox.ts +15 -49
  57. package/src/index.ts +96 -52
  58. package/src/install-config.ts +2 -2
  59. package/src/install-tools.mjs +100 -23
  60. package/src/isolation.ts +25 -0
  61. package/src/ledger-endpoint.d.mts +2 -0
  62. package/src/ledger-endpoint.mjs +17 -0
  63. package/src/menu.ts +16 -8
  64. package/src/message-history.ts +13 -18
  65. package/src/message.ts +35 -18
  66. package/src/plugin-broker.mjs +396 -0
  67. package/src/plugins/connection.mjs +2 -2
  68. package/src/plugins/manager.mjs +178 -26
  69. package/src/plugins/native-tasks.mjs +2 -2
  70. package/src/prompt-suffix.ts +12 -0
  71. package/src/react.ts +6 -4
  72. package/src/runs.ts +156 -243
  73. package/src/schedule-cli.ts +29 -10
  74. package/src/scheduled-tasks.ts +53 -10
  75. package/src/scheduler.ts +20 -7
  76. package/src/service.ts +3 -3
  77. package/src/setup.ts +11 -7
  78. package/src/software-status.ts +36 -4
  79. package/src/task-executor.ts +6 -3
  80. package/src/task-workspace.ts +4 -3
  81. package/src/tasks.ts +1 -1
  82. package/src/telegram-provisioning-cli.ts +22 -0
  83. package/src/telegram-provisioning.ts +214 -0
  84. package/src/updates/control.mjs +77 -7
  85. package/src/updates/runtime.mjs +29 -9
  86. package/src/updates/status.mjs +25 -3
  87. package/src/updates/supervisor.mjs +31 -18
  88. package/src/version.ts +20 -0
  89. package/src/workspace-confine.ts +38 -0
  90. package/src/workspace.ts +24 -20
  91. package/templates/agent-purpose.md +1 -0
  92. package/templates/failure-review.md +1 -1
  93. package/templates/maintainer-purpose.md +1 -1
  94. package/test/agent-guidance.test.ts +18 -88
  95. package/test/agent-install.test.ts +48 -0
  96. package/test/ai-cli.test.ts +18 -0
  97. package/test/ai.test.ts +84 -0
  98. package/test/application-channel.test.ts +66 -15
  99. package/test/application-controls.test.ts +56 -3
  100. package/test/application-only.test.ts +16 -13
  101. package/test/channel-backend.test.ts +3 -7
  102. package/test/channel-delivery.test.ts +4 -1
  103. package/test/channel-owner.test.ts +6 -3
  104. package/test/codex-context.test.ts +63 -3
  105. package/test/codex-session.test.ts +6 -1
  106. package/test/config.test.ts +47 -37
  107. package/test/control-state.test.ts +83 -4
  108. package/test/conversation-menu.test.ts +2 -4
  109. package/test/delivery-transport.test.ts +72 -0
  110. package/test/docker-runtime.test.ts +27 -14
  111. package/test/engine-handoff.test.ts +12 -8
  112. package/test/event-sources.test.ts +2 -2
  113. package/test/execution-authority.test.ts +9 -9
  114. package/test/executor.test.ts +47 -11
  115. package/test/failure.test.ts +18 -5
  116. package/test/files.test.ts +2 -2
  117. package/test/helpers/ledger.ts +18 -0
  118. package/test/host-executor.test.ts +80 -0
  119. package/test/inbox.test.ts +8 -10
  120. package/test/install-config.test.ts +18 -6
  121. package/test/install-tools.test.mjs +80 -7
  122. package/test/intake-relay.test.ts +58 -11
  123. package/test/message-history.test.ts +39 -47
  124. package/test/native-tasks.test.ts +1 -1
  125. package/test/plugin-broker.test.mjs +172 -0
  126. package/test/plugin-manager.test.mjs +143 -5
  127. package/test/reaction.test.ts +4 -0
  128. package/test/relay.test.ts +5 -7
  129. package/test/repair-policy.test.ts +1 -4
  130. package/test/schedule-cli.test.ts +14 -9
  131. package/test/scheduled-tasks.test.ts +29 -6
  132. package/test/scheduler-host.test.ts +5 -5
  133. package/test/scheduler-relay.test.ts +15 -9
  134. package/test/scheduler.test.ts +2 -2
  135. package/test/security.test.ts +6 -3
  136. package/test/software-status.test.ts +21 -3
  137. package/test/telegram-provisioning.test.ts +160 -0
  138. package/test/updates.test.mjs +102 -13
  139. package/test/workspace-confine.test.ts +47 -0
  140. package/test/workspace.test.ts +27 -11
  141. package/bin/ezenciel-agents-watch.mjs +0 -8
  142. package/compose.workforce-watch.yaml +0 -33
  143. package/docker/recovery.ts +0 -11
  144. package/docs/pagerduty.md +0 -42
  145. package/docs/workforce-watch.md +0 -101
  146. package/src/pagerduty.ts +0 -109
  147. package/src/reply-context.ts +0 -44
  148. package/src/update-attention.ts +0 -18
  149. package/src/workforce-watch-cli.ts +0 -14
  150. package/src/workforce-watch.ts +0 -155
  151. package/templates/agent/AGENTS.md +0 -15
  152. package/templates/agent/SOUL.md +0 -11
  153. package/templates/agent/USER.md +0 -5
  154. package/templates/agent-guidance.md +0 -114
  155. package/test/pagerduty.test.ts +0 -104
  156. package/test/update-attention.test.ts +0 -20
  157. package/test/workforce-watch.test.ts +0 -180
package/.dockerignore CHANGED
@@ -23,6 +23,7 @@
23
23
  !scripts/release-check.mjs
24
24
  !scripts/stage-qa.mjs
25
25
  !default-plugins.json
26
+ !compose.yaml
26
27
  !scripts/trusted-beta.mjs
27
28
  !scripts/generate-publish-caller.mjs
28
29
  !scripts/smoke-busy-reply.ts
package/.env.example CHANGED
@@ -32,22 +32,15 @@ EZ_EXECUTOR_CLI=agy
32
32
  # Does not grant GitHub access, merge or publication permissions.
33
33
  EZ_REPAIR_ENABLED=true
34
34
 
35
- # Optional PagerDuty incident paging for critical Stocks outages. Put the routing
36
- # key only in the private relay.env file when running Docker; place the other
37
- # settings in docker.env so Compose passes them to the relay.
38
- # PAGERDUTY_ROUTING_KEY=
39
- # EZ_PAGERDUTY_STOCKS_HEALTH_URL=http://10.97.0.1:3006/health/critical
40
- # EZ_PAGERDUTY_POLL_SECONDS=30
41
- # EZ_PAGERDUTY_FAILURE_THRESHOLD=3
42
-
43
- # Workforce Watch runs separately. Its enrollment secret and Telegram token are
44
- # monitor-only credentials; do not put them in an agent workspace.
45
- # EZ_WATCH_ENROLL_TOKEN=
46
- # EZ_WATCH_TELEGRAM_CHAT_ID=
47
-
48
- # Optional native application-only runtime: no Telegram bot or token.
49
- # Requires EZ_APPLICATION_PORT; administrator bootstrap uses the real operator ID.
50
- # EZ_TELEGRAM_ENABLED=false
35
+ # An agent without TELEGRAM_BOT_TOKEN is application-only.
36
+ # It requires EZ_APPLICATION_PORT; administrator bootstrap uses the real operator ID.
37
+ # Isolation class from agent creation (default isolated).
38
+ # EZ_ISOLATION=isolated
39
+ # EZ_EXECUTOR_TRANSPORT=local
40
+ # Host-capable: EZ_ISOLATION=host-capable and EZ_EXECUTOR_TRANSPORT=host
41
+ # Host-capable agents get a generated ledger.compose.yaml that publishes this
42
+ # loopback port so host CLIs can reach the relay ledger on macOS Docker.
43
+ # EZ_DELIVERY_TCP_PORT=23456
51
44
  # Private application-only local container isolation (optional; default workspace-write).
52
45
  # EZ_CODEX_SANDBOX=external
53
46
 
package/AGENTS.md CHANGED
@@ -14,13 +14,14 @@ Installed runtime operation uses Docker Compose. Read
14
14
  [Docker setup, state and QA](docs/docker-runtime.md). Docker owns relay/plugin services; the existing host CLI and login are shared
15
15
  through one generic transport, with separate agent workspaces and sessions.
16
16
 
17
- ## General core instructions
17
+ ## Operating-agent instructions
18
18
 
19
- When the owner refers to "general core instructions", they mean
20
- `templates/agent-guidance.md`, loaded from the installed package into CLI and
21
- desktop agent runs, including background tasks. Update that file for defaults
22
- that must reach all agents after upgrade. Workspace `AGENTS.md` files and
23
- `templates/agent/` seeds alone do not propagate changes to existing agents.
19
+ Do not inject a coding handbook into production agent minds. The package-owned
20
+ workspace locator is the managed `<!-- ez tools -->` block: the bound `ez`
21
+ launcher, `ez --help`, and `ez tools list --details`. Inventory is generated on
22
+ read; do not hardcode command recipes. Purpose lives in that agent's own
23
+ `AGENTS.md`. Coding defaults for this repository stay in this file and
24
+ CONTRIBUTING.md.
24
25
 
25
26
  ## Installing this package
26
27
 
@@ -53,6 +54,7 @@ This package will be published as an open-source, lightweight Telegram-to-CLI re
53
54
  - Plugins are ordinary CLI tools with explicit inputs, outputs and errors. Packaging a tool does not justify another LLM worker or business workflow owner.
54
55
  - **SUBTRACT is as valid as ADD.** Question the requirement; delete unnecessary behavior; simplify; shorten feedback; automate last. Name and try the subtraction option before proposing additions. Removing a wrapper or correcting existing engine instructions/tool contracts can be the complete fix.
55
56
  - Add code only for a demonstrated missing transport or tool capability. Prompts, agents, retries and lifecycle owners also count as machinery; do not replace deleted code with a scripted prompt workflow. Prefer existing engine, CLI and Docker capabilities.
57
+ - The spawn core (`src/executor.ts` `startExecutorJob`) is spawn + stream only: callers own admission/authority and pass the one trigger suffix; the core never reads the run ledger, history or the workspace.
56
58
  - `/goal do my daily routine` is literal task-prompt text for the engine. ez does not parse it, construct a native objective or own its workflow.
57
59
  - Keep changes focused. State what was removed, why anything added is necessary, and which observed outcome proves the simpler system works. Preserve authority and uncertain-delivery safeguards. Unchanged, non-actionable maintenance stops quietly; queued updates and passing tests alone do not prove an installed fix.
58
60
 
@@ -102,8 +104,8 @@ find flaws:
102
104
  If no answer identifies a material blocker, finish. Do not start another review
103
105
  round merely to seek more criticism, tests or hypothetical edge cases.
104
106
 
105
- ## 3. Crash-Safe Atomic Disk State
106
- - All persistent stores (`ControlStore`, `RunStore`, outbox queue) must be disk-backed JSON files.
107
+ ## 3. Crash-Safe Disk State
108
+ - Persistent authority state (`ControlStore` owner/pairing/sessions/presets, application bindings, schedule definitions) stays in disk-backed JSON files. Run/outbox/inbox bookkeeping is relay memory reached through the delivery socket (`src/delivery-socket.ts`): a restart drops in-flight runs and queued deliveries by design, uncertain sends report unknown rather than success, and Telegram redelivery is the replay mechanism. The live delivery socket also replaces the relay flock as the single-relay guard.
107
109
  - **Atomic write pattern:** Never write directly to a state file. Always write to a temporary file (`${target}.${process.pid}.tmp`) with mode `0o600`, then atomically `rename` it over the destination.
108
110
  - **Path sanitization:** Every ID parameter (`runId`, `outboxId`, `pairingId`) must be validated against `/^[a-zA-Z0-9_-]+$/` before being joined into paths. Never allow directory traversal (`..`).
109
111
 
@@ -119,7 +121,7 @@ round merely to seek more criticism, tests or hypothetical edge cases.
119
121
 
120
122
  ## 6. Concurrency & Workspace Invariants
121
123
  - **1 Writer Job per Workspace:** The agent's Markdown folder (`./agent/`) is its mind. Never run concurrent background processes writing to the same workspace simultaneously.
122
- - Main-conversation jobs queue sequentially in `RunStore`. Scheduled/background work uses separate task directories and native CLI sessions (up to four alongside chat). Never share a mutable task directory. 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.
124
+ - Main-conversation jobs queue sequentially in the relay's memory ledger; the delivery socket carries every cross-process enqueue/receipt, never control/ files. Scheduled/background work uses separate task directories under the control directory and native CLI sessions (up to four alongside chat). Never share a mutable task directory. 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
125
 
124
126
  ## 7. Fail-Closed Authority (Channel Access ≠ Execution)
125
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.
@@ -146,3 +148,9 @@ fix and green tests do not justify a conflicting execution or context owner.
146
148
  Existing legacy channel-backend code is a documented limitation, not a pattern
147
149
  for new integrations. Keep internal plans and private evidence outside this
148
150
  repository.
151
+
152
+ Source-checkout image builds are RCs: commit and open the PR first, then
153
+ `ezenciel-agents-install build --label X.Y.Z-beta.N.rc.M` (increasing `.rc.N`)
154
+ from the clean reviewed commit. The build refuses a dirty checkout or a package
155
+ tree that is not the checkout root; `/status` must show the RC tag for source
156
+ installs.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.36
4
+
5
+ - Move relay run/outbox/inbox bookkeeping into relay memory behind the control
6
+ delivery socket. A restart drops in-flight runs and queued deliveries by
7
+ design; Telegram redelivery is the replay mechanism. The live socket replaces
8
+ the relay flock as the single-relay guard.
9
+ - Slim the spawn core to spawn and stream only. Callers authorize admission and
10
+ pass the one trigger suffix; the core no longer reads the run ledger, history
11
+ or workspace.
12
+ - Add a dedicated plugin-broker service (isolated profile) that owns Docker
13
+ access with no network access. Isolated plugin commands run over a Unix socket
14
+ with single-use capabilities, pinned revisions and per-invocation receipts.
15
+ - Publish installed plugin versions through the broker and report the deployed
16
+ build identity; show the running RC tag, isolation/transport mode and host
17
+ platform in `/status`.
18
+ - Add `ezenciel-agents-provision-telegram` to attach a Telegram channel to an
19
+ existing botless deployment using that deployment's installed image.
20
+ - Add the Telegram `/tasks` list with numbered detail, attach Telegram identity
21
+ to an existing web owner, and infer Telegram transport from the token instead
22
+ of `EZ_TELEGRAM_ENABLED`.
23
+ - Remove PagerDuty and Workforce Watch from core. Authorization, transport,
24
+ scheduling, safety gates and delivery receipts remain.
25
+ - Stop waking the agent for software updates. The supervisor no longer writes an
26
+ attention notice and the relay no longer queues a wrapper-owned
27
+ `software_update_attention` run, so nothing outside the managed `ez tools`
28
+ locator is injected into an operating agent. `ez updates` stays owner-invoked.
29
+ - Stop injecting a package-owned coding handbook into operating-agent
30
+ `AGENTS.md`. Upgrades strip the old `ez shared guidance` block. Seed only the
31
+ installer purpose. The managed `ez tools` locator binds the agent launcher
32
+ (`ez --help` and `ez tools list --details`); inventory is generated on read.
33
+ Do not create `inbox/`, `work/`, `SOUL.md`, or `USER.md`.
34
+ - Generalize compatibility bridge upgrades so `.compat.N` builds hand off to
35
+ their canonical release, and allow reviewed legacy homes without `docker.env`.
36
+ - List the agent-bound Codex catalog in `/ai` for local relays, make declared
37
+ provider models authoritative and migrate persisted provider selections.
38
+ - Source-checkout RC builds require a clean reviewed commit and an increasing
39
+ `.rc.N` label; the label and commit SHA are baked into the image and shown
40
+ first in `/status`.
41
+ - Existing host-capable deployments need one installer-owned migration before
42
+ this release: `ezenciel-agents-install migrate-ledger --deployment <dir>` adds
43
+ the authenticated loopback ledger endpoint. Pre-beta.36 deployments also need
44
+ the installer-owned bootstrap because the new `plugin-broker` service and
45
+ environment changes do not match the previous release's deployment gate.
46
+ - Fix plugin application context delivery into the command container, plugin
47
+ status JSON parsing under Compose warnings, and EPERM lease liveness.
48
+
49
+ ## 0.1.0-beta.35
50
+
51
+ - Harden the beta.34 compatibility upgrade path, including supervisor handoff
52
+ after a completed main upgrade and cleanup of obsolete terminal backups while
53
+ retaining active, queued and rollback state.
54
+
55
+ - Publish one plugin author standard for local-only installation, exact public
56
+ releases, catalog registration, package boundaries, and executor verification.
57
+
58
+ ## 0.1.0-beta.34
59
+
60
+ - Correct setup guidance for host-capable custom Codex providers and replace
61
+ stale pinned-beta installation guidance with current release records.
62
+
63
+ - Retain only the newest completed updater backup per target and reconcile older
64
+ terminal backups after updates and supervisor restarts, without touching
65
+ active, queued or rollback state.
66
+
67
+ - Let existing agents self-upgrade across the beta.34 runtime compatibility
68
+ migration while retaining fail-closed rejection of structural deployment
69
+ changes.
70
+
71
+ - Record the reviewed Library beta14 command-contract migration and hand a
72
+ completed main upgrade back to the retained bootstrap so the new updater code
73
+ is loaded without a manual supervisor restart.
74
+
75
+ - Record an isolation class at agent creation. The default is `isolated`: the
76
+ native CLI runs in the relay (`EZ_EXECUTOR_TRANSPORT=local`). Pass
77
+ `--isolation host-capable` to reuse the installer UID and host CLI login.
78
+ Mismatched class and transport fail closed. Isolated Codex sets
79
+ `EZ_CODEX_SANDBOX=external`. Host transport is not started for isolated
80
+ agents. Unlabeled existing host transports stay host-capable.
81
+ - Install pinned Codex CLI `0.153.4` in the relay runtime image so isolated
82
+ agents resolve `codex` on PATH. Isolated jobs keep auth in
83
+ `control/cli/codex` and do not link the operator `~/.codex`.
84
+
85
+ - Confine macOS host jobs to their bound workspace and prevent cross-agent
86
+ plugin registry access.
87
+ - Support agent-scoped OpenRouter models through the native Codex engine and
88
+ start a new native session when the provider or model changes.
89
+ - Resolve native CLIs from the host PATH, expose captured presets in application
90
+ run receipts, and seed one purpose-scoped AGENTS.md for new agents.
91
+
3
92
  ## 0.1.0-beta.33
4
93
 
5
94
  - Allow compatible plugin updates to add commands to existing services, registering
@@ -21,6 +110,9 @@
21
110
  host CLI configuration needed for restricted read-only plugin commands.
22
111
  - Clarify domain-agent positioning while preserving native engine ownership.
23
112
 
113
+ - Prevent accidental cross-agent plugin access by rejecting relay-bound registry
114
+ calls outside the owning workspace, and restrict its host-folder bindings to
115
+ workspace roots explicitly granted by the host.
24
116
  - Answer native app-server requests with their current protocol shapes. Scheduled
25
117
  `codex-gui` browser work now surfaces approval requests and grants only its
26
118
  active turn session-scoped HTTPS browser-origin and Chrome app elicitations,
package/Dockerfile CHANGED
@@ -1,6 +1,6 @@
1
1
  FROM node:22.22.0-bookworm-slim@sha256:dd9d21971ec4395903fa6143c2b9267d048ae01ca6d3ea96f16cb30df6187d94 AS dependencies
2
2
  WORKDIR /app
3
- RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg && rm -rf /var/lib/apt/lists/*
3
+ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg docker.io docker-compose && rm -rf /var/lib/apt/lists/*
4
4
  RUN corepack enable && corepack prepare pnpm@10.30.3 --activate
5
5
  COPY package.json ./
6
6
  COPY docker/pnpm-lock.yaml ./pnpm-lock.yaml
@@ -9,15 +9,17 @@ COPY . .
9
9
  FROM dependencies AS test
10
10
  RUN pnpm verify
11
11
  FROM dependencies AS runtime
12
- RUN chmod +x docker/entrypoint.sh bin/ezenciel-agents* && mkdir -p /state/control /state/home /workspace && chown node:node /state/control /state/home /workspace
13
- RUN node -e 'for (const [name, target] of Object.entries(require("./package.json").bin)) require("node:fs").symlinkSync("/app/" + target, "/usr/local/bin/" + name)'
12
+ # Keep this pin aligned with TASK_CODEX_VERSION. Isolated agents run this CLI
13
+ # in the relay; do not bind-mount the operator's ~/.codex.
14
+ ARG CODEX_CLI_VERSION=0.153.4
15
+ RUN npm install -g @openai/codex@${CODEX_CLI_VERSION} && command -v codex
16
+ 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
17
+ 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")'
18
+ # Build identity for relay status (package version stays release-owned).
19
+ ARG BUILD_TAG=""
20
+ ARG BUILD_SHA=""
21
+ RUN node -e 'require("node:fs").writeFileSync("/app/build.json",JSON.stringify({tag:process.env.BUILD_TAG||"",sha:process.env.BUILD_SHA||""})+"\n")'
14
22
  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
15
23
  WORKDIR /workspace
16
24
  ENTRYPOINT ["/app/docker/entrypoint.sh"]
17
25
  CMD ["start"]
18
-
19
- FROM dependencies AS workforce-watch
20
- RUN chmod +x bin/ezenciel-agents-watch.mjs && mkdir /state
21
- ENV EZ_WATCH_STATE_DIR=/state
22
- WORKDIR /app
23
- CMD ["node", "--import", "tsx", "src/workforce-watch-cli.ts"]
package/README.md CHANGED
@@ -140,7 +140,8 @@ here:
140
140
 
141
141
  - **Install for a user:** [setup and verified onboarding](docs/setup.md), then the installer instructions below.
142
142
  - **Understand access and delegation:** [authority boundaries](docs/architecture/authority-boundaries.md) and [security scope](SECURITY.md).
143
- - **Discover or add capabilities:** [plugin contract](docs/plugins.md). Check the actual executor's native tools before adding an integration.
143
+ - **Build or share a plugin:** [plugin guide](docs/plugin-guide.md). It covers local unpublished installs, public releases, and catalog registration.
144
+ - **Discover or add capabilities:** [plugin manager contract](docs/plugins.md). Check the actual executor's native tools before adding an integration.
144
145
  - **Contribute:** [AGENTS.md](AGENTS.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
145
146
 
146
147
  Repository discovery is not an instruction to install or contact anyone. Act on
@@ -148,15 +149,19 @@ the user's request and preserve its scope.
148
149
 
149
150
  ## Package and runtime
150
151
 
151
- **Beta 0.1.0-beta.12 — for testing on a trusted machine.** Offline and Docker
152
- fixtures are verified. Live account onboarding and reboot acceptance for this
153
- release are deferred; this is not a production-readiness claim. The source branch
154
- may contain changes newer than the published beta; inspect the supplied artifact's
155
- documentation when installing a pinned release.
156
-
157
- The npm package is `@jc_stack/ez-agents` (channel `beta`). Download a pinned
158
- release with `npm pack @jc_stack/ez-agents@0.1.0-beta.12`, or get the tarball and SHA256SUMS from
159
- [GitHub prereleases](https://github.com/jdorado/ez-agents/releases/tag/v0.1.0-beta.12).
152
+ **Published beta releases are for testing on a trusted machine.** Offline and
153
+ Docker fixtures are verified for the selected release. Live account onboarding
154
+ and reboot acceptance remain beta gates; this is not a production-readiness
155
+ claim. The source branch may contain changes newer than the published beta, so
156
+ inspect the supplied artifact's documentation when installing a pinned release.
157
+
158
+ The npm package is `@jc_stack/ez-agents`. Approved beta publications use npm's
159
+ `latest` distribution tag while their versions and GitHub releases remain
160
+ prereleases. For a reproducible download, choose an exact version from
161
+ [GitHub prereleases](https://github.com/jdorado/ez-agents/releases) and substitute
162
+ it for `VERSION` in `npm pack @jc_stack/ez-agents@VERSION`.
163
+ You can also download the release's candidate tarball and verify its SHA-256
164
+ against the attached release receipt or release notes.
160
165
  GitHub remains under `jdorado`; npm uses `jc_stack`. Do not install the unrelated
161
166
  unscoped `ez-whatsapp` package.
162
167
 
@@ -165,13 +170,6 @@ services; the existing host CLI and login are shared through one generic
165
170
  transport, with separate agent workspaces and sessions. See
166
171
  [Docker setup, state and QA](docs/docker-runtime.md).
167
172
 
168
- For optional PagerDuty paging of a critical Stocks outage, see
169
- [PagerDuty critical-outage paging](docs/pagerduty.md).
170
-
171
- For independent agent check-ins and concise incident context, see
172
- [Workforce Watch](docs/workforce-watch.md). It runs separately from relays and
173
- does not receive their executor or provider credentials.
174
-
175
173
  Telegram `/status` shows the running relay and host versions plus installed
176
174
  plugin versions. Its read-only **Scheduled tasks** control lists the paired
177
175
  owner's active schedules in alphabetical order, with their engine/model/effort,
@@ -239,31 +237,37 @@ credentials, startup, and verified pairing; never present this as human homework
239
237
  | Tool | Purpose |
240
238
  |---|---|
241
239
  | `ezenciel-agents-install preflight --executor <path-or-name>` | Check prerequisites, disk and executor identity hints before setup |
242
- | `ezenciel-agents-install build` | Build once per artifact, with a private log and duplicate-build detection |
240
+ | `ezenciel-agents-install build [--label <X.Y.Z-beta.N.rc.M>]` | Build once per artifact, with a private log and duplicate-build detection; a source checkout requires the RC label and a clean reviewed commit |
241
+ | `ezenciel-agents-install migrate-ledger --deployment <path>` | Add the loopback host ledger endpoint to an existing host-capable deployment (idempotent; no restart) |
243
242
  | `ezenciel-agents-install status --deployment <path>` | Distinguish runtime/configuration, pairing and Telegram reply evidence |
244
243
  | `ezenciel-agents-create --register-cli <current-cli>` | Record the CLI that installs the initial package |
245
- | `ezenciel-agents-create --name <name> --purpose <purpose>` | Create an agent inheriting the installation CLI; token via stdin |
244
+ | `ezenciel-agents-create --name <name> --purpose <agent-brief>` | Create an agent from the installing LLM's compact brief; packaged Codex defaults isolated, other CLIs stay host-capable; token via stdin |
245
+ | `ezenciel-agents-create --name <name> --purpose <agent-brief> --isolation host-capable` | Explicitly reuse the host CLI and installer UID |
246
246
  | `ezenciel-agents-host` | Invoke the existing shared host CLI for this deployment |
247
- | `ezenciel-agents-setup configure <executor>` | Private configuration and missing starter files; preserves personal files |
248
- | `ezenciel-agents-setup configure <executor> --token-stdin` | Same, with the BotFather token supplied privately through stdin |
247
+ | `ezenciel-agents-setup configure <executor> --purpose-file <path>` | Private configuration and missing starter files; fresh workspaces require installer-scoped purpose; preserves personal files |
248
+ | `ezenciel-agents-setup configure <executor> --purpose-file <path> --token-stdin` | Same, with the BotFather token supplied privately through stdin |
249
249
  | `ezenciel-agents-setup service` | Start only the Docker relay bound by `docker.env` in the current directory; register the [host service](docs/host-service.md) separately |
250
250
  | `ezenciel-agents-owner status` | Inspect the owner and pending pairing requests |
251
251
  | `ezenciel-agents-owner approve <telegram-user-id>` | Approve the verified owner; never an arbitrary first sender |
252
252
  | `ezenciel-agents-setup status` | Inspect installed executor choices |
253
- | `ezenciel-agents-setup init` | Seed only missing workspace guidance |
253
+ | `ezenciel-agents-setup init --purpose-file <path>` | Seed missing workspace guidance; purpose is optional only when AGENTS.md already exists |
254
254
 
255
255
  Run runtime tools through `ezenciel-agents-docker run --rm relay <command>`
256
256
  as shown in the Docker guide. Keep the Docker engine available; Compose owns
257
- restart and shutdown. The installing agent registers the small host CLI transport with the native service manager.
257
+ restart and shutdown. Host-capable agents register the small host CLI transport with the native service manager. Isolated agents do not.
258
258
 
259
259
  ## Runtime and development reference
260
260
 
261
261
  The selected host CLI runs in this agent's persistent workspace and owns its
262
262
  Markdown/work files. New conversations and AI changes preserve those files.
263
- `AGENTS.md`, `SOUL.md`, and `USER.md` are seeded; `MEMORY.md` is
264
- optional. Installed plugin snippets and skill paths come from `ez tools list --details`; no tool inventory file needs maintenance. Received files go in `inbox/`, tasks and deliverables in `work/`.
265
- Credentials and control state stay outside the mind. File separation is not
266
- OS isolation against a process running as the same user.
263
+ One compact `AGENTS.md` is seeded with the installer purpose; `MEMORY.md` is
264
+ optional. The installing LLM authors the initial brief from the owner's request
265
+ and known application context; Ez stores it without trying to infer the product
266
+ itself. Installed plugin snippets and skill paths come from the bound
267
+ `ez tools list --details` locator; no tool inventory file needs maintenance.
268
+ Credentials and control state stay outside the mind. Isolated agents use relay
269
+ mounts as the filesystem boundary. Host-capable file separation is not OS
270
+ isolation against a process running as the same user.
267
271
 
268
272
  In Telegram, **Conversations** (or `/chats`) lists this agent's saved Ez
269
273
  conversations by name. Tap a name to continue its native engine session and
@@ -311,6 +315,7 @@ without authorization. Local registry rehearsal uses `pnpm publish:local`.
311
315
  - [Security scope](SECURITY.md)
312
316
 
313
317
  - [Contributing](CONTRIBUTING.md) and [releasing](docs/releasing.md)
318
+ - [Plugin author guide](docs/plugin-guide.md)
314
319
  - [First plugin and extension contract](docs/plugins.md)
315
320
  - [License and dependency notices](THIRD_PARTY_NOTICES.md)
316
321
 
@@ -8,7 +8,7 @@ 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 and Debian packages: redistributing that image requires
12
- review of its exact package build, notices and corresponding-source obligations.
13
- Docker and the selected host AI CLI are separately installed products, not covered
14
- by this repository's MIT license.
11
+ image installs ffmpeg, Debian packages, and pinned `@openai/codex` (Apache-2.0):
12
+ redistributing that image requires review of its exact package build, notices and
13
+ corresponding-source obligations. Docker and a host-capable AI CLI remain
14
+ separately installed products, not covered by this repository's MIT license.
package/bin/ez ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ import { createConnection } from 'node:net';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ const MAX_STDIN_BYTES = 1024 * 1024;
6
+ const RUN_ID = /^[A-Za-z0-9_-]{1,160}$/;
7
+ const socketPath = process.env.EZ_PLUGIN_BROKER_SOCKET?.trim();
8
+ const runId = process.env.EZ_RUN_ID?.trim();
9
+
10
+ const fail = message => {
11
+ process.stderr.write(`ez plugin broker: ${message}\n`);
12
+ process.exitCode = 1;
13
+ };
14
+
15
+ const request = (payload, signal = undefined) => new Promise((resolve, reject) => {
16
+ if (!socketPath || !socketPath.startsWith('/') || socketPath.length > 100) return reject(Error('isolated plugin broker is not configured'));
17
+ const timeoutMs = Number(process.env.EZ_PLUGIN_BROKER_CLIENT_TIMEOUT_MS ?? 600000)
18
+ const socket = createConnection(socketPath);
19
+ let buffer = '';
20
+ let settled = false;
21
+ const finish = (error, value) => {
22
+ if (settled) return;
23
+ settled = true;
24
+ clearTimeout(timer);
25
+ signal?.removeEventListener('abort', cancel);
26
+ socket.destroy();
27
+ if (error) reject(error); else resolve(value);
28
+ };
29
+ const cancel = () => {
30
+ socket.destroy();
31
+ finish(Error('plugin command cancelled'));
32
+ };
33
+ const timer = setTimeout(() => {
34
+ socket.destroy();
35
+ finish(Error('plugin broker request timed out'));
36
+ }, Number.isSafeInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : 600000);
37
+ signal?.addEventListener('abort', cancel, { once: true });
38
+ socket.on('error', error => finish(error));
39
+ socket.on('close', () => finish(Error('plugin broker closed the connection')));
40
+ socket.on('data', chunk => {
41
+ buffer += chunk.toString();
42
+ const end = buffer.indexOf('\n');
43
+ if (end < 0) return;
44
+ let value;
45
+ try { value = JSON.parse(buffer.slice(0, end)); } catch { return finish(Error('invalid plugin broker response')); }
46
+ finish(undefined, value);
47
+ });
48
+ socket.on('connect', () => socket.write(JSON.stringify(payload) + '\n'));
49
+ });
50
+
51
+ const readStdin = async () => {
52
+ if (process.stdin.isTTY) return '';
53
+ const chunks = [];
54
+ let bytes = 0;
55
+ for await (const chunk of process.stdin) {
56
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
57
+ bytes += value.length;
58
+ if (bytes > MAX_STDIN_BYTES) throw Error('plugin stdin exceeds the broker limit');
59
+ chunks.push(value);
60
+ }
61
+ return Buffer.concat(chunks).toString('utf8');
62
+ };
63
+
64
+ const invoke = async (args, stdin) => {
65
+ if (!runId || !RUN_ID.test(runId)) throw Error('EZ_RUN_ID is required for an isolated plugin command');
66
+ const alias = args.shift();
67
+ if (!alias || !/^[a-z][a-z0-9-]{0,39}$/.test(alias) || ['plugins', 'tools', 'status', 'updates', 'message', 'owner', 'approval', 'react', 'setup', 'help', 'version'].includes(alias)) throw Error('Use a declared plugin alias');
68
+ const resolved = await request({ version: 1, id: randomUUID(), operation: 'resolve', runId, alias });
69
+ if (!resolved?.ok) throw Error(resolved?.error || 'Plugin alias resolution failed');
70
+ const result = await request({ version: 1, id: randomUUID(), operation: 'invoke', runId, alias, revision: resolved.revision, capability: resolved.capability, args, ...(stdin ? { stdin } : {}) });
71
+ if (!result?.ok) throw Error(result?.error || 'Plugin invocation failed');
72
+ if (result.stdout) process.stdout.write(result.stdout);
73
+ if (result.stderr) process.stderr.write(result.stderr);
74
+ process.exitCode = Number.isInteger(result.code) ? result.code : 1;
75
+ };
76
+
77
+ const manager = async args => {
78
+ if (!runId || !RUN_ID.test(runId)) throw Error('EZ_RUN_ID is required for isolated plugin management');
79
+ const result = await request({ version: 1, id: randomUUID(), operation: 'manager', runId, args });
80
+ if (!result?.ok) throw Error(result?.error || 'Plugin broker management failed');
81
+ if (result.stdout) process.stdout.write(result.stdout);
82
+ if (result.stderr) process.stderr.write(result.stderr);
83
+ process.exitCode = Number.isInteger(result.code) ? result.code : 0;
84
+ };
85
+
86
+ const main = async () => {
87
+ const args = process.argv.slice(2);
88
+ if (!args.length || args[0] === '--help' || args[0] === '-h') {
89
+ process.stdout.write('Use `ez tools list` to discover installed plugins, then `ez <alias> ...` for a declared command.\n');
90
+ return;
91
+ }
92
+ const stdin = ['plugins', 'tools', 'updates', 'status'].includes(args[0]) ? '' : await readStdin();
93
+ if (['plugins', 'tools', 'updates', 'status'].includes(args[0])) await manager(args);
94
+ else await invoke(args, stdin);
95
+ };
96
+
97
+ main().catch(error => fail(error instanceof Error ? error.message : 'Plugin broker client failed'));
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+ const tsx = require.resolve('tsx');
10
+ const entry = join(here, '..', 'src', 'plugin-broker.mjs');
11
+ const child = spawn(process.execPath, ['--import', tsx, entry, ...process.argv.slice(2)], { stdio: 'inherit' });
12
+ child.on('error', error => { console.error(error.message); process.exitCode = 1; });
13
+ child.on('exit', (code, signal) => {
14
+ if (signal) process.kill(process.pid, signal);
15
+ process.exit(code ?? 1);
16
+ });
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url))
8
+ const require = createRequire(import.meta.url)
9
+ const child = spawn(process.execPath, ['--import', require.resolve('tsx'), join(here, '..', 'src', 'telegram-provisioning-cli.ts'), ...process.argv.slice(2)], { stdio: 'inherit' })
10
+ child.on('exit', (code, signal) => {
11
+ if (signal) process.kill(process.pid, signal)
12
+ process.exit(code ?? 1)
13
+ })
@@ -22,7 +22,7 @@ if (forwarded.length && (forwarded.length !== 1 || forwarded[0] !== 'start')) {
22
22
  process.exit(1)
23
23
  }
24
24
  if (!existsSync('/.dockerenv') && process.env.EZ_DEVELOPMENT !== '1') {
25
- const docker = spawn(join(here, 'ezenciel-agents-docker'), ['up', '-d', '--wait', 'relay'], { stdio: 'inherit' })
25
+ const docker = spawn(join(here, 'ezenciel-agents-docker'), ['up', '-d', '--wait'], { stdio: 'inherit' })
26
26
  docker.on('error', () => { console.error('Docker launcher could not start'); process.exitCode = 1 })
27
27
  docker.on('exit', code => { process.exitCode = code ?? 1 })
28
28
  } else {
package/compose.yaml CHANGED
@@ -17,10 +17,11 @@ services:
17
17
  EZ_RELAY_UID: ${EZ_RELAY_UID:-1001}
18
18
  EZ_REPAIR_ENABLED: ${EZ_REPAIR_ENABLED:-true}
19
19
  EZ_AGENT_PURPOSE_FILE: /run/agent-purpose.md
20
+ EZ_ISOLATION: ${EZ_ISOLATION:-}
20
21
  EZ_EXECUTOR_TRANSPORT: ${EZ_EXECUTOR_TRANSPORT:-host}
22
+ EZ_PLUGIN_BROKER_SOCKET: ${EZ_PLUGIN_BROKER_SOCKET:-}
21
23
  EZ_CHANNEL_BACKEND_URL: ${EZ_CHANNEL_BACKEND_URL:-}
22
24
  EZ_TELEGRAM_WEB_APP: ${EZ_TELEGRAM_WEB_APP:-}
23
- EZ_TELEGRAM_ENABLED: ${EZ_TELEGRAM_ENABLED:-true}
24
25
  EZ_APPLICATION_PORT: ${EZ_APPLICATION_PORT:-}
25
26
  EZ_APPLICATION_HOST: ${EZ_APPLICATION_HOST:-127.0.0.1}
26
27
  EZ_AGENT_WORKSPACE: ${EZ_AGENT_WORKSPACE:?Set this agent workspace}
@@ -29,9 +30,6 @@ services:
29
30
  EZ_CODEX_SANDBOX: ${EZ_CODEX_SANDBOX:-}
30
31
  EZ_CODEX_AUTO_COMPACT_TOKENS: ${EZ_CODEX_AUTO_COMPACT_TOKENS:-64000}
31
32
  EZ_EXECUTOR_TIMEOUT_SECONDS: ${EZ_EXECUTOR_TIMEOUT_SECONDS:-300}
32
- EZ_PAGERDUTY_STOCKS_HEALTH_URL: ${EZ_PAGERDUTY_STOCKS_HEALTH_URL:-}
33
- EZ_PAGERDUTY_POLL_SECONDS: ${EZ_PAGERDUTY_POLL_SECONDS:-30}
34
- EZ_PAGERDUTY_FAILURE_THRESHOLD: ${EZ_PAGERDUTY_FAILURE_THRESHOLD:-3}
35
33
  secrets: [relay_env]
36
34
  configs:
37
35
  - source: agent_purpose
@@ -48,9 +46,48 @@ services:
48
46
  timeout: 5s
49
47
  start_period: 30s
50
48
  retries: 3
49
+ plugin-broker:
50
+ profiles: [isolated]
51
+ image: ${EZ_RELAY_IMAGE:-ezenciel-agents:local}
52
+ build:
53
+ context: .
54
+ target: runtime
55
+ init: true
56
+ restart: unless-stopped
57
+ stop_grace_period: 10s
58
+ user: "0:0"
59
+ cap_drop: [ALL]
60
+ security_opt: [no-new-privileges:true]
61
+ network_mode: none
62
+ environment:
63
+ # Defaults keep Compose interpolation valid for host-capable deployments;
64
+ # the broker itself rejects these placeholders if its isolated profile is enabled.
65
+ EZ_PLUGIN_BROKER_HOME: ${EZ_TOOLS_HOME:-/nonexistent/ez-tools}
66
+ EZ_PLUGIN_BROKER_WORKSPACE: ${EZ_AGENT_WORKSPACE:-/nonexistent/ez-workspace}
67
+ EZ_PLUGIN_BROKER_CONTROL_DIR: ${EZ_CONTROL_DIR:-/nonexistent/ez-control}
68
+ EZ_PLUGIN_BROKER_SOCKET: ${EZ_PLUGIN_BROKER_SOCKET:-/nonexistent/ez-control/plugin-broker.sock}
69
+ EZ_PLUGIN_BROKER_HOST_CONFIG: ${EZ_PLUGIN_BROKER_HOST_CONFIG:-/nonexistent/host-executor.json}
70
+ EZ_PLUGIN_BROKER_TIMEOUT_MS: ${EZ_PLUGIN_BROKER_TIMEOUT_MS:-300000}
71
+ EZ_DOCKER_COMPOSE: standalone
72
+ HOME: /tmp
73
+ DOCKER_CONFIG: /tmp/docker-config
74
+ volumes:
75
+ - ${EZ_TOOLS_HOME:-/nonexistent/ez-tools}:${EZ_TOOLS_HOME:-/nonexistent/ez-tools}
76
+ - ${EZ_AGENT_WORKSPACE:-/nonexistent/ez-workspace}:${EZ_AGENT_WORKSPACE:-/nonexistent/ez-workspace}:ro
77
+ - ${EZ_CONTROL_DIR:-/nonexistent/ez-control}:${EZ_CONTROL_DIR:-/nonexistent/ez-control}
78
+ - ${EZ_PLUGIN_BROKER_HOST_CONFIG:-/nonexistent/host-executor.json}:${EZ_PLUGIN_BROKER_HOST_CONFIG:-/nonexistent/host-executor.json}:ro
79
+ - /var/run/docker.sock:/var/run/docker.sock
80
+ tmpfs: [/tmp]
81
+ entrypoint: [node, --import, /app/node_modules/tsx/dist/loader.mjs, /app/src/plugin-broker.mjs]
82
+ healthcheck:
83
+ test: [CMD, node, -e, "const fs=require('node:fs'); try { process.exit(fs.statSync(process.env.EZ_PLUGIN_BROKER_SOCKET).isSocket() ? 0 : 1) } catch { process.exit(1) }"]
84
+ interval: 2s
85
+ timeout: 5s
86
+ start_period: 5s
87
+ retries: 30
51
88
  configs:
52
89
  agent_purpose:
53
- file: ${EZ_AGENT_PURPOSE_FILE:-./templates/agent/SOUL.md}
90
+ file: ${EZ_AGENT_PURPOSE_FILE:-./templates/agent-purpose.md}
54
91
  secrets:
55
92
  relay_env:
56
93
  file: ${EZ_RELAY_ENV_FILE:?Set EZ_RELAY_ENV_FILE to the private relay env file}
@@ -28,6 +28,6 @@ fi
28
28
  ( : <&3 ) 2>/dev/null || exec 3</dev/null
29
29
  case "${1:-start}" in
30
30
  start|smoke|exec|setup)
31
- exec flock --no-fork -n -E 73 /state/control/relay.lock node --import /app/node_modules/tsx/dist/loader.mjs /app/docker/run.ts "$@" ;;
31
+ exec node --import /app/node_modules/tsx/dist/loader.mjs /app/docker/run.ts "$@" ;;
32
32
  *) exec node --import /app/node_modules/tsx/dist/loader.mjs /app/docker/run.ts "$@" ;;
33
33
  esac
@@ -1,10 +1,14 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ const heartbeatFile = process.env.EZ_HEARTBEAT_FILE || '/tmp/ez-relay-heartbeat.json';
2
3
  const relayControl = process.env.EZ_HEALTH_RELAY_CONTROL_DIR || '/state/control';
3
4
  const fail = code => { process.stderr.write(`EZ_HEALTH_${code}\n`); process.exit(1); };
4
5
  let value;
5
- try { value = JSON.parse(readFileSync(relayControl + '/heartbeat.json', 'utf8')); }
6
- catch { fail('RELAY_UNREADABLE'); }
7
- if (!value.polling && !(process.env.EZ_TELEGRAM_ENABLED === 'false' && value.applicationOnly === true)) fail('RELAY_NOT_POLLING');
6
+ try { value = JSON.parse(readFileSync(heartbeatFile, 'utf8')); }
7
+ catch {
8
+ try { value = JSON.parse(readFileSync(relayControl + '/heartbeat.json', 'utf8')); }
9
+ catch { fail('RELAY_UNREADABLE'); }
10
+ }
11
+ if (!value.polling && (value.applicationOnly !== true || value.telegramConfigured !== false)) fail('RELAY_NOT_POLLING');
8
12
  if (!Number.isFinite(value.at) || Date.now() - value.at > 20000) fail('RELAY_STALE');
9
13
 
10
14
  if (process.env.EZ_EXECUTOR_TRANSPORT === 'host') {
@@ -13,3 +17,7 @@ if (process.env.EZ_EXECUTOR_TRANSPORT === 'host') {
13
17
  catch { fail('HOST_UNREADABLE'); }
14
18
  if (!Number.isFinite(host.at) || Date.now() - host.at > 15000) fail('HOST_STALE');
15
19
  }
20
+ if (process.env.EZ_PLUGIN_BROKER_SOCKET?.trim()) {
21
+ try { if (!statSync(process.env.EZ_PLUGIN_BROKER_SOCKET).isSocket()) fail('PLUGIN_BROKER_UNREADABLE'); }
22
+ catch { fail('PLUGIN_BROKER_UNREADABLE'); }
23
+ }