@jc_stack/ez-agents 0.1.0-beta.34 → 0.1.0-beta.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +1 -0
- package/.env.example +5 -16
- package/AGENTS.md +17 -9
- package/CHANGELOG.md +96 -0
- package/Dockerfile +20 -11
- package/README.md +10 -13
- package/THIRD_PARTY_NOTICES.md +7 -4
- package/bin/ez +97 -0
- package/bin/ezenciel-agents-plugin-broker.mjs +16 -0
- package/bin/ezenciel-agents-provision-telegram.mjs +13 -0
- package/bin/ezenciel-agents.mjs +1 -1
- package/compose.yaml +41 -4
- package/docker/entrypoint.sh +1 -1
- package/docker/healthcheck.mjs +12 -4
- package/docker/run.ts +5 -6
- package/docker/smoke.mjs +28 -6
- package/docs/application-channel.md +32 -10
- package/docs/architecture/ai-selection.md +10 -2
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/channel-backend.md +4 -3
- package/docs/docker-runtime.md +98 -11
- package/docs/local-qa.md +2 -2
- package/docs/plugin-guide.md +202 -0
- package/docs/plugins.md +48 -2
- package/docs/releasing.md +13 -0
- package/docs/responsive-channels.md +1 -1
- package/docs/scheduling.md +4 -4
- package/docs/setup.md +11 -5
- package/docs/upgrades.md +46 -23
- package/package.json +5 -5
- package/scripts/smoke-scheduler.ts +10 -3
- package/scripts/smoke.ts +14 -11
- package/src/agent-guidance.ts +6 -9
- package/src/agent-install.ts +28 -7
- package/src/ai-cli.ts +1 -1
- package/src/ai.ts +131 -5
- package/src/application-channel.ts +78 -7
- package/src/application-cli.ts +2 -2
- package/src/approval-cli.ts +14 -7
- package/src/approval.ts +27 -37
- package/src/channel-backend.ts +2 -3
- package/src/config.ts +12 -27
- package/src/control-state.ts +175 -8
- package/src/conversation-menu.ts +6 -17
- package/src/delivery-socket-client.d.mts +4 -0
- package/src/delivery-socket-client.mjs +104 -0
- package/src/delivery-socket.ts +354 -0
- package/src/execution-authority.ts +21 -5
- package/src/executor.ts +81 -24
- package/src/files.ts +38 -11
- package/src/host-executor.ts +33 -17
- package/src/inbox.ts +15 -49
- package/src/index.ts +98 -54
- package/src/install-tools.mjs +97 -22
- package/src/ledger-endpoint.d.mts +2 -0
- package/src/ledger-endpoint.mjs +17 -0
- package/src/menu.ts +8 -3
- package/src/message-history.ts +13 -18
- package/src/message.ts +35 -18
- package/src/plugin-broker.mjs +396 -0
- package/src/plugins/connection.mjs +2 -2
- package/src/plugins/manager.mjs +131 -23
- package/src/plugins/native-tasks.mjs +2 -2
- package/src/prompt-suffix.ts +12 -0
- package/src/react.ts +6 -4
- package/src/runs.ts +156 -243
- package/src/schedule-cli.ts +29 -10
- package/src/scheduled-tasks.ts +53 -10
- package/src/scheduler.ts +20 -7
- package/src/service.ts +3 -3
- package/src/setup.ts +1 -0
- package/src/software-status.ts +36 -4
- package/src/task-executor.ts +2 -2
- package/src/task-workspace.ts +4 -3
- package/src/tasks.ts +1 -1
- package/src/telegram-provisioning-cli.ts +22 -0
- package/src/telegram-provisioning.ts +214 -0
- package/src/updates/control.mjs +77 -7
- package/src/updates/runtime.mjs +28 -8
- package/src/updates/status.mjs +25 -3
- package/src/updates/supervisor.mjs +15 -16
- package/src/version.ts +20 -0
- package/src/workspace.ts +3 -9
- package/templates/failure-review.md +1 -1
- package/templates/maintainer-purpose.md +1 -1
- package/test/agent-guidance.test.ts +14 -80
- package/test/agent-install.test.ts +14 -0
- package/test/ai.test.ts +220 -1
- package/test/application-channel.test.ts +61 -13
- package/test/application-only.test.ts +14 -13
- package/test/channel-backend.test.ts +3 -7
- package/test/channel-delivery.test.ts +4 -1
- package/test/channel-owner.test.ts +6 -3
- package/test/codex-context.test.ts +2 -2
- package/test/config.test.ts +38 -43
- package/test/control-state.test.ts +83 -4
- package/test/conversation-menu.test.ts +2 -4
- package/test/delivery-transport.test.ts +72 -0
- package/test/docker-runtime.test.ts +27 -14
- package/test/engine-handoff.test.ts +7 -6
- package/test/event-sources.test.ts +2 -2
- package/test/execution-authority.test.ts +9 -9
- package/test/executor.test.ts +120 -12
- package/test/failure.test.ts +12 -5
- package/test/files.test.ts +2 -2
- package/test/helpers/ledger.ts +18 -0
- package/test/host-executor.test.ts +87 -3
- package/test/inbox.test.ts +8 -10
- package/test/install-config.test.ts +1 -0
- package/test/install-tools.test.mjs +74 -7
- package/test/intake-relay.test.ts +58 -11
- package/test/message-history.test.ts +39 -47
- package/test/native-tasks.test.ts +1 -1
- package/test/plugin-broker.test.mjs +172 -0
- package/test/plugin-manager.test.mjs +136 -2
- package/test/reaction.test.ts +4 -0
- package/test/relay.test.ts +5 -7
- package/test/repair-policy.test.ts +1 -4
- package/test/schedule-cli.test.ts +14 -9
- package/test/scheduled-tasks.test.ts +29 -6
- package/test/scheduler-host.test.ts +5 -5
- package/test/scheduler-relay.test.ts +15 -9
- package/test/security.test.ts +6 -3
- package/test/software-status.test.ts +21 -3
- package/test/telegram-provisioning.test.ts +160 -0
- package/test/updates.test.mjs +93 -13
- package/test/workspace.test.ts +4 -1
- package/bin/ezenciel-agents-watch.mjs +0 -8
- package/compose.workforce-watch.yaml +0 -33
- package/docker/recovery.ts +0 -11
- package/docs/pagerduty.md +0 -42
- package/docs/workforce-watch.md +0 -101
- package/src/pagerduty.ts +0 -109
- package/src/reply-context.ts +0 -43
- package/src/update-attention.ts +0 -18
- package/src/workforce-watch-cli.ts +0 -14
- package/src/workforce-watch.ts +0 -155
- package/templates/agent/AGENTS.md +0 -5
- package/templates/agent-guidance.md +0 -17
- package/test/pagerduty.test.ts +0 -104
- package/test/update-attention.test.ts +0 -20
- package/test/workforce-watch.test.ts +0 -180
package/.dockerignore
CHANGED
package/.env.example
CHANGED
|
@@ -32,26 +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
|
-
#
|
|
36
|
-
#
|
|
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.
|
|
51
37
|
# Isolation class from agent creation (default isolated).
|
|
52
38
|
# EZ_ISOLATION=isolated
|
|
53
39
|
# EZ_EXECUTOR_TRANSPORT=local
|
|
54
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
|
|
55
44
|
# Private application-only local container isolation (optional; default workspace-write).
|
|
56
45
|
# EZ_CODEX_SANDBOX=external
|
|
57
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
|
-
##
|
|
17
|
+
## Operating-agent instructions
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
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
|
|
106
|
-
-
|
|
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
|
|
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,10 +1,106 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.37
|
|
4
|
+
|
|
5
|
+
- List installed OpenCode models with their provider variants as selectable
|
|
6
|
+
efforts in Choose AI, chat and scheduled tasks. The relay spawns the
|
|
7
|
+
installed `opencode` CLI with the whitelisted executor environment, so the
|
|
8
|
+
menu reflects that CLI's reported catalog; variants pass through verbatim as
|
|
9
|
+
`run --variant` on every platform. Restricted messaging tasks stay on the
|
|
10
|
+
audited Codex. The relay image now ships pinned OpenCode 1.18.32 for
|
|
11
|
+
isolated agents; host-capable agents reuse the host installation.
|
|
12
|
+
- Provision authenticated OpenCode providers per agent through
|
|
13
|
+
`control/cli/opencode/auth.json`: isolated runs resolve it via an
|
|
14
|
+
agent-bound data home. Paid models may appear in the catalog; execution
|
|
15
|
+
depends on valid provider authorization. Host runs keep the installer login
|
|
16
|
+
and the free tier stays the fallback.
|
|
17
|
+
- Scope selectable OpenCode catalog entries with `EZ_OPENCODE_PROVIDERS` (e.g.
|
|
18
|
+
Go only): unlisted providers, including the free tier, leave the menu.
|
|
19
|
+
Isolated relays read it from their environment; host transports take
|
|
20
|
+
`opencodeProviders` on the agent's host binding instead. An existing
|
|
21
|
+
model-less default still follows the native client's provider choice.
|
|
22
|
+
Existing deployments need an installer-owned bootstrap for the new Compose
|
|
23
|
+
environment field before their automatic main updater accepts this release.
|
|
24
|
+
- Allow unbound plugins to run through the isolated broker when a different
|
|
25
|
+
installed plugin declares a host network binding; bound plugins still fail
|
|
26
|
+
closed on an isolated deployment.
|
|
27
|
+
- Add a Pi native executor with the exact conversation session ID. It offers
|
|
28
|
+
its native default when no provider scope is set. Owner-curated model choices
|
|
29
|
+
require Pi's own provider credentials and are filtered by scope. The relay
|
|
30
|
+
image installs pinned Pi 0.87.1.
|
|
31
|
+
|
|
32
|
+
## 0.1.0-beta.36
|
|
33
|
+
|
|
34
|
+
- Move relay run/outbox/inbox bookkeeping into relay memory behind the control
|
|
35
|
+
delivery socket. A restart drops in-flight runs and queued deliveries by
|
|
36
|
+
design; Telegram redelivery is the replay mechanism. The live socket replaces
|
|
37
|
+
the relay flock as the single-relay guard.
|
|
38
|
+
- Slim the spawn core to spawn and stream only. Callers authorize admission and
|
|
39
|
+
pass the one trigger suffix; the core no longer reads the run ledger, history
|
|
40
|
+
or workspace.
|
|
41
|
+
- Add a dedicated plugin-broker service (isolated profile) that owns Docker
|
|
42
|
+
access with no network access. Isolated plugin commands run over a Unix socket
|
|
43
|
+
with single-use capabilities, pinned revisions and per-invocation receipts.
|
|
44
|
+
- Publish installed plugin versions through the broker and report the deployed
|
|
45
|
+
build identity; show the running RC tag, isolation/transport mode and host
|
|
46
|
+
platform in `/status`.
|
|
47
|
+
- Add `ezenciel-agents-provision-telegram` to attach a Telegram channel to an
|
|
48
|
+
existing botless deployment using that deployment's installed image.
|
|
49
|
+
- Add the Telegram `/tasks` list with numbered detail, attach Telegram identity
|
|
50
|
+
to an existing web owner, and infer Telegram transport from the token instead
|
|
51
|
+
of `EZ_TELEGRAM_ENABLED`.
|
|
52
|
+
- Remove PagerDuty and Workforce Watch from core. Authorization, transport,
|
|
53
|
+
scheduling, safety gates and delivery receipts remain.
|
|
54
|
+
- Stop waking the agent for software updates. The supervisor no longer writes an
|
|
55
|
+
attention notice and the relay no longer queues a wrapper-owned
|
|
56
|
+
`software_update_attention` run, so nothing outside the managed `ez tools`
|
|
57
|
+
locator is injected into an operating agent. `ez updates` stays owner-invoked.
|
|
58
|
+
- Stop injecting a package-owned coding handbook into operating-agent
|
|
59
|
+
`AGENTS.md`. Upgrades strip the old `ez shared guidance` block. Seed only the
|
|
60
|
+
installer purpose. The managed `ez tools` locator binds the agent launcher
|
|
61
|
+
(`ez --help` and `ez tools list --details`); inventory is generated on read.
|
|
62
|
+
Do not create `inbox/`, `work/`, `SOUL.md`, or `USER.md`.
|
|
63
|
+
- Generalize compatibility bridge upgrades so `.compat.N` builds hand off to
|
|
64
|
+
their canonical release, and allow reviewed legacy homes without `docker.env`.
|
|
65
|
+
- List the agent-bound Codex catalog in `/ai` for local relays, make declared
|
|
66
|
+
provider models authoritative and migrate persisted provider selections.
|
|
67
|
+
- Source-checkout RC builds require a clean reviewed commit and an increasing
|
|
68
|
+
`.rc.N` label; the label and commit SHA are baked into the image and shown
|
|
69
|
+
first in `/status`.
|
|
70
|
+
- Existing host-capable deployments need one installer-owned migration before
|
|
71
|
+
this release: `ezenciel-agents-install migrate-ledger --deployment <dir>` adds
|
|
72
|
+
the authenticated loopback ledger endpoint. Pre-beta.36 deployments also need
|
|
73
|
+
the installer-owned bootstrap because the new `plugin-broker` service and
|
|
74
|
+
environment changes do not match the previous release's deployment gate.
|
|
75
|
+
- Fix plugin application context delivery into the command container, plugin
|
|
76
|
+
status JSON parsing under Compose warnings, and EPERM lease liveness.
|
|
77
|
+
|
|
78
|
+
## 0.1.0-beta.35
|
|
79
|
+
|
|
80
|
+
- Harden the beta.34 compatibility upgrade path, including supervisor handoff
|
|
81
|
+
after a completed main upgrade and cleanup of obsolete terminal backups while
|
|
82
|
+
retaining active, queued and rollback state.
|
|
83
|
+
|
|
84
|
+
- Publish one plugin author standard for local-only installation, exact public
|
|
85
|
+
releases, catalog registration, package boundaries, and executor verification.
|
|
86
|
+
|
|
3
87
|
## 0.1.0-beta.34
|
|
4
88
|
|
|
5
89
|
- Correct setup guidance for host-capable custom Codex providers and replace
|
|
6
90
|
stale pinned-beta installation guidance with current release records.
|
|
7
91
|
|
|
92
|
+
- Retain only the newest completed updater backup per target and reconcile older
|
|
93
|
+
terminal backups after updates and supervisor restarts, without touching
|
|
94
|
+
active, queued or rollback state.
|
|
95
|
+
|
|
96
|
+
- Let existing agents self-upgrade across the beta.34 runtime compatibility
|
|
97
|
+
migration while retaining fail-closed rejection of structural deployment
|
|
98
|
+
changes.
|
|
99
|
+
|
|
100
|
+
- Record the reviewed Library beta14 command-contract migration and hand a
|
|
101
|
+
completed main upgrade back to the retained bootstrap so the new updater code
|
|
102
|
+
is loaded without a manual supervisor restart.
|
|
103
|
+
|
|
8
104
|
- Record an isolation class at agent creation. The default is `isolated`: the
|
|
9
105
|
native CLI runs in the relay (`EZ_EXECUTOR_TRANSPORT=local`). Pass
|
|
10
106
|
`--isolation host-capable` to reuse the installer UID and host CLI login.
|
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,19 +9,28 @@ COPY . .
|
|
|
9
9
|
FROM dependencies AS test
|
|
10
10
|
RUN pnpm verify
|
|
11
11
|
FROM dependencies AS runtime
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# Isolated agents run this CLI in the relay; do not bind-mount the
|
|
13
|
+
# operator's ~/.codex. Pin the reviewed CLI for repeatable release images.
|
|
14
14
|
ARG CODEX_CLI_VERSION=0.153.4
|
|
15
15
|
RUN npm install -g @openai/codex@${CODEX_CLI_VERSION} && command -v codex
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# Isolated agents run the selected CLI in the relay; host-capable agents reuse
|
|
17
|
+
# the host installation instead. OpenCode carries no task-runner pin because
|
|
18
|
+
# restricted messaging tasks stay on the audited Codex above.
|
|
19
|
+
# Pin the reviewed CLI for repeatable release images.
|
|
20
|
+
ARG OPENCODE_CLI_VERSION=1.18.32
|
|
21
|
+
RUN npm install -g opencode-ai@${OPENCODE_CLI_VERSION} && command -v opencode && opencode --version
|
|
22
|
+
# Isolated agents select pi from the relay menu; the binary must resolve on
|
|
23
|
+
# the relay PATH just like codex/opencode above.
|
|
24
|
+
# Pin the reviewed CLI for repeatable release images.
|
|
25
|
+
ARG PI_CLI_VERSION=0.87.1
|
|
26
|
+
RUN npm install -g @earendil-works/pi-coding-agent@${PI_CLI_VERSION} && command -v pi
|
|
27
|
+
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
|
|
28
|
+
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")'
|
|
29
|
+
# Build identity for relay status (package version stays release-owned).
|
|
30
|
+
ARG BUILD_TAG=""
|
|
31
|
+
ARG BUILD_SHA=""
|
|
32
|
+
RUN node -e 'require("node:fs").writeFileSync("/app/build.json",JSON.stringify({tag:process.env.BUILD_TAG||"",sha:process.env.BUILD_SHA||""})+"\n")'
|
|
18
33
|
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
|
|
19
34
|
WORKDIR /workspace
|
|
20
35
|
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
|
21
36
|
CMD ["start"]
|
|
22
|
-
|
|
23
|
-
FROM dependencies AS workforce-watch
|
|
24
|
-
RUN chmod +x bin/ezenciel-agents-watch.mjs && mkdir /state
|
|
25
|
-
ENV EZ_WATCH_STATE_DIR=/state
|
|
26
|
-
WORKDIR /app
|
|
27
|
-
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
|
-
- **
|
|
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
|
|
@@ -169,13 +170,6 @@ services; the existing host CLI and login are shared through one generic
|
|
|
169
170
|
transport, with separate agent workspaces and sessions. See
|
|
170
171
|
[Docker setup, state and QA](docs/docker-runtime.md).
|
|
171
172
|
|
|
172
|
-
For optional PagerDuty paging of a critical Stocks outage, see
|
|
173
|
-
[PagerDuty critical-outage paging](docs/pagerduty.md).
|
|
174
|
-
|
|
175
|
-
For independent agent check-ins and concise incident context, see
|
|
176
|
-
[Workforce Watch](docs/workforce-watch.md). It runs separately from relays and
|
|
177
|
-
does not receive their executor or provider credentials.
|
|
178
|
-
|
|
179
173
|
Telegram `/status` shows the running relay and host versions plus installed
|
|
180
174
|
plugin versions. Its read-only **Scheduled tasks** control lists the paired
|
|
181
175
|
owner's active schedules in alphabetical order, with their engine/model/effort,
|
|
@@ -243,7 +237,8 @@ credentials, startup, and verified pairing; never present this as human homework
|
|
|
243
237
|
| Tool | Purpose |
|
|
244
238
|
|---|---|
|
|
245
239
|
| `ezenciel-agents-install preflight --executor <path-or-name>` | Check prerequisites, disk and executor identity hints before setup |
|
|
246
|
-
| `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) |
|
|
247
242
|
| `ezenciel-agents-install status --deployment <path>` | Distinguish runtime/configuration, pairing and Telegram reply evidence |
|
|
248
243
|
| `ezenciel-agents-create --register-cli <current-cli>` | Record the CLI that installs the initial package |
|
|
249
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 |
|
|
@@ -265,10 +260,11 @@ restart and shutdown. Host-capable agents register the small host CLI transport
|
|
|
265
260
|
|
|
266
261
|
The selected host CLI runs in this agent's persistent workspace and owns its
|
|
267
262
|
Markdown/work files. New conversations and AI changes preserve those files.
|
|
268
|
-
One compact
|
|
269
|
-
The installing LLM authors the initial brief from the owner's request
|
|
270
|
-
application context; Ez stores it without trying to infer the product
|
|
271
|
-
Installed plugin snippets and skill paths come from
|
|
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.
|
|
272
268
|
Credentials and control state stay outside the mind. Isolated agents use relay
|
|
273
269
|
mounts as the filesystem boundary. Host-capable file separation is not OS
|
|
274
270
|
isolation against a process running as the same user.
|
|
@@ -319,6 +315,7 @@ without authorization. Local registry rehearsal uses `pnpm publish:local`.
|
|
|
319
315
|
- [Security scope](SECURITY.md)
|
|
320
316
|
|
|
321
317
|
- [Contributing](CONTRIBUTING.md) and [releasing](docs/releasing.md)
|
|
318
|
+
- [Plugin author guide](docs/plugin-guide.md)
|
|
322
319
|
- [First plugin and extension contract](docs/plugins.md)
|
|
323
320
|
- [License and dependency notices](THIRD_PARTY_NOTICES.md)
|
|
324
321
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -8,7 +8,10 @@ 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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
14
|
+
requires review of its exact package build, notices and corresponding-source
|
|
15
|
+
obligations. Docker and
|
|
16
|
+
host-capable AI CLIs remain separately installed products, not covered by this
|
|
17
|
+
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
|
+
})
|
package/bin/ezenciel-agents.mjs
CHANGED
|
@@ -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'
|
|
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
|
@@ -19,20 +19,18 @@ services:
|
|
|
19
19
|
EZ_AGENT_PURPOSE_FILE: /run/agent-purpose.md
|
|
20
20
|
EZ_ISOLATION: ${EZ_ISOLATION:-}
|
|
21
21
|
EZ_EXECUTOR_TRANSPORT: ${EZ_EXECUTOR_TRANSPORT:-host}
|
|
22
|
+
EZ_PLUGIN_BROKER_SOCKET: ${EZ_PLUGIN_BROKER_SOCKET:-}
|
|
22
23
|
EZ_CHANNEL_BACKEND_URL: ${EZ_CHANNEL_BACKEND_URL:-}
|
|
23
24
|
EZ_TELEGRAM_WEB_APP: ${EZ_TELEGRAM_WEB_APP:-}
|
|
24
|
-
EZ_TELEGRAM_ENABLED: ${EZ_TELEGRAM_ENABLED:-true}
|
|
25
25
|
EZ_APPLICATION_PORT: ${EZ_APPLICATION_PORT:-}
|
|
26
26
|
EZ_APPLICATION_HOST: ${EZ_APPLICATION_HOST:-127.0.0.1}
|
|
27
27
|
EZ_AGENT_WORKSPACE: ${EZ_AGENT_WORKSPACE:?Set this agent workspace}
|
|
28
28
|
EZ_CONTROL_DIR: ${EZ_CONTROL_DIR:?Set this agent control directory}
|
|
29
29
|
EZ_EXECUTOR_CLI: ${EZ_EXECUTOR_CLI:?Set the host installation CLI}
|
|
30
30
|
EZ_CODEX_SANDBOX: ${EZ_CODEX_SANDBOX:-}
|
|
31
|
+
EZ_OPENCODE_PROVIDERS: ${EZ_OPENCODE_PROVIDERS:-}
|
|
31
32
|
EZ_CODEX_AUTO_COMPACT_TOKENS: ${EZ_CODEX_AUTO_COMPACT_TOKENS:-64000}
|
|
32
33
|
EZ_EXECUTOR_TIMEOUT_SECONDS: ${EZ_EXECUTOR_TIMEOUT_SECONDS:-300}
|
|
33
|
-
EZ_PAGERDUTY_STOCKS_HEALTH_URL: ${EZ_PAGERDUTY_STOCKS_HEALTH_URL:-}
|
|
34
|
-
EZ_PAGERDUTY_POLL_SECONDS: ${EZ_PAGERDUTY_POLL_SECONDS:-30}
|
|
35
|
-
EZ_PAGERDUTY_FAILURE_THRESHOLD: ${EZ_PAGERDUTY_FAILURE_THRESHOLD:-3}
|
|
36
34
|
secrets: [relay_env]
|
|
37
35
|
configs:
|
|
38
36
|
- source: agent_purpose
|
|
@@ -49,6 +47,45 @@ services:
|
|
|
49
47
|
timeout: 5s
|
|
50
48
|
start_period: 30s
|
|
51
49
|
retries: 3
|
|
50
|
+
plugin-broker:
|
|
51
|
+
profiles: [isolated]
|
|
52
|
+
image: ${EZ_RELAY_IMAGE:-ezenciel-agents:local}
|
|
53
|
+
build:
|
|
54
|
+
context: .
|
|
55
|
+
target: runtime
|
|
56
|
+
init: true
|
|
57
|
+
restart: unless-stopped
|
|
58
|
+
stop_grace_period: 10s
|
|
59
|
+
user: "0:0"
|
|
60
|
+
cap_drop: [ALL]
|
|
61
|
+
security_opt: [no-new-privileges:true]
|
|
62
|
+
network_mode: none
|
|
63
|
+
environment:
|
|
64
|
+
# Defaults keep Compose interpolation valid for host-capable deployments;
|
|
65
|
+
# the broker itself rejects these placeholders if its isolated profile is enabled.
|
|
66
|
+
EZ_PLUGIN_BROKER_HOME: ${EZ_TOOLS_HOME:-/nonexistent/ez-tools}
|
|
67
|
+
EZ_PLUGIN_BROKER_WORKSPACE: ${EZ_AGENT_WORKSPACE:-/nonexistent/ez-workspace}
|
|
68
|
+
EZ_PLUGIN_BROKER_CONTROL_DIR: ${EZ_CONTROL_DIR:-/nonexistent/ez-control}
|
|
69
|
+
EZ_PLUGIN_BROKER_SOCKET: ${EZ_PLUGIN_BROKER_SOCKET:-/nonexistent/ez-control/plugin-broker.sock}
|
|
70
|
+
EZ_PLUGIN_BROKER_HOST_CONFIG: ${EZ_PLUGIN_BROKER_HOST_CONFIG:-/nonexistent/host-executor.json}
|
|
71
|
+
EZ_PLUGIN_BROKER_TIMEOUT_MS: ${EZ_PLUGIN_BROKER_TIMEOUT_MS:-300000}
|
|
72
|
+
EZ_DOCKER_COMPOSE: standalone
|
|
73
|
+
HOME: /tmp
|
|
74
|
+
DOCKER_CONFIG: /tmp/docker-config
|
|
75
|
+
volumes:
|
|
76
|
+
- ${EZ_TOOLS_HOME:-/nonexistent/ez-tools}:${EZ_TOOLS_HOME:-/nonexistent/ez-tools}
|
|
77
|
+
- ${EZ_AGENT_WORKSPACE:-/nonexistent/ez-workspace}:${EZ_AGENT_WORKSPACE:-/nonexistent/ez-workspace}:ro
|
|
78
|
+
- ${EZ_CONTROL_DIR:-/nonexistent/ez-control}:${EZ_CONTROL_DIR:-/nonexistent/ez-control}
|
|
79
|
+
- ${EZ_PLUGIN_BROKER_HOST_CONFIG:-/nonexistent/host-executor.json}:${EZ_PLUGIN_BROKER_HOST_CONFIG:-/nonexistent/host-executor.json}:ro
|
|
80
|
+
- /var/run/docker.sock:/var/run/docker.sock
|
|
81
|
+
tmpfs: [/tmp]
|
|
82
|
+
entrypoint: [node, --import, /app/node_modules/tsx/dist/loader.mjs, /app/src/plugin-broker.mjs]
|
|
83
|
+
healthcheck:
|
|
84
|
+
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) }"]
|
|
85
|
+
interval: 2s
|
|
86
|
+
timeout: 5s
|
|
87
|
+
start_period: 5s
|
|
88
|
+
retries: 30
|
|
52
89
|
configs:
|
|
53
90
|
agent_purpose:
|
|
54
91
|
file: ${EZ_AGENT_PURPOSE_FILE:-./templates/agent-purpose.md}
|
package/docker/entrypoint.sh
CHANGED
|
@@ -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
|
|
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
|
package/docker/healthcheck.mjs
CHANGED
|
@@ -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(
|
|
6
|
-
catch {
|
|
7
|
-
|
|
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
|
+
}
|
package/docker/run.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { executorEnvironment, executorInvocation } from '../src/executor.js'
|
|
|
4
4
|
import { parseEnv } from 'node:util'
|
|
5
5
|
import { writeFile, rm } from 'node:fs/promises'
|
|
6
6
|
import { loadConfig } from '../src/config.js'
|
|
7
|
-
import { recoverInterruptedRuns } from './recovery.js'
|
|
8
7
|
import { createRelay } from '../src/index.js'
|
|
9
8
|
import { packageVersion } from '../src/version.js'
|
|
10
9
|
|
|
@@ -21,18 +20,18 @@ try { privateEnv = parseEnv(readFileSync(3, 'utf8')) } catch (error) {
|
|
|
21
20
|
if ((error as NodeJS.ErrnoException).code !== 'EINVAL' && (error as NodeJS.ErrnoException).code !== 'EBADF') throw error
|
|
22
21
|
} finally { try { closeSync(3) } catch {} }
|
|
23
22
|
for (const [key, value] of Object.entries(privateEnv)) {
|
|
24
|
-
if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'EZ_CHANNEL_BACKEND_TOKEN'
|
|
23
|
+
if (value !== undefined && ['TELEGRAM_BOT_TOKEN', 'GEMINI_API_KEY', 'OPENAI_API_KEY', 'EZ_CHANNEL_BACKEND_TOKEN'].includes(key)) process.env[key] = value
|
|
25
24
|
}
|
|
26
25
|
privateEnv = {}
|
|
27
26
|
const [command = 'start', ...args] = process.argv.slice(2)
|
|
28
27
|
process.argv = [process.argv[0], '', ...args]
|
|
29
|
-
if (['start', 'smoke'].includes(command)) await recoverInterruptedRuns(loadConfig().controlDir, Boolean(loadConfig().channelBackendUrl))
|
|
30
28
|
if (command === 'start') {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
29
|
+
const config = loadConfig()
|
|
30
|
+
const relay = createRelay(config)
|
|
31
|
+
const heartbeat = process.env.EZ_HEARTBEAT_FILE || '/tmp/ez-relay-heartbeat.json'
|
|
33
32
|
await rm(heartbeat, { force: true })
|
|
34
33
|
const timer = setInterval(() => {
|
|
35
|
-
if (relay.isRunning()) void writeFile(heartbeat, JSON.stringify({ at: Date.now(), polling: Boolean(relay.bot?.isRunning()), applicationOnly:
|
|
34
|
+
if (relay.isRunning()) void writeFile(heartbeat, JSON.stringify({ at: Date.now(), polling: Boolean(relay.bot?.isRunning()), applicationOnly: !relay.telegramEnabled, telegramConfigured: config.telegramBotToken !== '', version: packageVersion }), { mode: 0o600 })
|
|
36
35
|
}, 5000)
|
|
37
36
|
for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => { clearInterval(timer); void relay.stop() })
|
|
38
37
|
try { await relay.start() } finally { clearInterval(timer); await rm(heartbeat, { force: true }) }
|