@jc_stack/ez-agents 0.1.0-beta.46 → 0.1.0-beta.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +2 -0
- package/.env.example +3 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +38 -0
- package/CONTRIBUTING.md +1 -1
- package/Dockerfile +18 -10
- package/THIRD_PARTY_NOTICES.md +6 -3
- package/bin/ez +7 -0
- package/docker/broker-readiness.mjs +54 -0
- package/docker/broker-update-smoke.mjs +86 -0
- package/docker/engine-pnpm-workspace.yaml +4 -0
- package/docker/run.ts +1 -1
- package/docker/smoke.mjs +1 -1
- package/docs/application-channel.md +32 -1
- package/docs/architecture/ai-selection.md +26 -5
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/development-and-testing.md +1 -1
- package/docs/docker-runtime.md +39 -0
- package/docs/isolated-plugin-update-readiness.md +66 -0
- package/docs/plugin-guide.md +1 -1
- package/docs/plugins.md +1 -1
- package/docs/releasing.md +5 -1
- package/docs/scheduling.md +117 -9
- package/docs/selective-monitoring.md +13 -0
- package/docs/upgrades.md +38 -1
- package/docs/work-preflight.md +9 -0
- package/package.json +4 -3
- package/pnpm-workspace.yaml +7 -0
- package/scripts/smoke-scheduler.ts +4 -1
- package/src/ai.ts +41 -4
- package/src/application-channel.ts +43 -3
- package/src/application-cli.ts +7 -1
- package/src/application-origin.ts +2 -0
- package/src/audio.ts +37 -22
- package/src/client-defaults.ts +3 -2
- package/src/config.ts +9 -0
- package/src/control-state.ts +3 -1
- package/src/delivery-socket-client.d.mts +2 -0
- package/src/delivery-socket-client.mjs +2 -0
- package/src/delivery-socket.ts +38 -1
- package/src/event-sources.ts +2 -2
- package/src/executor.ts +72 -1
- package/src/files.ts +47 -3
- package/src/host-executor.ts +40 -26
- package/src/index.ts +42 -17
- package/src/install-tools.mjs +18 -9
- package/src/menu.ts +4 -2
- package/src/model-policy.ts +4 -0
- package/src/plugin-broker.mjs +47 -4
- package/src/plugins/manager.mjs +11 -4
- package/src/run-evidence.ts +78 -0
- package/src/runs.ts +16 -2
- package/src/schedule-cli.ts +80 -10
- package/src/schedule-preflight.ts +23 -0
- package/src/scheduled-tasks.ts +16 -12
- package/src/scheduler.ts +102 -16
- package/src/scripts.ts +167 -0
- package/src/setup.ts +2 -2
- package/src/task-capability.ts +17 -1
- package/src/task-executor.ts +11 -8
- package/src/task-mcp.ts +20 -8
- package/src/tasks.ts +89 -9
- package/src/updates/runtime.mjs +53 -11
- package/src/updates/supervisor.mjs +23 -13
- package/test/ai.test.ts +85 -5
- package/test/application-channel.test.ts +16 -1
- package/test/audio.test.ts +19 -0
- package/test/channel-owner.test.ts +2 -2
- package/test/config.test.ts +8 -0
- package/test/desktop-bridge.test.ts +2 -1
- package/test/executor.test.ts +37 -0
- package/test/failure.test.ts +4 -4
- package/test/files.test.ts +44 -0
- package/test/host-executor.test.ts +49 -6
- package/test/install-json.test.mjs +21 -0
- package/test/install-tools.test.mjs +6 -5
- package/test/native-tasks.test.ts +1 -1
- package/test/plugin-broker.test.mjs +64 -0
- package/test/plugin-manager.test.mjs +20 -0
- package/test/private-speech-config.test.ts +22 -0
- package/test/run-evidence.test.ts +78 -0
- package/test/schedule-cli.test.ts +47 -3
- package/test/schedule-preflight.test.ts +49 -0
- package/test/scheduled-tasks.test.ts +2 -2
- package/test/scheduler-host.test.ts +21 -3
- package/test/scheduler-relay.test.ts +84 -4
- package/test/scheduler.test.ts +21 -1
- package/test/script-schedules.test.ts +219 -0
- package/test/task-native.test.ts +50 -12
- package/test/tasks.test.ts +97 -4
- package/test/updates.test.mjs +77 -18
package/docs/plugin-guide.md
CHANGED
package/docs/plugins.md
CHANGED
|
@@ -334,7 +334,7 @@ only to its authoritative service over the reviewed transport.
|
|
|
334
334
|
|
|
335
335
|
## Verification
|
|
336
336
|
|
|
337
|
-
`
|
|
337
|
+
`pnpm run verify` runs offline negative/contract tests. Explicit Docker integration:
|
|
338
338
|
`node docker/plugin-smoke.mjs`. It builds the actual WhatsApp package with the
|
|
339
339
|
synthetic transport in an isolated snapshot and tests installation, start,
|
|
340
340
|
registered CLI, literal file paths, idempotency, restart persistence and
|
package/docs/releasing.md
CHANGED
|
@@ -11,12 +11,16 @@ 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:
|
|
18
18
|
`docker build --target test -t ez-release-tests .` and
|
|
19
19
|
`docker build --target runtime -t ez-release-runtime .`.
|
|
20
|
+
Docker CI pins the reviewed Codex baseline (`TASK_CODEX_VERSION`) with the
|
|
21
|
+
existing `CODEX_CLI_VERSION` build argument; update that pin when the native
|
|
22
|
+
audit advances. Baseline CI does not validate a newly published native version.
|
|
23
|
+
Managed installs and upgrades keep their existing native version selection.
|
|
20
24
|
Main relay: `EZ_RELAY_IMAGE=ez-release-runtime node docker/smoke.mjs`.
|
|
21
25
|
The WhatsApp Compose overlay has no separate relay image; verify its
|
|
22
26
|
provider integration with the synthetic plugin smoke in `docs/plugins.md`.
|
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,11 +37,89 @@ 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
|
|
|
58
|
+
## Registered script schedules
|
|
59
|
+
|
|
60
|
+
A schedule has one execution type. **Agent** schedules run the saved prompt with
|
|
61
|
+
their saved engine, model and effort, as described below. **Script** schedules run
|
|
62
|
+
a registered, agent-owned script directly: no model, effort, prompt or native
|
|
63
|
+
session is involved.
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
ezenciel-agents-schedule script register book-freshness \
|
|
67
|
+
--file scripts/book-freshness.mjs --interpreter node [--arg=VALUE ...] [--timeout-seconds 900]
|
|
68
|
+
ezenciel-agents-schedule create actual-book-freshness-guard \
|
|
69
|
+
--script book-freshness [--arg=VALUE ...] --cron '15 * * * *' --timezone Asia/Dubai
|
|
70
|
+
ezenciel-agents-schedule script list | show ID | update ID [...] | remove ID
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The agent writes, tests and maintains the script; core owns registration,
|
|
74
|
+
scheduling, authorization, process execution, cancellation and run receipts.
|
|
75
|
+
A registration records the owner binding, a workspace-relative entry point, an
|
|
76
|
+
installed interpreter (command name on the executor PATH or absolute path),
|
|
77
|
+
literal arguments, a timeout and the entry point's SHA-256 under a new revision.
|
|
78
|
+
It references the workspace file; core keeps no source copy. Paths outside the
|
|
79
|
+
workspace (including symlink escapes) and shell command strings are rejected.
|
|
80
|
+
Core invokes `interpreter entry registrationArgs... scheduleArgs...` directly,
|
|
81
|
+
without a shell. Missing interpreters fail clearly; nothing is installed.
|
|
82
|
+
|
|
83
|
+
When an occurrence is queued, the run captures the current registration
|
|
84
|
+
revision and hash. Immediately before spawning, the executor requires that
|
|
85
|
+
revision to be current and the entry-point bytes to match. Changed bytes fail
|
|
86
|
+
the run until the agent explicitly runs `script update`. The hash identifies the
|
|
87
|
+
entry-point file only; it does not certify imported modules or other files.
|
|
88
|
+
|
|
89
|
+
Script runs use the existing scheduler, admission queue, scheduled concurrency
|
|
90
|
+
limit, one-occurrence-per-schedule rule, run records, delivery binding, manual
|
|
91
|
+
`trigger`, pause/resume/edit/remove and `cancel`. They run in the agent's
|
|
92
|
+
isolated environment (relay or host transport) under its normal identity, with
|
|
93
|
+
the executor environment allowlist plus `EZ_RUN_ID`, `EZ_SCHEDULE_ID`,
|
|
94
|
+
`EZ_SCHEDULE_REVISION`, `EZ_DUE_AT`, `EZ_SCRIPT_ID`, `EZ_SCRIPT_REVISION` and
|
|
95
|
+
`EZ_SCRIPT_SHA256`. Use the run/schedule/due identifiers to form a stable
|
|
96
|
+
occurrence identity. External operations go through the bound plugin CLI; a
|
|
97
|
+
script must not read private databases, call providers directly or acquire
|
|
98
|
+
credentials. A script run can read schedule and script state but cannot change
|
|
99
|
+
registrations or schedules.
|
|
100
|
+
|
|
101
|
+
A script runs with the agent's runtime identity and file access, but not inside
|
|
102
|
+
a native engine's own sandbox (for example host Codex `workspace-write`); register
|
|
103
|
+
only code the agent would be allowed to run itself. An interpreter inside the
|
|
104
|
+
workspace is rejected because it would be unhashed code.
|
|
105
|
+
|
|
106
|
+
Each registration has a bounded timeout (default 900 seconds, maximum 21600)
|
|
107
|
+
that does not affect agent runs. Timeout and `cancel` stop the process and its
|
|
108
|
+
children. Failed, timed-out or interrupted script runs are not retried, do not
|
|
109
|
+
fall back to an LLM and do not start a debugging agent. Inspect them with
|
|
110
|
+
`runs`, `run RUN_ID`, `failures` and `evidence`: the run shows the script ID,
|
|
111
|
+
revision and hash, queue/start/finish clocks, exit code, cancellation/timeout,
|
|
112
|
+
failure reason and a 4 KiB redacted tail of stdout/stderr. Those records follow
|
|
113
|
+
the normal relay-memory retention. Stdout is diagnostic only. Process success
|
|
114
|
+
means the script exited 0; domain success still requires the owning plugin's
|
|
115
|
+
validation, receipt and canonical readback, which the script performs and
|
|
116
|
+
records. Notifications use `ezenciel-agents-message` under the normal policy;
|
|
117
|
+
routine unchanged runs should stay quiet.
|
|
118
|
+
|
|
119
|
+
Editing a schedule replaces it completely, so it may switch execution type.
|
|
120
|
+
When replacing an agent schedule with a script, move any judgment the prompt
|
|
121
|
+
performed to its owning workflow first; the script schedule does not carry it.
|
|
122
|
+
|
|
44
123
|
## Execution and authority
|
|
45
124
|
|
|
46
125
|
Due occurrences enter the durable queue with stable IDs and literal task text.
|
|
@@ -49,17 +128,20 @@ No identity files or role instructions are generated. Existing workspace Markdow
|
|
|
49
128
|
provides context, including native `AGENTS.md` discovery and relative links; the
|
|
50
129
|
engine chooses what to read. Per-run native state stays under `control/`.
|
|
51
130
|
|
|
52
|
-
One foreground turn and
|
|
53
|
-
|
|
54
|
-
|
|
131
|
+
One foreground turn and up to six owner-scheduled turns can run at the same time.
|
|
132
|
+
Set `EZ_SCHEDULED_CONCURRENCY` to a positive integer to change the scheduled-job
|
|
133
|
+
limit; it defaults to six. For Docker deployments, put this setting in the
|
|
134
|
+
existing private relay environment file (`EZ_RELAY_ENV_FILE`); no Compose
|
|
135
|
+
layout migration is needed. Foreground inputs queue behind foreground work;
|
|
136
|
+
scheduled turns queue only when their limit is occupied. A long scheduled goal therefore does not block chat. Both streams
|
|
55
137
|
use the existing admission and executor path, with separate native sessions.
|
|
56
138
|
They share the agent-owned Markdown workspace: the agent coordinates shared file
|
|
57
139
|
updates, and plugins enforce canonical record-write guards. The host keeps plugin
|
|
58
|
-
workspace invocations excluded until
|
|
140
|
+
workspace invocations excluded until all native turns finish.
|
|
59
141
|
The agent decides when to send through the message CLI.
|
|
60
142
|
A recurring schedule has at most one pending or active occurrence. Shared
|
|
61
|
-
provider resources still need writer coordination.
|
|
62
|
-
|
|
143
|
+
provider resources still need writer coordination. This is admission to the existing native executor, not a second queue or
|
|
144
|
+
workflow controller.
|
|
63
145
|
|
|
64
146
|
Production relay/host execution has no wall-clock timeout. The old
|
|
65
147
|
`EZ_EXECUTOR_TIMEOUT_SECONDS` setting is ignored. Individual network/tool waits
|
|
@@ -156,6 +238,32 @@ Create a normal recurring schedule with `--every-seconds 900 --when unreviewed-f
|
|
|
156
238
|
|
|
157
239
|
`failures [--all] [--limit N]` returns failedAt, reason, exit code, native session, captured error and runtime versions. Capture keeps at most 4 KiB of redacted stderr, including a failed native Codex turn's error message when supplied. A failure without error details remains explicitly unknown; historical failures are not backfilled. `run RUN_ID` reads an owned run. `review RUN_ID --failed-at ISO --status resolved|attention --diagnosis TEXT --recovery TEXT --outcome TEXT` records the investigation without rewriting execution history. A stale timestamp is rejected; a later failure needs a new review. Restricted reply, external and isolated-task callers cannot review failures. An attention review is handed off, not repeatedly relaunched; another new failure wakes the next review.
|
|
158
240
|
|
|
241
|
+
For a shareable operational projection, use `evidence [RUN_ID] --offset 0 --limit 100`.
|
|
242
|
+
This reads the live relay through its existing authenticated socket, under the
|
|
243
|
+
bound operator or an active unrestricted Telegram owner run. Application/channel,
|
|
244
|
+
external and isolated-task callers cannot use it. It exposes only current-owner
|
|
245
|
+
Telegram foreground/scheduled runs, stable IDs, clocks/status/presets, failure
|
|
246
|
+
metadata without prose, and retained run-associated outbox metadata. No native
|
|
247
|
+
session, prompt, reasoning, tool arguments/results, message text or channel identity
|
|
248
|
+
is returned. Failure text is represented by its SHA-256, not copied or diagnosed.
|
|
249
|
+
|
|
250
|
+
The response carries exact `readStartedAt`/`observedAt`, `snapshotSha256`, and
|
|
251
|
+
coverage total/offset/limit/returned/nextOffset. Follow `nextOffset` with the same
|
|
252
|
+
`--expected SNAPSHOT_SHA256`; changed retained evidence fails closed and requires
|
|
253
|
+
a fresh traversal. Each page recomputes the projection; no snapshot store or
|
|
254
|
+
background capture is added. History resets on relay restart and is bounded to
|
|
255
|
+
2000 terminal runs and 2000 terminal outbox items; full historical coverage cannot
|
|
256
|
+
be inferred. Unassociated deliveries and excluded scopes remain unknown.
|
|
257
|
+
|
|
258
|
+
Delivery records identify queued/sent/failed/unknown state and whether a retained
|
|
259
|
+
Telegram receipt contains a valid clock and returned message IDs (only their count
|
|
260
|
+
is exported). The submitted UTF-8 text/voice-text hash can be compared to an exact
|
|
261
|
+
canonical source. It does not prove rendered or current recipient-side content;
|
|
262
|
+
attachments are not exported. Empty receipt lists never prove that no delivery
|
|
263
|
+
occurred. The relay does not retain native tool/delegation events, so that coverage
|
|
264
|
+
is explicitly unavailable. This command does not mark failures reviewed, send
|
|
265
|
+
messages, read private sessions, trigger work or certify owning results.
|
|
266
|
+
|
|
159
267
|
The prompt controls diagnosis, authorized recovery and quiet notification behavior. Inspect prior effects and receipts before retrying anything. A failed review run itself remains visible as a new failure for the next occurrence.
|
|
160
268
|
|
|
161
269
|
The Telegram Scheduled tasks menu lists enabled schedules that still have a pending
|
|
@@ -178,3 +178,16 @@ After revocation/expiry no subsequent reply may dispatch. Keep these checks
|
|
|
178
178
|
agent-owned: the owner supplies only necessary confirmation, QR scan if needed,
|
|
179
179
|
and the test contact/message. Do not bypass a missing grant by polling the inbox
|
|
180
180
|
in an unrestricted scheduled owner session or sending through the raw CLI.
|
|
181
|
+
|
|
182
|
+
Task `send` accepts `voice: true` for spoken replies up to 1,000 characters on sources advertising `taskVoice`. It uses the relay’s configured speech renderer, rechecks authorization after generation, and preserves the same uncertain-send key contract. Synthesis failure happens before the provider is contacted: it returns an error, sends no text and stores no operation, so the same key may be retried. Voice and file attachments cannot be combined.
|
|
183
|
+
|
|
184
|
+
### Incoming files and images
|
|
185
|
+
|
|
186
|
+
The restricted MCP `read_attachment` tool accepts only an `incomingId` from the
|
|
187
|
+
current run's correspondence. The channel supplies private bounded bytes through
|
|
188
|
+
`task-document`, bound to its account and exact conversation. Core reuses
|
|
189
|
+
`stageChatAttachment` validation (10 MiB) and returns UTF-8 TXT/Markdown or
|
|
190
|
+
PDF text via Poppler, at most 256000 bytes. JPEG/PNG/WebP images return their
|
|
191
|
+
original bounded pixels as MCP image content to the native engine, never an
|
|
192
|
+
extra inference call or a base64 text prompt. It never accepts an owner file path.
|
|
193
|
+
MP4 clips up to 10 MiB and 120 seconds return at most eight sampled JPEG frames, 640 pixels wide and at most 1 MiB combined, through the same native image content. Speech transcripts supplied by the source remain in incoming context. The audited native CLI accepts text and images, not raw video/audio; sampled frames do not capture every motion. Scanned PDFs with no text layer need OCR and return a clear limitation.
|
package/docs/upgrades.md
CHANGED
|
@@ -16,6 +16,8 @@ automatic channel for core and plugins without a saved policy. Existing explicit
|
|
|
16
16
|
stable or manual policies are preserved. The owner may select either per target.
|
|
17
17
|
The main target and installed plugins version independently.
|
|
18
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
|
+
|
|
19
21
|
The owner chooses the saved update policy. The host supervisor owns
|
|
20
22
|
interruption-safe replacement. There is one host service per agent, not a second
|
|
21
23
|
agent or an additional updater daemon. Its replaceable child runs the normal CLI
|
|
@@ -91,6 +93,12 @@ same candidate again and apply the new job; do not edit the old receipt. Only
|
|
|
91
93
|
Finish the requesting turn after queuing either operation. Verify the main relay
|
|
92
94
|
before requesting a separate plugin upgrade.
|
|
93
95
|
|
|
96
|
+
Codex version discovery parses machine-readable stdout separately from package
|
|
97
|
+
manager notices on stderr. Invalid stdout fails with a named discovery error;
|
|
98
|
+
it is never repaired by stripping arbitrary lines. A historical generic JSON
|
|
99
|
+
parse error alone does not establish which command or output stream caused it.
|
|
100
|
+
Reconcile the job's completed stages and installed state before retrying.
|
|
101
|
+
|
|
94
102
|
## Agent interface
|
|
95
103
|
|
|
96
104
|
Telegram `/status` shows the relay's loaded version, fresh host version and
|
|
@@ -232,7 +240,7 @@ Upgrading does not invent an explicit setting for an omitted field.
|
|
|
232
240
|
Verify upgrade from the previous supported artifact, retained identity/state,
|
|
233
241
|
failed-health rollback, and rejection of incompatible candidates. Main runtime
|
|
234
242
|
changes also need supervisor restart and requesting-process-exit tests. Run
|
|
235
|
-
`pnpm verify`, `
|
|
243
|
+
`pnpm verify`, `pnpm run release:check`, packed Docker builds and
|
|
236
244
|
`node docker/upgrade-smoke.mjs` with a local `EZ_WHATSAPP_SOURCE` containing the
|
|
237
245
|
WhatsApp fixture. The smoke uses synthetic transport only. VM installation,
|
|
238
246
|
agent-led upgrades, restart and real account acceptance remain separate QA gates.
|
|
@@ -283,3 +291,32 @@ the host or create a repair task. An automatically failed or rolled-back version
|
|
|
283
291
|
is not retried until a newer version appears or the owner explicitly retries.
|
|
284
292
|
Inspect `ez updates check` and `ez updates status` for target diagnostics.
|
|
285
293
|
Actual update transactions retain their existing admission, drain and rollback rules.
|
|
294
|
+
|
|
295
|
+
### Isolated broker readiness
|
|
296
|
+
|
|
297
|
+
Isolated activation and rollback explicitly recreate only `plugin-broker` with
|
|
298
|
+
`--no-deps --force-recreate --no-build --pull never --wait`. Plugin-only refresh
|
|
299
|
+
pins the installed broker image ID; main activation uses the newly built image
|
|
300
|
+
and rollback uses the recorded old image. This refreshes the narrow read-only
|
|
301
|
+
host-configuration file mount after atomic replacement. The relay and native
|
|
302
|
+
sessions are preserved during plugin-only refresh. Host-capable updates do not
|
|
303
|
+
start a broker. Existing update admission and drain remain authoritative.
|
|
304
|
+
|
|
305
|
+
Socket health is liveness only. Before reporting completion, the updater runs
|
|
306
|
+
`docker/broker-readiness.mjs` inside the actual broker, with expected SHA-256
|
|
307
|
+
fingerprints of the host binding, registry and registry configuration. This
|
|
308
|
+
bounded diagnostic checks socket existence, agent/registry ownership, packaged
|
|
309
|
+
source identity, declared command routes, folder grants and reviewed network
|
|
310
|
+
pins. It reads state without rewriting Compose files, executing a plugin,
|
|
311
|
+
creating authorization or supplying `EZ_RUN_ID`. Fingerprints are checked again
|
|
312
|
+
after validation and on the host; a changed binding fails verification.
|
|
313
|
+
`brokerReadiness` and `rollbackBrokerReadiness` in update status are structural
|
|
314
|
+
readbacks at that transaction, not continuous health or native invocation proof.
|
|
315
|
+
A failed refresh or probe enters the existing rollback/recovery path.
|
|
316
|
+
|
|
317
|
+
A genuine authorized native invocation through the bound launcher, with its real
|
|
318
|
+
receipt, remains separate acceptance evidence. Structural readiness and Docker
|
|
319
|
+
health cannot prove provider delivery or a successful native command. Do not
|
|
320
|
+
invent a run ID for that evidence. The synthetic regression is
|
|
321
|
+
`EZ_RELAY_IMAGE=<exact runtime image ID> node docker/broker-update-smoke.mjs`;
|
|
322
|
+
it uses disposable Linux state, no credentials or provider calls.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Plugin-backed scheduled preflight
|
|
2
|
+
|
|
3
|
+
Empty or unchanged provider work can skip a scheduled session. Eligible work uses the schedule's saved execution settings. Core owns admission; installed read-only plugin commands own provider eligibility. Preflight requires the bound plugin broker used by isolated deployments; unavailable broker or provider reads fail closed.
|
|
4
|
+
|
|
5
|
+
A schedule's `preflight` is `{ "on": "eligible" | "changed", "checks": [{ "alias": "registered-read-only-command", "args": ["literal", "arguments"] }] }`.
|
|
6
|
+
The plugin emits `{ "schemaVersion": 1, "eligible": boolean, "fingerprint": "sha256 hex", "observedAt": "ISO timestamp", "count": integer }`.
|
|
7
|
+
Any check eligible admits the owning saved task; changed mode additionally requires a new combined fingerprint. No domain rules or DAG execution live in core. Empty/unchanged checks advance the ordinary schedule cursor without a run/session. Errors retain a sanitized preflight receipt and skip the occurrence; no empty-success fallback.
|
|
8
|
+
|
|
9
|
+
Use `--preflight-file FILE` when creating/editing a native task. Editing preserves its condition unless explicitly replaced; `--clear-preflight` removes it. Preflight receipts remain on the native schedule cursor outside the agent workspace. Manual triggers run the same gate; an ineligible manual request reports unmet preflight and creates no run.
|
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.48",
|
|
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,45 @@ 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
|
+
// Claude has no catalog command; its installed help is the native metadata for
|
|
150
|
+
// the selectable model aliases and effort levels. No session or inference runs.
|
|
151
|
+
// Unparseable or unavailable help keeps the client default, never guessed names.
|
|
152
|
+
export const claudeCatalogModels = async (run?: (args: string[]) => Promise<string>): Promise<ModelChoice[]> => {
|
|
153
|
+
const fallback = [{ cli: 'claude', name: 'claude · client default', efforts: [] as string[] }]
|
|
154
|
+
let help: string
|
|
155
|
+
try {
|
|
156
|
+
help = run ? await run(['--help']) : await (async () => {
|
|
157
|
+
const invocation = executorInvocation('claude', ['--help'])
|
|
158
|
+
return (await promisify(execFile)(invocation.command, invocation.args, { env: executorEnvironment(), timeout: 8000, maxBuffer: 1024 * 1024 })).stdout
|
|
159
|
+
})()
|
|
160
|
+
} catch { return fallback }
|
|
161
|
+
const option = (flag: string) => help.split(/\r?\n(?=\s*(?:-\w, )?--)/).find((block) => block.trimStart().replace(/^-\w, /, '').startsWith(`${flag} `))?.replace(/\s+/g, ' ') ?? ''
|
|
162
|
+
const aliases = [...(option('--model').match(/\(e\.g\. ([^)]*)\)/)?.[1] ?? '').matchAll(/'([a-z][a-z0-9-]{0,31})'/g)].map((match) => match[1])
|
|
163
|
+
const levels = (option('--effort').match(/\(([a-z][a-z0-9, -]*)\)\s*$/)?.[1] ?? '').split(',').map((level) => level.trim())
|
|
164
|
+
.filter((level) => safe(level) && allowedEffort(level, undefined, 'claude'))
|
|
165
|
+
if (!aliases.length) return fallback
|
|
166
|
+
return [...fallback, ...[...new Set(aliases)].map((alias) => ({ cli: 'claude', model: alias, name: `claude · ${alias}`, efforts: levels }))]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
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>, claudeRunner?: (args: string[]) => Promise<string>): Promise<ModelChoice[]> => {
|
|
132
170
|
const models: ModelChoice[] = []
|
|
133
171
|
const record = (value: unknown): Record<string, unknown> =>
|
|
134
172
|
value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
|
@@ -147,7 +185,7 @@ export const readModels = async (home = homedir(), available = installed, codexH
|
|
|
147
185
|
}
|
|
148
186
|
}
|
|
149
187
|
if (await available('codex')) {
|
|
150
|
-
const cache = await
|
|
188
|
+
const cache = record(await readCodexCatalog(codexHome, codexRunner))
|
|
151
189
|
for (const entry of Array.isArray(cache.models) ? cache.models : []) {
|
|
152
190
|
const info = record(entry)
|
|
153
191
|
if (info.visibility !== 'list' || !safe(info.slug)) continue
|
|
@@ -161,8 +199,7 @@ export const readModels = async (home = homedir(), available = installed, codexH
|
|
|
161
199
|
}))
|
|
162
200
|
models.push(...(desktop.length ? desktop : [{ cli: 'codex-gui', name: 'codex-gui · desktop', efforts: [] }]))
|
|
163
201
|
}
|
|
164
|
-
|
|
165
|
-
if (await available('claude')) models.push({ cli: 'claude', name: 'claude · client default', efforts: [] })
|
|
202
|
+
if (await available('claude')) models.push(...await claudeCatalogModels(claudeRunner))
|
|
166
203
|
const allow = opencodeAllowlist ?? opencodeProviderAllowlist()
|
|
167
204
|
if (await available('opencode')) models.push(...await opencodeCatalogModels(opencodeRunner, opencodeDataHome, allow))
|
|
168
205
|
// Pi has separate provider configuration. OpenCode's catalog cannot
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
2
2
|
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto'
|
|
3
|
-
import { mkdir, readFile, writeFile, rename, open, unlink } from 'node:fs/promises'
|
|
3
|
+
import { mkdir, readFile, writeFile, rename, open, unlink, stat, chown } from 'node:fs/promises'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { ControlStore, sessionTitle, telegramOwner, type ControlGuard, type Owner, sameOwner, validOwner, ownerId, ownerEpoch } from './control-state.js'
|
|
6
6
|
import { RunStore, type RunRecord, type OutboxItem } from './runs.js'
|
|
@@ -16,7 +16,11 @@ import { SpeechCreditsDepletedError } from './audio.js'
|
|
|
16
16
|
|
|
17
17
|
const hash = (value: string) => createHash('sha256').update(value).digest('hex')
|
|
18
18
|
export const applicationScope = (bindingId: string, scope: string) => hash(JSON.stringify([bindingId, scope]))
|
|
19
|
-
type
|
|
19
|
+
export type TaskLaunch = { command: string; args: string[]; tasks: "all" | string[] }
|
|
20
|
+
export const validTaskLaunch = (v: unknown): v is TaskLaunch => {
|
|
21
|
+
const x=v as TaskLaunch; return !!x && Object.keys(x).every(k=>["command","args","tasks"].includes(k)) && /^[a-zA-Z0-9_-]{1,100}$/.test(x.command) && Array.isArray(x.args) && x.args.length>0 && x.args.length<=8 && (x.tasks==="all" || (Array.isArray(x.tasks) && x.tasks.length>0 && x.tasks.length<=128 && x.tasks.every(t=>/^task_[a-f0-9]{32}$/.test(t)))) && x.args.every(a=>typeof a==="string" && a.length<=200 && !/[\x00-\x1f\x7f]/.test(a))
|
|
22
|
+
}
|
|
23
|
+
export type Binding = { id: string; bindingId: string; tokenHash: string; owner: Owner; shareTelegram?: boolean; taskLaunch?: TaskLaunch }
|
|
20
24
|
|
|
21
25
|
export function validateApplicationRegistration(id: string, token: string | null): void {
|
|
22
26
|
if (!applicationId(id) || (token !== null && !/^[A-Za-z0-9_-]{43,200}$/.test(token))) throw new Error('Use a simple application ID and a random token of at least 256 bits encoded as base64url')
|
|
@@ -27,7 +31,7 @@ export class ApplicationBindings {
|
|
|
27
31
|
async list(): Promise<Binding[]> {
|
|
28
32
|
try {
|
|
29
33
|
const bindings = JSON.parse(await readFile(join(this.controlDir, 'application-bindings.json'), 'utf8'))
|
|
30
|
-
if (!Array.isArray(bindings) || bindings.some(binding => !applicationId(binding?.id) || !/^[a-f0-9-]{36}$/.test(binding.bindingId) || !/^[a-f0-9]{64}$/.test(binding.tokenHash) || !validOwner(binding.owner) || (binding.shareTelegram !== undefined && typeof binding.shareTelegram !== 'boolean')) || ['tokenHash','id','bindingId'].some(key => new Set(bindings.map(b => b[key])).size !== bindings.length)) throw new Error('Invalid application binding state')
|
|
34
|
+
if (!Array.isArray(bindings) || bindings.some(binding => !applicationId(binding?.id) || !/^[a-f0-9-]{36}$/.test(binding.bindingId) || !/^[a-f0-9]{64}$/.test(binding.tokenHash) || !validOwner(binding.owner) || (binding.taskLaunch !== undefined && !validTaskLaunch(binding.taskLaunch)) || (binding.shareTelegram !== undefined && typeof binding.shareTelegram !== 'boolean')) || ['tokenHash','id','bindingId'].some(key => new Set(bindings.map(b => b[key])).size !== bindings.length)) throw new Error('Invalid application binding state')
|
|
31
35
|
return bindings
|
|
32
36
|
} catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error }
|
|
33
37
|
}
|
|
@@ -51,6 +55,23 @@ export class ApplicationBindings {
|
|
|
51
55
|
return binding
|
|
52
56
|
} finally { await lock.close(); await unlink(`${file}.lock`) }
|
|
53
57
|
}
|
|
58
|
+
async taskLaunch(id: string, launch: TaskLaunch | null): Promise<void> {
|
|
59
|
+
if (launch !== null && !validTaskLaunch(launch)) throw Error('Invalid task launch command')
|
|
60
|
+
const file=join(this.controlDir,'application-bindings.json'), lock=await open(`${file}.lock`,'wx',0o600)
|
|
61
|
+
try {
|
|
62
|
+
const bindings=await this.list(), binding=bindings.find(b=>b.id===id)
|
|
63
|
+
const owner=(await new ControlStore(this.controlDir,900000).status()).owner
|
|
64
|
+
if(!binding || !sameOwner(binding.owner,owner))throw Error('Application authority revoked')
|
|
65
|
+
if(launch)binding.taskLaunch=launch; else delete binding.taskLaunch
|
|
66
|
+
const previous=await stat(file),tmp=`${file}.${randomUUID()}.tmp`
|
|
67
|
+
try {
|
|
68
|
+
await writeFile(tmp,JSON.stringify(bindings),{mode:0o600,flag:'wx'})
|
|
69
|
+
const created=await stat(tmp)
|
|
70
|
+
if(created.uid!==previous.uid || created.gid!==previous.gid)await chown(tmp,previous.uid,previous.gid)
|
|
71
|
+
await rename(tmp,file)
|
|
72
|
+
}finally{await unlink(tmp).catch(error=>{if((error as NodeJS.ErrnoException).code!=='ENOENT')throw error})}
|
|
73
|
+
}finally{await lock.close();await unlink(`${file}.lock`)}
|
|
74
|
+
}
|
|
54
75
|
async authenticate(token: string): Promise<Binding> {
|
|
55
76
|
if (!/^[A-Za-z0-9_-]{43,200}$/.test(token)) throw new Error('Unauthorized application')
|
|
56
77
|
const fingerprint = Buffer.from(hash(token), 'hex')
|
|
@@ -65,6 +86,7 @@ export class ApplicationBindings {
|
|
|
65
86
|
const owner = (await new ControlStore(this.controlDir, 900000).status()).owner
|
|
66
87
|
const binding = (await this.list()).find(item => item.bindingId === origin.bindingId)
|
|
67
88
|
if (!binding || !sameOwner(binding.owner, owner) || !ownsRun(owner, run)) throw new Error('Application authority revoked')
|
|
89
|
+
if(run.application?.taskAccess)await new Tasks(this.controlDir).applicationAccess(run.application.taskAccess,binding.bindingId)
|
|
68
90
|
}
|
|
69
91
|
}
|
|
70
92
|
|
|
@@ -224,6 +246,17 @@ export class ApplicationChannel {
|
|
|
224
246
|
if ('followTelegram' in rest) throw new Error('Invalid application request: choose one conversation option')
|
|
225
247
|
input = {...rest, followTelegram: followOwner}
|
|
226
248
|
}
|
|
249
|
+
if(input && typeof input==='object' && 'taskToken' in input) {
|
|
250
|
+
const v=input as Record<string,unknown>
|
|
251
|
+
if(Object.keys(v).some(k=>!['requestId','scope','text','taskToken'].includes(k)) || !applicationId(v.requestId) || typeof v.text!=='string' || !v.text.trim() || v.text.length>16000 || typeof v.taskToken!=='string' || !/^[a-f0-9]{64}$/.test(v.taskToken))throw Error('Invalid application task request')
|
|
252
|
+
const ticketHash=hash(v.taskToken), access=await new Tasks(this.options.controlDir).applicationAccess(ticketHash,bindingId)
|
|
253
|
+
const scope='task:'+access.task.id
|
|
254
|
+
if(v.scope!==scope)throw Error('Invalid application task scope')
|
|
255
|
+
const id=`r_app_${hash(JSON.stringify([bindingId,v.requestId]))}`, existing=await this.runs.get(id)
|
|
256
|
+
if(existing){await this.bindings.authorize(existing);if(existing.application?.taskAccess!==ticketHash || existing.texts[0]!==v.text || existing.taskId!==access.task.id)throw Error('Application request ID conflicts with prior task');return existing}
|
|
257
|
+
const run=await this.runs.create({id,ownerId:ownerId(access.task.owner),ownerEpoch:ownerEpoch(access.task.owner),taskId:access.task.id,texts:[v.text],execution:access.execution,application:{bindingId,scope,requestId:v.requestId,taskAccess:ticketHash}})
|
|
258
|
+
this.options.wake();return run
|
|
259
|
+
}
|
|
227
260
|
const value = input as { requestId?: unknown; scope?: unknown; text?: unknown; attachment?: { name?: unknown; data?: unknown }; context?: Record<string, unknown>; expectedNativeSessionId?: unknown; activateTelegram?: unknown; followTelegram?: unknown; ai?: { cli?: unknown; provider?: unknown; model?: unknown; effort?: unknown } }
|
|
228
261
|
if (!value || !applicationId(value.requestId) || !applicationId(value.scope) || typeof value.text !== 'string' || (!value.text.trim() && value.attachment === undefined) || value.text.length > 16000 || Object.keys(value).some(key => !['requestId','scope','text','attachment','context','expectedNativeSessionId','activateTelegram','followTelegram','ai'].includes(key))) throw new Error('Invalid application request')
|
|
229
262
|
if (value.expectedNativeSessionId !== undefined && (typeof value.expectedNativeSessionId !== 'string' || !/^[a-zA-Z0-9_-]{1,160}$/.test(value.expectedNativeSessionId))) throw new Error('Invalid application native session assertion')
|
|
@@ -299,6 +332,13 @@ export class ApplicationChannel {
|
|
|
299
332
|
if (request.method === 'GET' && path === '/v1/registration') {
|
|
300
333
|
send(200, {ownerId: ownerId(binding.owner), bindingId: binding.bindingId, channel: binding.id}); return
|
|
301
334
|
}
|
|
335
|
+
if(request.method==='POST' && path==='/v1/registration') {
|
|
336
|
+
let raw='';for await(const chunk of request){raw+=chunk;if(Buffer.byteLength(raw)>1024)throw Error('Invalid application task request')}
|
|
337
|
+
const v=JSON.parse(raw)
|
|
338
|
+
if(Object.keys(v).length!==1 || typeof v.taskToken!=='string' || !/^[a-f0-9]{64}$/.test(v.taskToken))throw Error('Invalid application task request')
|
|
339
|
+
const access=await new Tasks(this.options.controlDir).applicationAccess(hash(v.taskToken),binding.bindingId)
|
|
340
|
+
send(200,{taskId:access.task.id,expiresAt:access.expiresAt,purpose:access.task.purpose});return
|
|
341
|
+
}
|
|
302
342
|
if (path === '/v1/telegram' && request.method === 'GET') {
|
|
303
343
|
if ([...url.searchParams.keys()].length) throw new Error('Invalid application Telegram request')
|
|
304
344
|
if (!binding.shareTelegram) { send(403, { error: 'Application authority does not permit Telegram connection' }); return }
|
package/src/application-cli.ts
CHANGED
|
@@ -8,11 +8,12 @@ import { applicationId } from './application-origin.js'
|
|
|
8
8
|
|
|
9
9
|
async function main() {
|
|
10
10
|
const { values } = parseArgs({ options: {
|
|
11
|
+
'task-launch-command': {type:'string'}, 'task-launch-args':{type:'string'}, 'task-launch-tasks':{type:'string'}, 'disable-task-launch':{type:'boolean'},
|
|
11
12
|
owner: {type:'string'}, id: {type:'string'}, 'token-file': {type:'string'}, revoke: {type:'boolean'}, list: {type:'boolean'}, help: {type:'boolean'},
|
|
12
13
|
'owner-id': {type:'string'}, rotate: {type:'boolean'}, 'share-owner': {type:'boolean'}, 'share-active': {type:'boolean'},
|
|
13
14
|
'import-scope': {type:'string'}, 'native-session': {type:'string'}, cli: {type:'string'}, 'share-telegram': {type:'boolean'},
|
|
14
15
|
} })
|
|
15
|
-
if (values.help) { console.log('ezenciel-agents-application --id CHANNEL --token-file PRIVATE_FILE [--owner-id VERIFIED_OWNER_ID] [--share-owner] [--rotate] | --id CHANNEL --revoke | --list | --id CHANNEL --import-scope SCOPE --native-session ID --cli CLI'); return }
|
|
16
|
+
if (values.help) { console.log('ezenciel-agents-application --id CHANNEL --token-file PRIVATE_FILE [--owner-id VERIFIED_OWNER_ID] [--share-owner] [--rotate] | --id CHANNEL --task-launch-command COMMAND --task-launch-args JSON --task-launch-tasks JSON | --id CHANNEL --disable-task-launch | --id CHANNEL --revoke | --list | --id CHANNEL --import-scope SCOPE --native-session ID --cli CLI'); return }
|
|
16
17
|
if (process.env.EZ_RUN_ID) throw new Error('Application authority is configured by the installing administrator outside agent turns')
|
|
17
18
|
const config = loadControlConfig(), control = new ControlStore(config.controlDir, config.pairingTtlMs)
|
|
18
19
|
if (values.owner && values['owner-id']) throw new Error('Choose one owner registration form')
|
|
@@ -25,6 +26,11 @@ async function main() {
|
|
|
25
26
|
const bindings = new ApplicationBindings(config.controlDir)
|
|
26
27
|
if (values.list) { console.log(JSON.stringify((await bindings.list()).map(({id,bindingId}) => ({id,bindingId})))); return }
|
|
27
28
|
if (!values.id || !applicationId(values.id)) throw new Error('Application ID required')
|
|
29
|
+
if(values['task-launch-command'] || values['disable-task-launch']) {
|
|
30
|
+
if(values['token-file'] || values.revoke || values.rotate || values['import-scope'] || (values['task-launch-command'] && values['disable-task-launch']))throw Error('Task launch settings cannot be combined with registration')
|
|
31
|
+
await bindings.taskLaunch(values.id,values['disable-task-launch'] ? null : {command:values['task-launch-command']!,args:JSON.parse(values['task-launch-args'] ?? '[]'),tasks:JSON.parse(values['task-launch-tasks'] ?? 'null')})
|
|
32
|
+
console.log(JSON.stringify({ok:true,id:values.id,taskLaunch:!values['disable-task-launch']}));return
|
|
33
|
+
}
|
|
28
34
|
if (values['import-scope']) {
|
|
29
35
|
const binding = (await bindings.list()).find(item => item.id === values.id)
|
|
30
36
|
if (!binding || !sameOwner(binding.owner, owner) || !applicationId(values['import-scope']) || !values['native-session'] || !/^[a-zA-Z0-9_-]{1,160}$/.test(values['native-session']) || !values.cli) throw new Error('Current application binding, scope, native session and CLI required')
|
|
@@ -2,6 +2,7 @@ export type ApplicationOrigin = {
|
|
|
2
2
|
bindingId: string
|
|
3
3
|
scope: string
|
|
4
4
|
requestId: string
|
|
5
|
+
taskAccess?: string
|
|
5
6
|
followTelegram?: boolean
|
|
6
7
|
inputText?: string
|
|
7
8
|
attachmentHash?: string
|
|
@@ -12,6 +13,7 @@ export const applicationId = (value: unknown): value is string =>
|
|
|
12
13
|
export const validApplicationOrigin = (value: unknown): value is ApplicationOrigin => {
|
|
13
14
|
const origin = value as ApplicationOrigin | undefined
|
|
14
15
|
return !!origin && /^[a-f0-9-]{36}$/.test(origin.bindingId) && applicationId(origin.scope) && applicationId(origin.requestId) &&
|
|
16
|
+
(origin.taskAccess === undefined || /^[a-f0-9]{64}$/.test(origin.taskAccess)) &&
|
|
15
17
|
(origin.followTelegram === undefined || typeof origin.followTelegram === 'boolean') &&
|
|
16
18
|
(origin.inputText === undefined || (typeof origin.inputText === 'string' && origin.inputText.length <= 16000)) &&
|
|
17
19
|
(origin.attachmentHash === undefined || /^[a-f0-9]{64}$/.test(origin.attachmentHash)) &&
|