@jc_stack/ez-agents 0.1.0-beta.45 → 0.1.0-beta.47
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 +2 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +1 -1
- package/Dockerfile +13 -9
- package/docker/engine-pnpm-workspace.yaml +4 -0
- package/docker/smoke.mjs +1 -1
- package/docs/application-channel.md +3 -0
- package/docs/architecture/ai-selection.md +8 -2
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/development-and-testing.md +1 -1
- package/docs/docker-runtime.md +10 -0
- package/docs/plugin-guide.md +1 -1
- package/docs/plugins.md +6 -1
- package/docs/releasing.md +1 -1
- package/docs/scheduling.md +17 -3
- package/docs/upgrades.md +7 -1
- package/package.json +4 -3
- package/pnpm-workspace.yaml +7 -0
- package/scripts/smoke-scheduler.ts +4 -1
- package/src/ai.ts +20 -2
- package/src/application-channel.ts +3 -1
- package/src/delivery-socket.ts +15 -1
- package/src/host-executor.ts +5 -0
- package/src/index.ts +3 -1
- package/src/install-tools.mjs +11 -5
- package/src/model-policy.ts +4 -0
- package/src/plugins/manager.mjs +7 -1
- package/src/runs.ts +4 -1
- package/src/schedule-cli.ts +12 -3
- package/src/scheduler.ts +28 -3
- package/src/task-executor.ts +2 -2
- package/src/updates/control.mjs +2 -1
- package/src/updates/runtime.mjs +15 -6
- package/src/updates/supervisor.mjs +23 -13
- package/test/ai.test.ts +45 -5
- package/test/application-controls.test.ts +3 -0
- package/test/channel-owner.test.ts +2 -2
- package/test/desktop-bridge.test.ts +2 -1
- package/test/failure.test.ts +4 -4
- package/test/host-executor.test.ts +4 -4
- package/test/install-tools.test.mjs +6 -5
- package/test/native-tasks.test.ts +1 -1
- package/test/plugin-manager.test.mjs +15 -0
- package/test/schedule-cli.test.ts +47 -3
- package/test/scheduled-tasks.test.ts +2 -2
- package/test/scheduler-host.test.ts +14 -3
- package/test/scheduler-relay.test.ts +47 -2
- package/test/scheduler.test.ts +21 -1
- package/test/tasks.test.ts +1 -1
- package/test/updates.test.mjs +68 -8
package/.dockerignore
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.47
|
|
4
|
+
|
|
5
|
+
- Trigger an existing scheduled task manually with its saved instructions, model,
|
|
6
|
+
reasoning effort and delivery settings in a fresh session. Preserve recurring
|
|
7
|
+
timing and prevent overlapping schedule runs.
|
|
8
|
+
- Require explicit saved models for scheduled work; reject missing or mismatched
|
|
9
|
+
models before launch. Legacy definitions remain readable for explicit repair.
|
|
10
|
+
- Refresh Codex catalogs through the bound native CLI. Managed builds and upgrades
|
|
11
|
+
resolve latest Codex before cache lookup and supersede stale deployment pins.
|
|
12
|
+
Record the concrete native version; restricted correspondence uses audited
|
|
13
|
+
Codex 0.160.0 and fails closed on unaudited versions.
|
|
14
|
+
- Keep owner channels responsive while upgrades wait for active work to finish.
|
|
15
|
+
- Standardize dependency installs and runtime builds on pinned pnpm.
|
|
16
|
+
|
|
17
|
+
Beta limits: live fresh-host/reboot acceptance and the Daily Leverage VM smoke
|
|
18
|
+
remain pending. Publication does not establish an installed-agent upgrade.
|
|
19
|
+
|
|
20
|
+
## 0.1.0-beta.46
|
|
21
|
+
|
|
22
|
+
- Allow private RC/QA candidates to graduate to their matching published beta
|
|
23
|
+
through the existing updater, without permitting a move to an older beta.
|
|
24
|
+
|
|
25
|
+
- Refresh the isolated broker after plugin upgrade or rollback so status reports
|
|
26
|
+
the resulting installed versions rather than its startup inventory. Preserve
|
|
27
|
+
the previous registry snapshot independently during plugin activation.
|
|
28
|
+
Preserve stopped plugins as stopped when Docker retains a created container.
|
|
29
|
+
|
|
30
|
+
- Include the existing Telegram runtime/version summary in authorized private
|
|
31
|
+
channel controls so channel status can show the running relay and host versions.
|
|
32
|
+
|
|
3
33
|
## 0.1.0-beta.45
|
|
4
34
|
|
|
5
35
|
- Keep owner chat responsive while a scheduled native turn runs. Foreground
|
package/CONTRIBUTING.md
CHANGED
|
@@ -12,7 +12,7 @@ repository. Public docs describe shipped behavior and explicit limitations.
|
|
|
12
12
|
unnecessary behavior first, and explain why remaining code is needed. Change
|
|
13
13
|
the code, user instructions and focused tests together. Authority,
|
|
14
14
|
paths, credentials, cancellation and uncertain writes need negative tests.
|
|
15
|
-
4. Run `pnpm verify`, `
|
|
15
|
+
4. Run `pnpm verify`, `pnpm run release:check` and `git diff --check`.
|
|
16
16
|
Packaging/runtime changes also need the Docker checks in docs/releasing.md.
|
|
17
17
|
5. Open a PR explaining the problem, resulting behavior, verification and limits.
|
|
18
18
|
Include a short sanitized reproduction. State which tests were not run.
|
package/Dockerfile
CHANGED
|
@@ -3,8 +3,12 @@ FROM node:22.22.0-bookworm-slim@sha256:dd9d21971ec4395903fa6143c2b9267d048ae01ca
|
|
|
3
3
|
WORKDIR /app
|
|
4
4
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates git util-linux ffmpeg docker.io && rm -rf /var/lib/apt/lists/*
|
|
5
5
|
COPY --from=compose-bin /docker-compose /usr/local/bin/docker-compose
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
ENV PNPM_HOME=/opt/pnpm COREPACK_HOME=/opt/corepack COREPACK_DEFAULT_TO_LATEST=0
|
|
7
|
+
ENV PATH=/opt/pnpm:$PATH
|
|
8
|
+
RUN corepack enable && corepack prepare pnpm@10.30.3 --activate && pnpm config set --global global-bin-dir /usr/local/bin
|
|
9
|
+
RUN mkdir -p /opt/pnpm/global/5
|
|
10
|
+
COPY docker/engine-pnpm-workspace.yaml /opt/pnpm/global/5/pnpm-workspace.yaml
|
|
11
|
+
COPY package.json pnpm-workspace.yaml ./
|
|
8
12
|
COPY docker/pnpm-lock.yaml ./pnpm-lock.yaml
|
|
9
13
|
RUN pnpm install --frozen-lockfile --ignore-scripts
|
|
10
14
|
COPY . .
|
|
@@ -12,27 +16,27 @@ FROM dependencies AS test
|
|
|
12
16
|
RUN pnpm verify
|
|
13
17
|
FROM dependencies AS runtime
|
|
14
18
|
# Isolated agents run this CLI in the relay; do not bind-mount the
|
|
15
|
-
# operator's ~/.codex.
|
|
16
|
-
ARG CODEX_CLI_VERSION=
|
|
17
|
-
RUN
|
|
19
|
+
# operator's ~/.codex. Managed builds resolve latest to a concrete cache key.
|
|
20
|
+
ARG CODEX_CLI_VERSION=latest
|
|
21
|
+
RUN pnpm add --global --ignore-scripts @openai/codex@${CODEX_CLI_VERSION} && command -v codex && codex --version
|
|
18
22
|
# Isolated agents run the selected CLI in the relay; host-capable agents reuse
|
|
19
23
|
# the host installation instead. OpenCode carries no task-runner pin because
|
|
20
24
|
# restricted messaging tasks stay on the audited Codex above.
|
|
21
25
|
# Pin the reviewed CLI for repeatable release images.
|
|
22
26
|
ARG OPENCODE_CLI_VERSION=1.18.32
|
|
23
|
-
RUN
|
|
27
|
+
RUN pnpm add --global --allow-build=opencode-ai@${OPENCODE_CLI_VERSION} opencode-ai@${OPENCODE_CLI_VERSION} && command -v opencode && opencode --version
|
|
24
28
|
# Isolated agents select pi from the relay menu; the binary must resolve on
|
|
25
29
|
# the relay PATH just like codex/opencode above.
|
|
26
30
|
# Pin the reviewed CLI for repeatable release images.
|
|
27
31
|
ARG PI_CLI_VERSION=0.87.1
|
|
28
|
-
RUN
|
|
32
|
+
RUN pnpm add --global --ignore-scripts @earendil-works/pi-coding-agent@${PI_CLI_VERSION} && command -v pi
|
|
29
33
|
RUN chmod +x docker/entrypoint.sh bin/ez bin/ezenciel-agents* && mkdir -p /state/control /state/home /workspace && chown node:node /state/control /state/home /workspace
|
|
30
34
|
RUN node -e 'for (const [name, target] of Object.entries(require("./package.json").bin)) require("node:fs").symlinkSync("/app/" + target, "/usr/local/bin/" + name); require("node:fs").symlinkSync("/app/bin/ez", "/usr/local/bin/ez")'
|
|
31
35
|
# Build identity for relay status (package version stays release-owned).
|
|
32
36
|
ARG BUILD_TAG=""
|
|
33
37
|
ARG BUILD_SHA=""
|
|
34
|
-
RUN node -e 'require("node:fs").writeFileSync("/app/build.json",JSON.stringify({tag:process.env.BUILD_TAG||"",sha:process.env.BUILD_SHA||""})+"\n")'
|
|
35
|
-
ENV HOME=/state/home EZ_AGENT_WORKSPACE=/workspace EZ_CONTROL_DIR=/state/control EZ_EXECUTOR_CLI=grok PATH=/app/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
38
|
+
RUN node -e 'require("node:fs").writeFileSync("/app/build.json",JSON.stringify({tag:process.env.BUILD_TAG||"",sha:process.env.BUILD_SHA||"",codexVersion:require("node:child_process").execFileSync("codex",["--version"],{encoding:"utf8"}).trim()})+"\n")'
|
|
39
|
+
ENV HOME=/state/home EZ_AGENT_WORKSPACE=/workspace EZ_CONTROL_DIR=/state/control EZ_EXECUTOR_CLI=grok PATH=/app/bin:/opt/pnpm:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
36
40
|
WORKDIR /workspace
|
|
37
41
|
ENTRYPOINT ["/app/docker/entrypoint.sh"]
|
|
38
42
|
CMD ["start"]
|
package/docker/smoke.mjs
CHANGED
|
@@ -34,7 +34,7 @@ try {
|
|
|
34
34
|
assert.equal(login.status,0,login.stderr);
|
|
35
35
|
assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-application/);
|
|
36
36
|
assert.match(login.stdout,/\/usr\/local\/bin\/ezenciel-agents-message/);
|
|
37
|
-
assert.match(login.stdout,/codex-cli
|
|
37
|
+
assert.match(login.stdout,/codex-cli \d+\.\d+\.\d+/);
|
|
38
38
|
const started = run(['run','-d','--name',application,...nonroot,image,'start']);
|
|
39
39
|
assert.equal(started.status,0,started.stderr);
|
|
40
40
|
let ready = false;
|
|
@@ -58,6 +58,9 @@ the ordinary last-selection-wins behavior. Already-admitted runs keep their
|
|
|
58
58
|
captured conversation and AI. Uncertain control responses require a fresh read,
|
|
59
59
|
not blind repetition of `/new`.
|
|
60
60
|
|
|
61
|
+
The scope-control response includes `software`, the same current runtime/version
|
|
62
|
+
summary used by Telegram status. It is read-only and does not create a session.
|
|
63
|
+
|
|
61
64
|
For private application conversations, `GET /v1/scope-control?scope=<encoded-scope>` returns
|
|
62
65
|
the same catalog and the current scope's public control ID and preset. The scope
|
|
63
66
|
is the application's original admission scope, resolved under its authenticated
|
|
@@ -36,7 +36,12 @@ References used for the adapter contract:
|
|
|
36
36
|
- [Codex non-interactive execution](https://learn.chatgpt.com/docs/non-interactive-mode)
|
|
37
37
|
- [OpenCode native run events](https://github.com/anomalyco/opencode/blob/dev/packages/opencode/src/cli/cmd/run.ts)
|
|
38
38
|
|
|
39
|
-
The catalog is
|
|
39
|
+
The catalog is native metadata, not an authentication or billing health check.
|
|
40
|
+
Codex refreshes through `codex debug models` with the whitelisted executor
|
|
41
|
+
environment and the execution-bound `CODEX_HOME`; discovery starts no session
|
|
42
|
+
or inference. Only visible model names and supported efforts are projected.
|
|
43
|
+
Unavailable discovery retains the native cache. The installed native version
|
|
44
|
+
still owns which models it advertises; Core never guesses newly released aliases.
|
|
40
45
|
Grok/Codex/OpenCode support explicit listed model/effort choices. Other installed clients
|
|
41
46
|
offer their own default only in this slice. OpenCode lists the installed `opencode models`
|
|
42
47
|
catalog with the whitelisted executor environment, so only models the relay can actually
|
|
@@ -46,7 +51,8 @@ Authenticated providers appear only when the agent-bound
|
|
|
46
51
|
login); otherwise the free tier lists. `EZ_OPENCODE_PROVIDERS` optionally
|
|
47
52
|
restricts the catalog to named providers (e.g. `opencode-go`); a set
|
|
48
53
|
allowlist that matches nothing offers no OpenCode choice rather than falling
|
|
49
|
-
back outside it.
|
|
54
|
+
back outside it. Codex discovery refreshes directly; for other cache-only
|
|
55
|
+
catalogs, refresh by opening the native client;
|
|
50
56
|
the relay does not install models, manage subscriptions or guess aliases.
|
|
51
57
|
|
|
52
58
|
Setup initialization and relay startup seed one default choice per installed client.
|
|
@@ -109,12 +109,12 @@ after service outages; core revocation blocks sends immediately.
|
|
|
109
109
|
|
|
110
110
|
## Native execution and core tools
|
|
111
111
|
|
|
112
|
-
V1 uses audited Codex CLI **0.
|
|
112
|
+
V1 uses audited Codex CLI **0.160.0** for task work, regardless of the owner's
|
|
113
113
|
selected executor. Missing or different versions fail closed; upgrading this pin
|
|
114
114
|
requires repeating the native tool inventory test. Owner work retains its normal
|
|
115
115
|
executor. The task runner creates a fresh ephemeral home/session, skips user
|
|
116
116
|
config, rules and ancestor project instructions, and disables shell, file/image,
|
|
117
|
-
browser, apps, hooks, memory and agent spawning tools. A native permissions
|
|
117
|
+
browser, apps, hooks, memory, goals and agent spawning tools. A native permissions
|
|
118
118
|
profile denies general filesystem access and tool network access. No owner
|
|
119
119
|
workspace or conversation is passed to this runner. The runner uses the pinned
|
|
120
120
|
CLI's bundled model catalog with task-specific tool defaults: direct MCP calls,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use Node 22+ and pnpm 10.30.3: `pnpm install --frozen-lockfile`, then
|
|
4
4
|
`pnpm verify`. Tests use temporary synthetic state; TypeScript checks source and
|
|
5
|
-
tests. Use `
|
|
5
|
+
tests. Use `pnpm run release:check` for package contents. See CONTRIBUTING.md for
|
|
6
6
|
PR expectations and [release checks](releasing.md) for Docker and clean-host QA.
|
|
7
7
|
|
|
8
8
|
For the cross-repository fixture, place the reviewed WhatsApp source beside this
|
package/docs/docker-runtime.md
CHANGED
|
@@ -162,6 +162,16 @@ control state, then remove only the verified orphaned `control-state.lock` and
|
|
|
162
162
|
restart the single relay. Never remove a lock while a writer might still be live.
|
|
163
163
|
Model catalog metadata is exported from the selected host CLI without credentials.
|
|
164
164
|
The AI menu stays within that CLI. No automatic executor fallback is performed.
|
|
165
|
+
Codex discovery invokes `codex debug models` in the agent's bound `CODEX_HOME`
|
|
166
|
+
and projects only visible model names and supported reasoning efforts. Older
|
|
167
|
+
clients without that command or unavailable discovery retain the native cache.
|
|
168
|
+
The native CLI owns availability: a new model may require a CLI upgrade as well
|
|
169
|
+
as a catalog refresh. Managed builds and upgrades resolve npm Codex `latest` before cache lookup and
|
|
170
|
+
record the concrete version in the build/update receipt and image identity. Old
|
|
171
|
+
deployment Codex pins are superseded. Host installations remain operator-owned.
|
|
172
|
+
Restricted correspondence retains its audited native version gate and fails closed
|
|
173
|
+
when the normal CLI is newer than the audited version. Direct Docker builds use
|
|
174
|
+
`latest`; use `--no-cache` to refresh native acquisition on a reused builder.
|
|
165
175
|
|
|
166
176
|
## Secrets and plugins
|
|
167
177
|
|
package/docs/plugin-guide.md
CHANGED
package/docs/plugins.md
CHANGED
|
@@ -157,6 +157,11 @@ snapshots. Reinstallation reuses that data. Plain install does not replace a dif
|
|
|
157
157
|
compatible upgrades under saved policy; see [upgrades](upgrades.md). Manual
|
|
158
158
|
uninstall/reinstall remains available but does not promise schema rollback.
|
|
159
159
|
|
|
160
|
+
Installed package files retain their reviewed modes (0644 for ordinary files,
|
|
161
|
+
0755 for executables and nested directories), including under restrictive umasks
|
|
162
|
+
or inherited default ACLs. The snapshot root remains private (0700); provider
|
|
163
|
+
data and private operation receipts keep their separate permissions.
|
|
164
|
+
|
|
160
165
|
For another reviewed local package, inspect with `plugins inspect <id> --source
|
|
161
166
|
/absolute/source`, then install with that source and the returned `--revision
|
|
162
167
|
sha256:...`. Changed contents fail. To make a reviewed package discoverable without installing,
|
|
@@ -329,7 +334,7 @@ only to its authoritative service over the reviewed transport.
|
|
|
329
334
|
|
|
330
335
|
## Verification
|
|
331
336
|
|
|
332
|
-
`
|
|
337
|
+
`pnpm run verify` runs offline negative/contract tests. Explicit Docker integration:
|
|
333
338
|
`node docker/plugin-smoke.mjs`. It builds the actual WhatsApp package with the
|
|
334
339
|
synthetic transport in an isolated snapshot and tests installation, start,
|
|
335
340
|
registered CLI, literal file paths, idempotency, restart persistence and
|
package/docs/releasing.md
CHANGED
|
@@ -11,7 +11,7 @@ separate release bot is not required.
|
|
|
11
11
|
update plugin manifest version when present. After lockfile changes, copy
|
|
12
12
|
`pnpm-lock.yaml` to `docker/pnpm-lock.yaml` (npm omits the root lockfile; CI
|
|
13
13
|
checks this shipping copy is identical). Never overwrite a published version.
|
|
14
|
-
2. Run `pnpm install --frozen-lockfile`, `pnpm verify`, `
|
|
14
|
+
2. Run `pnpm install --frozen-lockfile`, `pnpm verify`, `pnpm run release:check`,
|
|
15
15
|
`git diff --check`. Review `npm audit --omit=dev` and dependency licenses.
|
|
16
16
|
Record accepted risks with a reason; never run an unreviewed audit fix.
|
|
17
17
|
3. Build both Docker targets from a clean checkout:
|
package/docs/scheduling.md
CHANGED
|
@@ -13,6 +13,7 @@ ezenciel-agents-schedule create --name Research --now \
|
|
|
13
13
|
--text '/goal Complete the authorized research objective. Save evidence, verify the outcome, and send the owner the result.'
|
|
14
14
|
ezenciel-agents-schedule list
|
|
15
15
|
ezenciel-agents-schedule runs
|
|
16
|
+
ezenciel-agents-schedule trigger SCHEDULE_ID --key owner-smoke-20261003
|
|
16
17
|
ezenciel-agents-schedule pause SCHEDULE_ID
|
|
17
18
|
ezenciel-agents-schedule resume SCHEDULE_ID
|
|
18
19
|
ezenciel-agents-schedule remove SCHEDULE_ID
|
|
@@ -36,9 +37,22 @@ the host changes zones. Nonexistent DST wall times are skipped; repeated wall
|
|
|
36
37
|
times fire once, at the earlier instant. Search is bounded to eight years.
|
|
37
38
|
Public-holiday calendars and arbitrary RRULE syntax are not implemented.
|
|
38
39
|
|
|
39
|
-
New tasks
|
|
40
|
-
`--effort` override those choices
|
|
41
|
-
|
|
40
|
+
New tasks capture selected engine settings. Explicit `--cli`, `--model` and
|
|
41
|
+
`--effort` override those choices. Edits preserve existing choices.
|
|
42
|
+
Core always passes the task's concrete saved model. Creation and edits
|
|
43
|
+
require a model in the captured settings or an explicit `--model`; missing models
|
|
44
|
+
are rejected. Legacy tasks with no saved model remain inspectable, but dispatch
|
|
45
|
+
and executor admission refuse to run them until they are explicitly configured.
|
|
46
|
+
Chat changes and native defaults never supply a missing model at run time.
|
|
47
|
+
To run an existing task outside its cadence, use `trigger ID --key REQUEST_KEY`: it queues
|
|
48
|
+
the task's saved instructions, engine, model, effort and delivery binding in a
|
|
49
|
+
fresh session, without changing its definition or next regular occurrence.
|
|
50
|
+
Model/text/trigger overrides are rejected. Reuse the same request key after an
|
|
51
|
+
uncertain response; it reads back the same run while the relay retains it.
|
|
52
|
+
After a relay restart, inspect native history and provider/canonical receipts
|
|
53
|
+
before issuing another request: the run ledger remains intentionally in memory.
|
|
54
|
+
Paused, stale-owner, overlapping or held tasks and unmet conditions cannot be
|
|
55
|
+
triggered. Historical deferred tasks keep their source context
|
|
42
56
|
available through `ezenciel-agents-schedule context`.
|
|
43
57
|
|
|
44
58
|
## Execution and authority
|
package/docs/upgrades.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Agent-owned software upgrades
|
|
2
2
|
|
|
3
|
+
Private `beta.N.rc.M` and `beta.N.qa.M` candidates may upgrade to the matching
|
|
4
|
+
published `beta.N`, despite SemVer's prerelease ordering. All identity, integrity,
|
|
5
|
+
deployment and owner-policy checks still apply; older betas remain rejected.
|
|
6
|
+
|
|
3
7
|
For an application's independently deployed frontend, backend or embedded gateway,
|
|
4
8
|
see [managed applications](managed-applications.md). They use their existing
|
|
5
9
|
deployment tools and a saved maintenance mandate; this package updater inventories
|
|
@@ -12,6 +16,8 @@ automatic channel for core and plugins without a saved policy. Existing explicit
|
|
|
12
16
|
stable or manual policies are preserved. The owner may select either per target.
|
|
13
17
|
The main target and installed plugins version independently.
|
|
14
18
|
|
|
19
|
+
Queued upgrades leave channel admission open while native work runs. The supervisor pauses admission only after work is idle and releases that pause if a turn raced the check.
|
|
20
|
+
|
|
15
21
|
The owner chooses the saved update policy. The host supervisor owns
|
|
16
22
|
interruption-safe replacement. There is one host service per agent, not a second
|
|
17
23
|
agent or an additional updater daemon. Its replaceable child runs the normal CLI
|
|
@@ -228,7 +234,7 @@ Upgrading does not invent an explicit setting for an omitted field.
|
|
|
228
234
|
Verify upgrade from the previous supported artifact, retained identity/state,
|
|
229
235
|
failed-health rollback, and rejection of incompatible candidates. Main runtime
|
|
230
236
|
changes also need supervisor restart and requesting-process-exit tests. Run
|
|
231
|
-
`pnpm verify`, `
|
|
237
|
+
`pnpm verify`, `pnpm run release:check`, packed Docker builds and
|
|
232
238
|
`node docker/upgrade-smoke.mjs` with a local `EZ_WHATSAPP_SOURCE` containing the
|
|
233
239
|
WhatsApp fixture. The smoke uses synthetic transport only. VM installation,
|
|
234
240
|
agent-led upgrades, restart and real account acceptance remain separate QA gates.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jc_stack/ez-agents",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.47",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A lightweight foundation for persistent business AI assistants using existing AI harnesses, workspaces and plugins.",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"scripts/assert-local-registry.mjs",
|
|
53
53
|
".dockerignore",
|
|
54
54
|
"scripts/trusted-beta.mjs",
|
|
55
|
-
"scripts/generate-publish-caller.mjs"
|
|
55
|
+
"scripts/generate-publish-caller.mjs",
|
|
56
|
+
"pnpm-workspace.yaml"
|
|
56
57
|
],
|
|
57
58
|
"publishConfig": {
|
|
58
59
|
"access": "public",
|
|
@@ -70,7 +71,7 @@
|
|
|
70
71
|
"verify": "pnpm test && pnpm build",
|
|
71
72
|
"smoke:scheduler": "tsx scripts/smoke-scheduler.ts",
|
|
72
73
|
"smoke": "./bin/ezenciel-agents-docker run --rm --no-deps relay smoke",
|
|
73
|
-
"prepublishOnly": "
|
|
74
|
+
"prepublishOnly": "pnpm run verify",
|
|
74
75
|
"registry:up": "docker compose -f registry/docker-compose.yml up -d",
|
|
75
76
|
"registry:down": "docker compose -f registry/docker-compose.yml down",
|
|
76
77
|
"publish:local": "node scripts/publish-local.mjs",
|
|
@@ -10,6 +10,7 @@ import { RunStore } from '../src/runs.js'
|
|
|
10
10
|
import { serveDeliverySocket, createLedgerHandler } from '../src/delivery-socket.js'
|
|
11
11
|
import { packageVersion } from '../src/version.js'
|
|
12
12
|
import { initialPreset } from '../src/ai.js'
|
|
13
|
+
import { discoverDefaults } from '../src/client-defaults.js'
|
|
13
14
|
import { initializeWorkspace } from '../src/workspace.js'
|
|
14
15
|
import type { Update } from 'grammy/types'
|
|
15
16
|
|
|
@@ -21,7 +22,9 @@ const purpose=join(root,'purpose.md');await writeFile(purpose,'Verify native sch
|
|
|
21
22
|
await initializeWorkspace(workspace,purpose)
|
|
22
23
|
const control=new ControlStore(controlDir,1000),scheduler=new Scheduler(controlDir),runs=new RunStore(controlDir)
|
|
23
24
|
await control.requestPairing(101,101);await control.approveOwner(101)
|
|
24
|
-
const
|
|
25
|
+
const initial=initialPreset(cli)
|
|
26
|
+
const defaults=initial.model ? initial : (await discoverDefaults(workspace,{nativeCodexFallback:true})).find(p=>p.cli===cli)
|
|
27
|
+
const owner=(await control.status()).owner!,execution=await control.captureChoice({...initial,model:defaults?.model,effort:defaults?.effort})
|
|
25
28
|
const replies:{at:number;text:string}[]=[]
|
|
26
29
|
const relay=createRelay({workspace,controlDir,pairingTtlMs:1000,executorTimeoutMs:0,executorCli:cli,telegramBotToken:'fixture'})
|
|
27
30
|
// Engine children reach this process's memory ledger through the socket.
|
package/src/ai.ts
CHANGED
|
@@ -128,7 +128,25 @@ export const readCuratedModels = async (controlDir?: string): Promise<ModelChoic
|
|
|
128
128
|
return curated
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
// Let the installed native client refresh its own catalog. No conversation or
|
|
132
|
+
// prompt is submitted; CODEX_HOME is the same agent binding used for execution.
|
|
133
|
+
export const readCodexCatalog = async (codexHome: string, run?: (args: string[], env: NodeJS.ProcessEnv) => Promise<string>): Promise<unknown> => {
|
|
134
|
+
const env = { ...executorEnvironment(), CODEX_HOME: codexHome }
|
|
135
|
+
const args = ['debug', 'models']
|
|
136
|
+
try {
|
|
137
|
+
const stdout = run ? await run(args, env) : await (async () => {
|
|
138
|
+
const invocation = executorInvocation('codex', args)
|
|
139
|
+
return (await promisify(execFile)(invocation.command, invocation.args, {
|
|
140
|
+
cwd: codexHome, env, timeout: 8000, maxBuffer: 4 * 1024 * 1024,
|
|
141
|
+
})).stdout
|
|
142
|
+
})()
|
|
143
|
+
const catalog = JSON.parse(stdout)
|
|
144
|
+
if (catalog && Array.isArray(catalog.models) && catalog.models.length) return catalog
|
|
145
|
+
} catch { /* Older/offline native clients retain their last known catalog. */ }
|
|
146
|
+
try { return JSON.parse(await readFile(join(codexHome, 'models_cache.json'), 'utf8')) } catch { return {} }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const readModels = async (home = homedir(), available = installed, codexHome = join(home, '.codex'), opencodeRunner?: (args: string[]) => Promise<string>, opencodeDataHome?: string, opencodeAllowlist?: string[], curationDir?: string, codexRunner?: (args: string[], env: NodeJS.ProcessEnv) => Promise<string>): Promise<ModelChoice[]> => {
|
|
132
150
|
const models: ModelChoice[] = []
|
|
133
151
|
const record = (value: unknown): Record<string, unknown> =>
|
|
134
152
|
value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
|
@@ -147,7 +165,7 @@ export const readModels = async (home = homedir(), available = installed, codexH
|
|
|
147
165
|
}
|
|
148
166
|
}
|
|
149
167
|
if (await available('codex')) {
|
|
150
|
-
const cache = await
|
|
168
|
+
const cache = record(await readCodexCatalog(codexHome, codexRunner))
|
|
151
169
|
for (const entry of Array.isArray(cache.models) ? cache.models : []) {
|
|
152
170
|
const info = record(entry)
|
|
153
171
|
if (info.visibility !== 'list' || !safe(info.slug)) continue
|
|
@@ -11,6 +11,7 @@ import { MAX_INCOMING_ATTACHMENT_BYTES, stageChatAttachment } from './files.js'
|
|
|
11
11
|
import { assertEffort } from './model-policy.js'
|
|
12
12
|
import { ApprovalStore } from './approval.js'
|
|
13
13
|
import { Tasks } from './tasks.js'
|
|
14
|
+
import { softwareStatus } from './software-status.js'
|
|
14
15
|
import { SpeechCreditsDepletedError } from './audio.js'
|
|
15
16
|
|
|
16
17
|
const hash = (value: string) => createHash('sha256').update(value).digest('hex')
|
|
@@ -72,7 +73,7 @@ export class ApplicationChannel {
|
|
|
72
73
|
private admissions = Promise.resolve()
|
|
73
74
|
readonly bindings: ApplicationBindings
|
|
74
75
|
constructor(private options: {
|
|
75
|
-
controlDir: string; workspace?: string; initial: AiPreset
|
|
76
|
+
controlDir: string; workspace?: string; isolation?: string; initial: AiPreset
|
|
76
77
|
wake: () => void
|
|
77
78
|
cancel: (id: string) => Promise<void>
|
|
78
79
|
speech?: (text: string, language: 'en' | 'es') => Promise<{ buffer: Buffer; mimeType: string }>
|
|
@@ -180,6 +181,7 @@ export class ApplicationChannel {
|
|
|
180
181
|
await this.sharedBinding(bindingId, false)
|
|
181
182
|
return {ai:{...ai, selectedId:session?.preset?.id ?? ai.selectedId,
|
|
182
183
|
presets:session?.preset ? [...ai.presets.filter(item=>item.id!==session.preset!.id),session.preset] : ai.presets},
|
|
184
|
+
software: await softwareStatus(this.options.controlDir, this.options.isolation),
|
|
183
185
|
models, activeSessionId:session?.sessionId ?? null, sessions:session ? [{id:session.sessionId,title:sessionTitle(session),cli:session.cli,archived:false}] : []}
|
|
184
186
|
}
|
|
185
187
|
async changeScopeControls(bindingId: string, scope: string, input: unknown) {
|
package/src/delivery-socket.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ApprovalStore } from './approval.js'
|
|
|
8
8
|
import { deliveredMessages } from './message-history.js'
|
|
9
9
|
import { readOnlyOwner, requireOwnerExecution } from './execution-authority.js'
|
|
10
10
|
import { telegramOwner } from './control-state.js'
|
|
11
|
+
import { Scheduler } from './scheduler.js'
|
|
11
12
|
|
|
12
13
|
// Stateless pipe transport. The relay owns the runs/outbox ledger in process
|
|
13
14
|
// memory; engine children, plugin children and the host executor reach it
|
|
@@ -27,6 +28,7 @@ export { deliverySocketPath }
|
|
|
27
28
|
export type DeliverySocketOp =
|
|
28
29
|
| { op: 'ping' }
|
|
29
30
|
| { op: 'status' }
|
|
31
|
+
| { op: 'triggerSchedule'; payload: { scheduleId: string; revision: string; key: string; callerRunId?: string } }
|
|
30
32
|
| { op: 'enqueue'; payload: Record<string, unknown> }
|
|
31
33
|
| { op: 'wait'; payload: { id: string; timeoutMs?: number } }
|
|
32
34
|
| { op: 'get'; payload: { runId: string } }
|
|
@@ -208,6 +210,19 @@ export const createLedgerHandler = (controlDir: string, hooks: LedgerHooks): Del
|
|
|
208
210
|
queued: all.filter((run) => run.status === 'queued').length,
|
|
209
211
|
}
|
|
210
212
|
}
|
|
213
|
+
case 'triggerSchedule': {
|
|
214
|
+
const {scheduleId,revision,key,callerRunId}=op.payload ?? {}
|
|
215
|
+
if (!nonEmptyString(scheduleId) || !nonEmptyString(revision) || !nonEmptyString(key)) throw new Error('Task ID, revision and request key are required')
|
|
216
|
+
if (callerRunId !== undefined) {
|
|
217
|
+
const caller=await requireOwnerExecution(controlDir,callerRunId)
|
|
218
|
+
if (caller.replyOnly || (caller.scheduled && caller.scheduled.pairedAt !== (await readOnlyOwner(controlDir))?.pairedAt)) throw new Error('Scheduling requires an active owner-authorized run')
|
|
219
|
+
}
|
|
220
|
+
const owner=await readOnlyOwner(controlDir)
|
|
221
|
+
if (!owner) throw new Error('Pair an owner before scheduling')
|
|
222
|
+
const run=await new Scheduler(controlDir).trigger(scheduleId,revision,key,owner,runs)
|
|
223
|
+
hooks.wake()
|
|
224
|
+
return run
|
|
225
|
+
}
|
|
211
226
|
case 'enqueue': {
|
|
212
227
|
const envelope = (op.payload ?? {}) as Record<string, unknown>
|
|
213
228
|
if (envelope.kind === 'owner') {
|
|
@@ -351,4 +366,3 @@ export const deliverySocketAlive = (socketPath: string, timeoutMs = 2000): Promi
|
|
|
351
366
|
// closed DeliverySocketOp union.
|
|
352
367
|
export const callDeliverySocket = (socketPath: string, op: DeliverySocketOp, timeoutMs = 130_000): Promise<unknown> =>
|
|
353
368
|
callSocket(socketPath, op, timeoutMs)
|
|
354
|
-
|
package/src/host-executor.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { readModels, validateSelection, validateOpencodeProviders, type ModelCho
|
|
|
13
13
|
import type { ChildProcess } from 'node:child_process'
|
|
14
14
|
import { packageVersion } from './version.js'
|
|
15
15
|
import { installedPluginVersions } from './software-status.js'
|
|
16
|
+
import { assertScheduledModel } from './model-policy.js'
|
|
16
17
|
import { processSnapshot } from './process-tree.js'
|
|
17
18
|
|
|
18
19
|
export type PluginNetworkRoute = { revisions:string[]; bindings:{service:string;network:string}[] }
|
|
@@ -194,6 +195,10 @@ export const serveHostExecutor = async (installation: HostInstallation, signal:
|
|
|
194
195
|
await new Tasks(agent.controlDir).authorize(run, false)
|
|
195
196
|
} else await authorizeRun(agent.controlDir, path.basename(base))
|
|
196
197
|
const opts=request.options as ExecutorOptions
|
|
198
|
+
if (run?.scheduled) {
|
|
199
|
+
assertScheduledModel(run.execution?.preset.model)
|
|
200
|
+
if (opts.model !== run.execution!.preset.model) throw new Error('Scheduled run model does not match its saved task model')
|
|
201
|
+
}
|
|
197
202
|
const cli = opts.cli || installation.cli
|
|
198
203
|
resolveExecutor(cli)
|
|
199
204
|
const provider = cli === 'codex' && opts.provider
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { loadConfig, type Config } from './config.js'
|
|
|
26
26
|
import { ControlStore, telegramOwner, ownerId, ownerEpoch } from './control-state.js'
|
|
27
27
|
import { ApprovalStore } from './approval.js'
|
|
28
28
|
import { startExecutorJob, terminateJob, opencodeDataHome } from './executor.js'
|
|
29
|
+
import { assertScheduledModel } from './model-policy.js'
|
|
29
30
|
import { RunStore, type RunRecord } from './runs.js'
|
|
30
31
|
import { splitTelegramText } from './reply.js'
|
|
31
32
|
import { markdownToTelegramHtml, escapeHtml } from './format.js'
|
|
@@ -100,7 +101,7 @@ export const createRelay = (config: Config, launch = startExecutorJob) => {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
const applicationChannel = new ApplicationChannel({
|
|
103
|
-
controlDir: config.controlDir, workspace: config.workspace, initial: aiMenu.initial,
|
|
104
|
+
controlDir: config.controlDir, workspace: config.workspace, isolation: config.isolation, initial: aiMenu.initial,
|
|
104
105
|
aiControls: aiMenu,
|
|
105
106
|
speech: config.geminiApiKey ? (text, language) => synthesizeSpeech(text, {
|
|
106
107
|
geminiApiKey: config.geminiApiKey,
|
|
@@ -240,6 +241,7 @@ export const createRelay = (config: Config, launch = startExecutorJob) => {
|
|
|
240
241
|
? { sessionId: randomUUID(), hasStarted: false, nativeSessionId: undefined }
|
|
241
242
|
: await control.executionSession(started.execution!)
|
|
242
243
|
const selected = run.taskId ? (started.execution?.preset.cli === 'codex' ? started.execution.preset : initialPreset('codex')) : started.execution!.preset
|
|
244
|
+
if (run.scheduled) assertScheduledModel(selected.model)
|
|
243
245
|
const { child, cleanup } = await launch(texts, {
|
|
244
246
|
workspace: config.workspace,
|
|
245
247
|
timeoutMs: config.executorTimeoutMs,
|
package/src/install-tools.mjs
CHANGED
|
@@ -114,6 +114,11 @@ export async function migrateLedger({ deployment } = {}) {
|
|
|
114
114
|
return { ok: true, deployment: directory, changed: true, port, overlay };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export async function latestCodexVersion(invoke,manager={command:"npm",args:[]}) {
|
|
118
|
+
const version=JSON.parse(await invoke(manager.command,[...manager.args,'view','@openai/codex@latest','version','--json','--registry','https://registry.npmjs.org/']));
|
|
119
|
+
if(typeof version!=='string'||!/^\d+\.\d+\.\d+$/.test(version))throw Error('Invalid latest Codex version');
|
|
120
|
+
return version;
|
|
121
|
+
}
|
|
117
122
|
async function fingerprintOf(source,invoke) {
|
|
118
123
|
const listing=JSON.parse(await invoke('npm',['pack','--dry-run','--ignore-scripts','--json'],{cwd:source}));
|
|
119
124
|
const hash=createHash('sha256');
|
|
@@ -144,8 +149,9 @@ async function buildIdentity(source,label,invoke) {
|
|
|
144
149
|
export async function build({home=defaultHome(),source=root,label},invoke=run) {
|
|
145
150
|
source=await fs.realpath(source);
|
|
146
151
|
const identity=await buildIdentity(source,label,invoke);
|
|
152
|
+
const codexVersion=await latestCodexVersion(invoke);
|
|
147
153
|
const fingerprint=await fingerprintOf(source,invoke);
|
|
148
|
-
const artifact=createHash('sha256').update(JSON.stringify([fingerprint,identity.label??'',identity.sha??''])).digest('hex');
|
|
154
|
+
const artifact=createHash('sha256').update(JSON.stringify([fingerprint,identity.label??'',identity.sha??'',codexVersion])).digest('hex');
|
|
149
155
|
const image='ezenciel-agents:install-'+artifact.slice(0,24);
|
|
150
156
|
const dir=path.join(path.resolve(home),'builds',artifact);await fs.mkdir(dir,{recursive:true,mode:0o700});
|
|
151
157
|
const lock=path.join(dir,'lock'),receipt=path.join(dir,'status.json'),log=path.join(dir,'build.log');
|
|
@@ -155,14 +161,14 @@ export async function build({home=defaultHome(),source=root,label},invoke=run) {
|
|
|
155
161
|
await handle.writeFile(JSON.stringify({pid:process.pid,source,image,...identity}));
|
|
156
162
|
const prior=await read(receipt).catch(absent);
|
|
157
163
|
if(prior?.state==='completed')try{if(await invoke('docker',['image','inspect','--format','{{.Id}}',image])===prior.imageId)return {...prior,reused:true};}catch{/* Image was removed; rebuild under the same exclusive lock. */}
|
|
158
|
-
await atomic(receipt,{state:'building',pid:process.pid,source,image,log,...identity});
|
|
159
|
-
console.error(JSON.stringify({state:'building',image,log,...identity}));
|
|
160
|
-
const buildArgs=identity.label?['--build-arg',`BUILD_TAG=${identity.label}`,'--build-arg',`BUILD_SHA=${identity.sha}`]:[];
|
|
164
|
+
await atomic(receipt,{state:'building',pid:process.pid,source,image,log,codexVersion,...identity});
|
|
165
|
+
console.error(JSON.stringify({state:'building',image,log,codexVersion,...identity}));
|
|
166
|
+
const buildArgs=['--build-arg',`CODEX_CLI_VERSION=${codexVersion}`,...(identity.label?['--build-arg',`BUILD_TAG=${identity.label}`,'--build-arg',`BUILD_SHA=${identity.sha}`]:[])];
|
|
161
167
|
const tags=[image,...(identity.label?[`ezenciel-agents:${identity.label}`]:[])];
|
|
162
168
|
const output=await fs.open(log,'a',0o600);
|
|
163
169
|
try{await invoke('docker',['build','--progress','plain','--target','runtime',...buildArgs,...tags.flatMap(t=>['-t',t]),source],{log:output.fd,timeout:3600000});}finally{await output.close();}
|
|
164
170
|
if(await fingerprintOf(source,invoke)!==fingerprint)throw Error('Package changed during build; keep the source stable and retry');
|
|
165
|
-
const result={state:'completed',source,image,imageId:await invoke('docker',['image','inspect','--format','{{.Id}}',image]),log,...identity};await atomic(receipt,result);return result;
|
|
171
|
+
const result={state:'completed',source,image,imageId:await invoke('docker',['image','inspect','--format','{{.Id}}',image]),log,codexVersion,...identity};await atomic(receipt,result);return result;
|
|
166
172
|
}catch(e){await atomic(receipt,{state:'failed',source,image,log,error:e.message,...identity});throw e;}
|
|
167
173
|
finally{await handle.close();await fs.rm(lock);}
|
|
168
174
|
}
|
package/src/model-policy.ts
CHANGED
|
@@ -3,6 +3,10 @@ export const allowedEffort = (effort?: string, _model?: string, _cli?: string) =
|
|
|
3
3
|
export function assertEffort(effort?: string, model?: string, cli?: string) {
|
|
4
4
|
if (!allowedEffort(effort,model,cli)) throw new Error('Invalid reasoning effort')
|
|
5
5
|
}
|
|
6
|
+
export function assertScheduledModel(model: unknown): asserts model is string {
|
|
7
|
+
if (typeof model !== 'string' || !/^[a-zA-Z0-9_./:-]{1,160}$/.test(model))
|
|
8
|
+
throw new Error('Scheduled tasks require a saved explicit model; configure the task with --model')
|
|
9
|
+
}
|
|
6
10
|
export function executionDefaults<T extends { model?: string; effort?: string }>(cli: string, options: T): T {
|
|
7
11
|
assertEffort(options.effort,options.model,cli)
|
|
8
12
|
return options
|
package/src/plugins/manager.mjs
CHANGED
|
@@ -553,7 +553,13 @@ export async function install(home,config,name,source,revision) {
|
|
|
553
553
|
const base=path.join(home,'packages',name),target=path.join(base,revision.slice(7));await privateDir(base);
|
|
554
554
|
const stage=path.join(base,`.stage-${randomUUID()}`);await privateDir(stage);
|
|
555
555
|
try {
|
|
556
|
-
for(const [relative,file] of p.files) {
|
|
556
|
+
for(const [relative,file] of p.files) {
|
|
557
|
+
const dest=path.join(stage,relative);await fs.mkdir(path.dirname(dest),{recursive:true,mode:0o755});
|
|
558
|
+
// Package modes are part of the reviewed snapshot; umask/default ACLs
|
|
559
|
+
// must not make Docker COPY inputs unreadable to the runtime user.
|
|
560
|
+
for(let dir=path.dirname(dest);dir!==stage;dir=path.dirname(dir))await fs.chmod(dir,0o755);
|
|
561
|
+
await fs.writeFile(dest,file.data,{mode:file.mode,flag:'wx'});await fs.chmod(dest,file.mode);
|
|
562
|
+
}
|
|
557
563
|
// A previously interrupted install may leave a snapshot. Never silently replace it.
|
|
558
564
|
try { await fs.rename(stage,target); } catch(error) {
|
|
559
565
|
if(!['EEXIST','ENOTEMPTY'].includes(error.code)) throw error;
|
package/src/runs.ts
CHANGED
|
@@ -141,7 +141,7 @@ export class RunStore {
|
|
|
141
141
|
taskId?: string
|
|
142
142
|
application?: ApplicationOrigin
|
|
143
143
|
delivery?: { bindingId: string; scope: string }
|
|
144
|
-
}): Promise<RunRecord> {
|
|
144
|
+
}, exclusiveSchedule = false): Promise<RunRecord> {
|
|
145
145
|
const store = runsFor(this.controlDir)
|
|
146
146
|
if (input.id) {
|
|
147
147
|
assertId(input.id)
|
|
@@ -152,6 +152,9 @@ export class RunStore {
|
|
|
152
152
|
return existing
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
if (exclusiveSchedule && input.scheduled && [...store.values()].some(run =>
|
|
156
|
+
run.scheduled?.id === input.scheduled!.id && ['queued','running'].includes(run.status)))
|
|
157
|
+
throw new Error('Schedule already has a queued or running occurrence')
|
|
155
158
|
const run: RunRecord = {
|
|
156
159
|
version: input.taskId ? 2 : 1,
|
|
157
160
|
taskId: input.taskId,
|