@jc_stack/ez-agents 0.1.0-beta.27 → 0.1.0-beta.29
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/.env.example +9 -0
- package/AGENTS.md +25 -1
- package/CHANGELOG.md +29 -0
- package/CONTRIBUTING.md +28 -0
- package/Dockerfile +1 -0
- package/README.md +79 -8
- package/bin/ezenciel-agents-application +2 -0
- package/bin/ezenciel-agents-application.mjs +16 -0
- package/compose.yaml +8 -0
- package/docker/entrypoint.sh +20 -2
- package/docker/healthcheck.mjs +1 -1
- package/docker/run.ts +3 -3
- package/docker/smoke.mjs +41 -2
- package/docs/application-channel.md +366 -0
- package/docs/docker-runtime.md +20 -0
- package/docs/managed-applications.md +68 -0
- package/docs/plugin-catalog.md +1 -0
- package/docs/plugin-connection.md +76 -0
- package/docs/plugins.md +40 -4
- package/docs/upgrades.md +11 -1
- package/package.json +7 -2
- package/src/application-channel.ts +308 -0
- package/src/application-cli.ts +41 -0
- package/src/application-client.mjs +87 -0
- package/src/application-origin.ts +15 -0
- package/src/codex-session.ts +5 -3
- package/src/config.ts +20 -2
- package/src/control-state.ts +256 -15
- package/src/conversation-menu.ts +89 -0
- package/src/delivery-context.d.mts +5 -0
- package/src/delivery-context.mjs +25 -0
- package/src/event-sources.ts +2 -2
- package/src/execution-authority.ts +2 -0
- package/src/executor.ts +10 -4
- package/src/host-executor.ts +7 -1
- package/src/identity.ts +11 -3
- package/src/index.ts +149 -54
- package/src/menu.ts +26 -9
- package/src/message-history.ts +52 -0
- package/src/message.ts +48 -7
- package/src/owner.ts +7 -1
- package/src/plugins/connection-artifacts.mjs +31 -0
- package/src/plugins/connection.mjs +124 -0
- package/src/plugins/manager.mjs +63 -18
- package/src/plugins/native-tasks.d.mts +4 -0
- package/src/plugins/native-tasks.mjs +66 -0
- package/src/plugins/workspace-lease.d.mts +3 -0
- package/src/plugins/workspace-lease.mjs +44 -0
- package/src/runs.ts +67 -9
- package/src/schedule-cli.ts +11 -6
- package/src/scheduler.ts +17 -7
- package/src/updates/control.mjs +4 -0
- package/src/web-launcher.ts +19 -0
- package/templates/agent-guidance.md +58 -2
- package/templates/deployments.md +24 -0
- package/test/application-channel.test.ts +283 -0
- package/test/application-client.test.mjs +84 -0
- package/test/application-controls.test.ts +224 -0
- package/test/application-only.test.ts +100 -0
- package/test/channel-delivery.test.ts +63 -0
- package/test/channel-owner.test.ts +161 -0
- package/test/codex-session.test.ts +8 -5
- package/test/config.test.ts +15 -0
- package/test/connection-artifacts.test.mjs +32 -0
- package/test/conversation-menu.test.ts +67 -0
- package/test/conversations.test.ts +84 -0
- package/test/executor.test.ts +56 -0
- package/test/host-executor.test.ts +28 -0
- package/test/intake-relay.test.ts +126 -5
- package/test/message-history.test.ts +127 -0
- package/test/native-tasks.test.ts +36 -0
- package/test/plugin-connection.test.mjs +124 -0
- package/test/plugin-manager.test.mjs +34 -0
- package/test/runtime-identity.test.mjs +18 -0
- package/test/updates.test.mjs +39 -0
package/.env.example
CHANGED
|
@@ -44,3 +44,12 @@ EZ_REPAIR_ENABLED=true
|
|
|
44
44
|
# monitor-only credentials; do not put them in an agent workspace.
|
|
45
45
|
# EZ_WATCH_ENROLL_TOKEN=
|
|
46
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
|
|
51
|
+
# Private application-only local container isolation (optional; default workspace-write).
|
|
52
|
+
# EZ_CODEX_SANDBOX=external
|
|
53
|
+
|
|
54
|
+
# Optional Mini App launcher (public URL, no credentials).
|
|
55
|
+
# EZ_TELEGRAM_WEB_APP={"command":"voice","label":"Voice","url":"https://voice.example.com/"}
|
package/AGENTS.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Ez supports two independent, composable uses: an autonomous Telegram agent and
|
|
4
4
|
workspace-scoped plugins called by your existing local CLI/GUI executor.
|
|
5
|
+
The autonomous agent also supports authenticated application channels without
|
|
6
|
+
Telegram; see docs/application-channel.md. One installation owner can link several
|
|
7
|
+
channels. Channel credentials and reply destinations do not create new owners.
|
|
5
8
|
For CLI-only requests, follow [standalone setup](docs/standalone-cli.md): no Telegram
|
|
6
9
|
pairing, relay or host executor is required. The main-first Telegram onboarding
|
|
7
10
|
rules below apply only when installing the autonomous relay. When both are
|
|
@@ -53,6 +56,23 @@ This package will be published as an open-source, lightweight Telegram-to-CLI re
|
|
|
53
56
|
- `/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.
|
|
54
57
|
- 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.
|
|
55
58
|
|
|
59
|
+
## Fast KISS iterations
|
|
60
|
+
|
|
61
|
+
Deliver the smallest useful product increment and verify its main user path.
|
|
62
|
+
Once that works within the architecture and authority boundaries, complete the
|
|
63
|
+
authorized delivery instead of spending disproportionate effort on rare,
|
|
64
|
+
low-impact edge cases. Prefer fast feedback and a focused follow-up fix over
|
|
65
|
+
speculative abstractions, fallback layers or exhaustive test matrices.
|
|
66
|
+
|
|
67
|
+
Scale validation to likelihood, impact and reversibility: test the changed
|
|
68
|
+
behavior and relevant failure boundaries, run required checks, then stop when
|
|
69
|
+
they pass. Broaden testing only for a concrete unresolved risk or new failure.
|
|
70
|
+
Architecture violations, authorization/secret exposure, data loss and uncertain
|
|
71
|
+
external writes remain blockers even when rare; minor recoverable limitations
|
|
72
|
+
can be stated briefly and deferred. Reviewers distinguish those blockers from
|
|
73
|
+
optional follow-ups and do not hold a working increment for hypothetical polish.
|
|
74
|
+
Measure progress by usable outcomes and feedback, not code or test volume.
|
|
75
|
+
|
|
56
76
|
## 3. Crash-Safe Atomic Disk State
|
|
57
77
|
- All persistent stores (`ControlStore`, `RunStore`, outbox queue) must be disk-backed JSON files.
|
|
58
78
|
- **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.
|
|
@@ -100,7 +120,11 @@ Own the complete engineering/release handoff in CONTRIBUTING.md. Once ready,
|
|
|
100
120
|
proactively request only missing merge/release authority, then ship and verify;
|
|
101
121
|
do not leave the maintainer to discover ready drafts or operate the release.
|
|
102
122
|
|
|
103
|
-
|
|
123
|
+
For every core or plugin PR you author, revise or review, apply CONTRIBUTING.md's
|
|
124
|
+
architecture check and the README's engine/application boundary. A symptom fix
|
|
125
|
+
and green tests do not justify a conflicting execution or context owner. Existing
|
|
126
|
+
legacy channel-backend code is a documented limitation, not a pattern for new
|
|
127
|
+
integrations; applications submit agent turns through the native Ez execution path.
|
|
104
128
|
|
|
105
129
|
A coding-task handoff must include the pushed commit and draft PR URL, checks,
|
|
106
130
|
independent-review status and remaining QA. If PR creation is blocked, state the
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.1.0-beta.29
|
|
6
|
+
|
|
7
|
+
- Republish the beta.28 Voice connection and web-serving contract at a monotonic
|
|
8
|
+
version so installations already running private beta.28 QA builds can upgrade
|
|
9
|
+
through the standard verified public-package path. Runtime behavior is unchanged.
|
|
10
|
+
|
|
11
|
+
## 0.1.0-beta.28
|
|
12
|
+
|
|
13
|
+
- Hide empty conversation placeholders from history and update the same menu
|
|
14
|
+
when navigating Back, rather than adding another menu message.
|
|
15
|
+
- Keep the conversation list to names only; show Archive/Restore for the selected
|
|
16
|
+
conversation. Label older chats from saved owner messages instead of opaque IDs.
|
|
17
|
+
- Add named Telegram conversations with switching, renaming, archiving and
|
|
18
|
+
restoring. Preserve native engine bindings and queued work across switches.
|
|
19
|
+
- Add the authenticated application channel to the standard runtime, including
|
|
20
|
+
botless deployments, explicit owner sharing, native conversation continuity,
|
|
21
|
+
isolated non-root containers and writable-folder grants.
|
|
22
|
+
- Add owner-bound persistent plugin connections with installed-tool discovery,
|
|
23
|
+
native task access and revocation-safe Telegram delivery receipts.
|
|
24
|
+
- Add explicit loopback-only `tools serve`, live read-only owner discovery and an
|
|
25
|
+
optional authenticated private-chat Mini App launcher for plugin-owned web UIs.
|
|
26
|
+
- Fix packaged application command availability and private-plugin update checks.
|
|
27
|
+
- Document agent-owned maintenance for separately deployed applications and add
|
|
28
|
+
owner-bound, on-demand Telegram delivery-history lookup across native sessions.
|
|
29
|
+
- Clarify the native-engine, transport and plugin ownership boundaries and the
|
|
30
|
+
proportionate KISS contribution/review standard.
|
|
31
|
+
|
|
3
32
|
## 0.1.0-beta.27
|
|
4
33
|
|
|
5
34
|
- Pass literal task input and `/goal` requests to the selected engine; remove
|
package/CONTRIBUTING.md
CHANGED
|
@@ -36,6 +36,34 @@ should explain the user problem and a small acceptance example. Security reports
|
|
|
36
36
|
follow SECURITY.md. Release and new-plugin requirements: docs/releasing.md and
|
|
37
37
|
docs/plugin-contributions.md.
|
|
38
38
|
|
|
39
|
+
## Proportional scope and validation
|
|
40
|
+
|
|
41
|
+
Follow AGENTS.md's fast KISS iteration principle. Define the smallest useful
|
|
42
|
+
increment, verify the main user path and relevant failure boundaries, and run
|
|
43
|
+
required checks. Once these pass and the architecture is preserved, finish the
|
|
44
|
+
authorized delivery. Do not expand scope or repeat tests to cover hypothetical,
|
|
45
|
+
low-impact edge cases. No new tests are needed merely to mirror wording or an
|
|
46
|
+
obvious reversible edit. Broaden verification only when evidence warrants it.
|
|
47
|
+
Reviewers must distinguish material blockers from optional follow-ups; briefly
|
|
48
|
+
record real deferred limitations without making them conditions of this PR.
|
|
49
|
+
|
|
50
|
+
## Architecture check for every PR
|
|
51
|
+
|
|
52
|
+
Authors and reviewers apply the [engine and application boundaries](README.md#engine-and-application-boundaries)
|
|
53
|
+
to every proposed or revised core/plugin change. State the failing boundary,
|
|
54
|
+
what can be removed or simplified, which component owns each remaining
|
|
55
|
+
responsibility, and the focused evidence for the fix. Evaluate instructions,
|
|
56
|
+
retries and lifecycle owners as implementation complexity too.
|
|
57
|
+
|
|
58
|
+
The independent reviewer must confirm that the final diff preserves this boundary;
|
|
59
|
+
passing CI or fixing the immediate symptom is insufficient. Reject replacement
|
|
60
|
+
conversation/prompt assembly, competing agent-turn runners or queues, and domain
|
|
61
|
+
workflows in transport. Preserve necessary authorization, persistence and delivery
|
|
62
|
+
safeguards, standard controls, and the intentional engine-decided chat guidance.
|
|
63
|
+
Resolve violations before approval or merge. Report existing out-of-scope problems
|
|
64
|
+
with sanitized evidence and a next action in an existing issue when available;
|
|
65
|
+
do not disguise them as supported architecture or start a duplicate repair.
|
|
66
|
+
|
|
39
67
|
## Isolated work and review
|
|
40
68
|
|
|
41
69
|
- One coherent change, one branch, one dedicated Git worktree, one PR per
|
package/Dockerfile
CHANGED
|
@@ -10,6 +10,7 @@ FROM dependencies AS test
|
|
|
10
10
|
RUN pnpm verify
|
|
11
11
|
FROM dependencies AS runtime
|
|
12
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)'
|
|
13
14
|
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
|
|
14
15
|
WORKDIR /workspace
|
|
15
16
|
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
package/README.md
CHANGED
|
@@ -10,27 +10,70 @@ following up on an agreed task. Each assistant has its own working context and
|
|
|
10
10
|
responsibilities. The tools and permissions you configure determine what it can
|
|
11
11
|
do.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
## Engine and application boundaries
|
|
14
|
+
|
|
15
|
+
Ez is a thin wrapper and transport layer around the native agent CLI engine.
|
|
16
|
+
The engine owns inference, native sessions, context management, tool execution,
|
|
17
|
+
goals, delegation and continuation. Ez supplies channel delivery, scheduling,
|
|
18
|
+
standard runtime controls, authorization, secret isolation, session binding,
|
|
19
|
+
durable admission, cancellation and delivery receipts.
|
|
20
|
+
|
|
21
|
+
User text passes through unchanged, with only minimal additional metadata or
|
|
22
|
+
instructions needed for channel-specific elements such as attachments, quoted
|
|
23
|
+
messages and reply delivery. Ez may adapt media through transcription or speech
|
|
24
|
+
synthesis. Its deliberate, narrow workflow opinion is chat responsiveness:
|
|
25
|
+
instructions encourage the engine to move lengthy work into an Ez-scheduled task
|
|
26
|
+
or use native delegation when appropriate. The engine decides whether and how
|
|
27
|
+
to do so; Ez does not enforce a business workflow or manage native goals.
|
|
28
|
+
|
|
29
|
+
Ez must not reconstruct conversation history for automatic prompt replay,
|
|
30
|
+
assemble a conventional LLM/API conversation prompt or wrap each message in a
|
|
31
|
+
replacement system prompt. The native engine maintains its session and context.
|
|
32
|
+
Native workspace instructions, plugin discovery and explicit agent-requested
|
|
33
|
+
access to delivery receipts or task context complement that context without
|
|
34
|
+
replacing it.
|
|
35
|
+
|
|
36
|
+
Applications integrate through public or purpose-built private plugins exposing
|
|
37
|
+
documented CLI commands to the agent. Agent-initiated application actions run
|
|
38
|
+
through those commands, which enforce authentication, authorization, validation
|
|
39
|
+
and canonical persistence directly or through authoritative application services.
|
|
40
|
+
Authenticated application interfaces and separate human approval flows may
|
|
41
|
+
access those services directly.
|
|
42
|
+
|
|
43
|
+
Application interfaces submit agent turns through the same Ez execution path,
|
|
44
|
+
without separate agent runners, application-owned model-routing layers,
|
|
45
|
+
conversation engines or competing execution queues for those turns.
|
|
46
|
+
Deterministic plugin services, provider connections, domain jobs, UI transcripts
|
|
47
|
+
and delivery queues are permitted when they do not take ownership of agent
|
|
48
|
+
execution.
|
|
49
|
+
|
|
50
|
+
Preserve standard Ez commands and behavior across channels, including scheduling
|
|
51
|
+
and runtime controls; document any current capability gaps explicitly.
|
|
52
|
+
Application-specific behavior belongs in plugin instructions and CLI commands,
|
|
53
|
+
not transport branches or replacement runtime controls.
|
|
54
|
+
|
|
55
|
+
These are architecture requirements. The current application-only runtime accepts
|
|
56
|
+
foreground application turns; background scheduling with application delivery is
|
|
57
|
+
not implemented. The legacy channel-backend mode delegates execution and changes
|
|
58
|
+
standard controls, so it does not meet this integration boundary. New integrations
|
|
59
|
+
use the [native application channel](docs/application-channel.md).
|
|
17
60
|
|
|
18
61
|
## What makes an Ez assistant
|
|
19
62
|
|
|
20
63
|
| Part | What it contributes |
|
|
21
64
|
|---|---|
|
|
22
65
|
| **Workspace** | Markdown instructions, business context and working files that persist across conversations and changes of AI. |
|
|
23
|
-
| **
|
|
66
|
+
| **Native engine** | Inference, native sessions, context management and tool execution. See [AI selection](docs/architecture/ai-selection.md). |
|
|
24
67
|
| **Authority** | A verified owner and explicit limits on delegated work. Receiving a message or installing a tool does not grant permission to act. |
|
|
25
|
-
| **Plugins** |
|
|
26
|
-
| **
|
|
68
|
+
| **Plugins** | Documented CLI commands and agent instructions for application capabilities. Commands enforce authentication, authorization, validation and canonical persistence directly or through authoritative application services. |
|
|
69
|
+
| **Ez transport and runtime** | Channel delivery, scheduling and standard runtime controls around the native engine. |
|
|
27
70
|
|
|
28
71
|
The design starts with one assistant, one mind and clear responsibilities.
|
|
29
72
|
Business instructions live in the workspace; reusable capabilities live in tools
|
|
30
73
|
and plugins. The agent decides how to do the work using those instructions and
|
|
31
74
|
the capabilities available to it.
|
|
32
75
|
|
|
33
|
-
Use the
|
|
76
|
+
Use the engine's native capabilities when they meet the need. Add a standalone
|
|
34
77
|
plugin when a missing integration earns the maintenance. Ez keeps the surrounding
|
|
35
78
|
runtime small, so your files, tools and business knowledge remain useful as AI
|
|
36
79
|
clients improve.
|
|
@@ -211,6 +254,34 @@ optional. Installed plugin snippets and skill paths come from `ez tools list --d
|
|
|
211
254
|
Credentials and control state stay outside the mind. File separation is not
|
|
212
255
|
OS isolation against a process running as the same user.
|
|
213
256
|
|
|
257
|
+
In Telegram, **Conversations** (or `/chats`) lists this agent's saved Ez
|
|
258
|
+
conversations by name. Tap a name to continue its native engine session and
|
|
259
|
+
restore its AI choice. Names start with the first message; `/rename Client launch`
|
|
260
|
+
sets a custom name for the current conversation. **New conversation** keeps the
|
|
261
|
+
old one available. **Archive this conversation**, shown after selecting a name, hides a conversation without deleting its engine
|
|
262
|
+
history; **Archived conversations → select a name → Restore conversation** brings it back. Archiving the current conversation
|
|
263
|
+
leaves the next message to start a fresh one. Running and queued work stays bound
|
|
264
|
+
to its original conversation; archive does not cancel work.
|
|
265
|
+
|
|
266
|
+
Agents can inspect what was sent to the bound Telegram chat across conversations
|
|
267
|
+
with `ezenciel-agents-message history` (latest 8 deliveries), `--limit N` (1–50),
|
|
268
|
+
or `--message-id ID` for a specific message, including a chunk of a longer report.
|
|
269
|
+
This read-only command requires an active owner Telegram run and returns confirmed
|
|
270
|
+
local receipts with message IDs, delivery times and source run/session references.
|
|
271
|
+
It does not change the selected conversation or inject history into prompts.
|
|
272
|
+
Only retained receipts from the current owner binding are available; Telegram
|
|
273
|
+
edits/deletions and attachment contents are not fetched.
|
|
274
|
+
|
|
275
|
+
Existing conversations without a title show their first saved message and date when
|
|
276
|
+
available. Empty routing placeholders are omitted from history; + New conversation
|
|
277
|
+
is the create action. Back updates the current menu without creating a conversation. Use /rename for a custom name. Older
|
|
278
|
+
records without a saved model reuse a known choice for their original CLI.
|
|
279
|
+
Antigravity's latest-only resume cannot switch back to an older conversation.
|
|
280
|
+
The list covers sessions already tracked by this Ez agent; it does not import
|
|
281
|
+
unrelated GUI/CLI history. Application-backed channels keep session controls in
|
|
282
|
+
the connected application. Shared workspace files still persist across topics.
|
|
283
|
+
|
|
284
|
+
|
|
214
285
|
Telegram intake is owner-gated. The executor sends replies with the messaging
|
|
215
286
|
CLI; stdout is not a reply. A process exit code is not delivery proof.
|
|
216
287
|
Voice/media needs its additional providers and tools; defer optional setup
|
|
@@ -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', 'application-cli.ts')
|
|
11
|
+
const forwarded = process.argv.slice(2).filter((arg) => arg !== '--')
|
|
12
|
+
const child = spawn(process.execPath, ['--import', tsx, entry, ...forwarded], { stdio: 'inherit' })
|
|
13
|
+
child.on('exit', (code, signal) => {
|
|
14
|
+
if (signal) process.kill(process.pid, signal)
|
|
15
|
+
process.exit(code ?? 1)
|
|
16
|
+
})
|
package/compose.yaml
CHANGED
|
@@ -12,13 +12,21 @@ services:
|
|
|
12
12
|
# still removes every capability from the relay and its executor children.
|
|
13
13
|
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID, SETPCAP, KILL]
|
|
14
14
|
environment:
|
|
15
|
+
EZ_RUNTIME_UID: ${EZ_RUNTIME_UID:-1000}
|
|
16
|
+
EZ_RUNTIME_GID: ${EZ_RUNTIME_GID:-1000}
|
|
17
|
+
EZ_RELAY_UID: ${EZ_RELAY_UID:-1001}
|
|
15
18
|
EZ_REPAIR_ENABLED: ${EZ_REPAIR_ENABLED:-true}
|
|
16
19
|
EZ_AGENT_PURPOSE_FILE: /run/agent-purpose.md
|
|
17
20
|
EZ_EXECUTOR_TRANSPORT: ${EZ_EXECUTOR_TRANSPORT:-host}
|
|
18
21
|
EZ_CHANNEL_BACKEND_URL: ${EZ_CHANNEL_BACKEND_URL:-}
|
|
22
|
+
EZ_TELEGRAM_WEB_APP: ${EZ_TELEGRAM_WEB_APP:-}
|
|
23
|
+
EZ_TELEGRAM_ENABLED: ${EZ_TELEGRAM_ENABLED:-true}
|
|
24
|
+
EZ_APPLICATION_PORT: ${EZ_APPLICATION_PORT:-}
|
|
25
|
+
EZ_APPLICATION_HOST: ${EZ_APPLICATION_HOST:-127.0.0.1}
|
|
19
26
|
EZ_AGENT_WORKSPACE: ${EZ_AGENT_WORKSPACE:?Set this agent workspace}
|
|
20
27
|
EZ_CONTROL_DIR: ${EZ_CONTROL_DIR:?Set this agent control directory}
|
|
21
28
|
EZ_EXECUTOR_CLI: ${EZ_EXECUTOR_CLI:?Set the host installation CLI}
|
|
29
|
+
EZ_CODEX_SANDBOX: ${EZ_CODEX_SANDBOX:-}
|
|
22
30
|
EZ_CODEX_AUTO_COMPACT_TOKENS: ${EZ_CODEX_AUTO_COMPACT_TOKENS:-64000}
|
|
23
31
|
EZ_EXECUTOR_TIMEOUT_SECONDS: ${EZ_EXECUTOR_TIMEOUT_SECONDS:-300}
|
|
24
32
|
EZ_PAGERDUTY_STOCKS_HEALTH_URL: ${EZ_PAGERDUTY_STOCKS_HEALTH_URL:-}
|
package/docker/entrypoint.sh
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
#!/bin/sh -p
|
|
2
2
|
set -eu
|
|
3
3
|
umask 077
|
|
4
|
+
runtime_uid=${EZ_RUNTIME_UID:-1000}
|
|
5
|
+
runtime_gid=${EZ_RUNTIME_GID:-1000}
|
|
6
|
+
relay_uid=${EZ_RELAY_UID:-1001}
|
|
7
|
+
for runtime_id in "$runtime_uid" "$runtime_gid" "$relay_uid"; do
|
|
8
|
+
case "$runtime_id" in
|
|
9
|
+
''|*[!0-9]*|0*) echo 'Runtime UID/GID values must be positive decimal IDs' >&2; exit 64 ;;
|
|
10
|
+
esac
|
|
11
|
+
if [ "${#runtime_id}" -gt 10 ] || [ "$runtime_id" -gt 2147483647 ]; then
|
|
12
|
+
echo 'Runtime UID/GID values must not exceed 2147483647' >&2; exit 64
|
|
13
|
+
fi
|
|
14
|
+
done
|
|
15
|
+
if [ "$runtime_uid" = "$relay_uid" ]; then
|
|
16
|
+
echo 'EZ_RELAY_UID must differ from EZ_RUNTIME_UID to isolate relay secrets' >&2
|
|
17
|
+
exit 64
|
|
18
|
+
fi
|
|
4
19
|
if [ "$(id -u)" = 0 ]; then
|
|
5
20
|
mkdir -p /state/control /state/home /workspace /run/secrets
|
|
6
21
|
chmod 700 /run/secrets
|
|
7
|
-
chown
|
|
22
|
+
chown "$runtime_uid:$runtime_gid" /state/control /state/home /workspace
|
|
8
23
|
if [ -f /run/secrets/relay_env ]; then exec 3</run/secrets/relay_env; else exec 3</dev/null; fi
|
|
9
|
-
exec setpriv --ruid=
|
|
24
|
+
exec setpriv --ruid="$relay_uid" --euid="$runtime_uid" --regid="$runtime_gid" --clear-groups --bounding-set=-all --no-new-privs /app/docker/entrypoint.sh "$@"
|
|
10
25
|
fi
|
|
26
|
+
# Reserve the private environment descriptor before Node can use it for libuv.
|
|
27
|
+
# Root startup already opened it; preserve that inherited secret descriptor.
|
|
28
|
+
( : <&3 ) 2>/dev/null || exec 3</dev/null
|
|
11
29
|
case "${1:-start}" in
|
|
12
30
|
start|smoke|exec|setup)
|
|
13
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 "$@" ;;
|
package/docker/healthcheck.mjs
CHANGED
|
@@ -4,7 +4,7 @@ const fail = code => { process.stderr.write(`EZ_HEALTH_${code}\n`); process.exit
|
|
|
4
4
|
let value;
|
|
5
5
|
try { value = JSON.parse(readFileSync(relayControl + '/heartbeat.json', 'utf8')); }
|
|
6
6
|
catch { fail('RELAY_UNREADABLE'); }
|
|
7
|
-
if (!value.polling) fail('RELAY_NOT_POLLING');
|
|
7
|
+
if (!value.polling && !(process.env.EZ_TELEGRAM_ENABLED === 'false' && value.applicationOnly === true)) fail('RELAY_NOT_POLLING');
|
|
8
8
|
if (!Number.isFinite(value.at) || Date.now() - value.at > 20000) fail('RELAY_STALE');
|
|
9
9
|
|
|
10
10
|
if (process.env.EZ_EXECUTOR_TRANSPORT === 'host') {
|
package/docker/run.ts
CHANGED
|
@@ -32,7 +32,7 @@ if (command === 'start') {
|
|
|
32
32
|
const heartbeat = '/state/control/heartbeat.json'
|
|
33
33
|
await rm(heartbeat, { force: true })
|
|
34
34
|
const timer = setInterval(() => {
|
|
35
|
-
if (relay.
|
|
35
|
+
if (relay.isRunning()) void writeFile(heartbeat, JSON.stringify({ at: Date.now(), polling: Boolean(relay.bot?.isRunning()), applicationOnly: process.env.EZ_TELEGRAM_ENABLED === 'false', version: packageVersion }), { mode: 0o600 })
|
|
36
36
|
}, 5000)
|
|
37
37
|
for (const signal of ['SIGINT', 'SIGTERM'] as const) process.once(signal, () => { clearInterval(timer); void relay.stop() })
|
|
38
38
|
try { await relay.start() } finally { clearInterval(timer); await rm(heartbeat, { force: true }) }
|
|
@@ -47,6 +47,6 @@ if (command === 'start') {
|
|
|
47
47
|
child.on('exit', (code) => { process.exitCode = code ?? 1 })
|
|
48
48
|
} else if (command === 'setup') {
|
|
49
49
|
await (await import('../src/setup.js')).runCli()
|
|
50
|
-
} else if (['owner', 'source', 'message', 'react'].includes(command)) {
|
|
51
|
-
await import(`../src/${command === 'source' ? 'source-cli' : command}.js`)
|
|
50
|
+
} else if (['owner', 'source', 'application', 'message', 'react'].includes(command)) {
|
|
51
|
+
await import(`../src/${command === 'source' ? 'source-cli' : command === 'application' ? 'application-cli' : command}.js`)
|
|
52
52
|
} else throw new Error('Use start, smoke, exec, setup, owner, source, message or react')
|
package/docker/smoke.mjs
CHANGED
|
@@ -7,10 +7,37 @@ const image = process.env.EZ_RELAY_IMAGE || 'ezenciel-agents:local';
|
|
|
7
7
|
const dir = mkdtempSync(join(tmpdir(), 'ez-docker-qa-'));
|
|
8
8
|
const holder = `ez-main-lock-qa-${process.pid}`;
|
|
9
9
|
const volume = `${holder}-control`;
|
|
10
|
+
const application = `${holder}-application`;
|
|
10
11
|
const marker = 'qa-private-secret-never-in-executor';
|
|
11
12
|
writeFileSync(join(dir, 'relay.env'), `TELEGRAM_BOT_TOKEN=${marker}\n`, { mode: 0o600 });
|
|
13
|
+
writeFileSync(join(dir, 'node'), `#!/bin/sh\nIFS= read -r value <&3\n[ "$value" = "TELEGRAM_BOT_TOKEN=${marker}" ] || exit 91\nexec /usr/local/bin/node "$@"\n`, { mode: 0o555 });
|
|
12
14
|
const run = (args) => spawnSync('docker', args, { encoding: 'utf8', timeout: 60000 });
|
|
13
15
|
try {
|
|
16
|
+
const inherited = run(['run','--rm','--mount',`type=bind,src=${join(dir,'relay.env')},dst=/run/secrets/relay_env,readonly`,
|
|
17
|
+
'--mount',`type=bind,src=${join(dir,'node')},dst=/qa/node,readonly`,'-e','PATH=/qa:/usr/local/bin:/usr/bin:/bin',image,'application','--help']);
|
|
18
|
+
assert.equal(inherited.status,0,inherited.stderr);
|
|
19
|
+
const nonroot = ['--user','20000:20000','--read-only','--cap-drop','ALL','--security-opt','no-new-privileges',
|
|
20
|
+
'--tmpfs','/tmp:mode=1777','--tmpfs','/state/control:uid=20000,gid=20000,mode=700',
|
|
21
|
+
'--tmpfs','/state/home:uid=20000,gid=20000,mode=700','--tmpfs','/workspace:uid=20000,gid=20000,mode=700',
|
|
22
|
+
'-e','EZ_TELEGRAM_ENABLED=false','-e','EZ_APPLICATION_PORT=8110','-e','EZ_EXECUTOR_TRANSPORT=local','-e','EZ_EXECUTOR_CLI=codex'];
|
|
23
|
+
const help = run(['run','--rm',...nonroot,image,'application','--help']);
|
|
24
|
+
assert.equal(help.status,0,help.stderr);
|
|
25
|
+
assert.match(help.stdout,/ezenciel-agents-application/);
|
|
26
|
+
const login = run(['run','--rm',...nonroot,'--entrypoint','/bin/bash',image,'-lc',
|
|
27
|
+
'command -v ezenciel-agents-application && command -v ezenciel-agents-message && ezenciel-agents-message --help']);
|
|
28
|
+
assert.equal(login.status,0,login.stderr);
|
|
29
|
+
assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-application/);
|
|
30
|
+
assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-message/);
|
|
31
|
+
const started = run(['run','-d','--name',application,...nonroot,image,'start']);
|
|
32
|
+
assert.equal(started.status,0,started.stderr);
|
|
33
|
+
let ready = false;
|
|
34
|
+
for (let n=0;n<60;n++) {
|
|
35
|
+
if (run(['exec',application,'node','/app/docker/healthcheck.mjs']).status===0) { ready=true; break; }
|
|
36
|
+
await new Promise(resolve=>setTimeout(resolve,200));
|
|
37
|
+
}
|
|
38
|
+
assert.ok(ready,run(['logs',application]).stderr);
|
|
39
|
+
assert.equal(run(['stop','--time','10',application]).status,0);
|
|
40
|
+
assert.equal(run(['inspect','--format','{{.State.ExitCode}}',application]).stdout.trim(),'0');
|
|
14
41
|
const probe = `
|
|
15
42
|
const fs = require('fs'), assert = require('assert/strict');
|
|
16
43
|
assert.equal(process.getuid(), 1000);
|
|
@@ -28,6 +55,18 @@ try {
|
|
|
28
55
|
const result = run(['run', '--rm', '--mount', `type=bind,src=${join(dir,'relay.env')},dst=/run/secrets/relay_env,readonly`, image, 'exec', 'node', '-e', probe, literal]);
|
|
29
56
|
assert.equal(result.status, 0, result.stderr);
|
|
30
57
|
assert.deepEqual(JSON.parse(result.stdout).argv, [literal]);
|
|
58
|
+
const customProbe = probe.replace('process.getuid(), 1000', 'process.getuid(), 20001') + `
|
|
59
|
+
assert.equal(process.geteuid(),20001);
|
|
60
|
+
assert.equal(process.getgid(),20002);
|
|
61
|
+
assert.equal(process.getegid(),20002);
|
|
62
|
+
for (const directory of ['/state/control','/state/home','/workspace']) {
|
|
63
|
+
const info=fs.statSync(directory); assert.equal(info.uid,20001); assert.equal(info.gid,20002);
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const custom = run(['run','--rm','-e','EZ_RUNTIME_UID=20001','-e','EZ_RUNTIME_GID=20002','-e','EZ_RELAY_UID=20003',
|
|
67
|
+
'--mount',`type=bind,src=${join(dir,'relay.env')},dst=/run/secrets/relay_env,readonly`,image,'exec','node','-e',customProbe]);
|
|
68
|
+
assert.equal(custom.status,0,custom.stderr);
|
|
69
|
+
assert.equal(JSON.parse(custom.stdout).uid,20001);
|
|
31
70
|
const failed = run(['run','--rm',image,'exec','node','-e','process.exit(23)']);
|
|
32
71
|
assert.equal(failed.status, 23, failed.stderr);
|
|
33
72
|
const held = run(['run','-d','--name',holder,'-v',`${volume}:/state/control`,image,'exec','node','-e',"require('fs').writeFileSync('/state/control/ready','yes');setInterval(()=>{},1000)"]);
|
|
@@ -43,5 +82,5 @@ try {
|
|
|
43
82
|
assert.equal(run(['kill',holder]).status,0);
|
|
44
83
|
const recovered=run(['run','--rm','-v',`${volume}:/state/control`,image,'exec','node','-e','process.exit(0)']);
|
|
45
84
|
assert.equal(recovered.status,0,recovered.stderr);
|
|
46
|
-
console.log('Docker smoke passed: non-root executor, private secret
|
|
47
|
-
} finally { run(['rm','-f',holder]); run(['volume','rm',volume]); rmSync(dir, {recursive:true, force:true}); }
|
|
85
|
+
console.log('Docker smoke passed: direct non-root application help/start/health/stop, inherited private descriptor, non-root executor, private secret isolation, literal argv, exit code, no Docker socket, duplicate writer rejection and crash lock release.');
|
|
86
|
+
} finally { run(['rm','-f',holder,application]); run(['volume','rm',volume]); rmSync(dir, {recursive:true, force:true}); }
|