@jc_stack/ez-agents 0.1.0-beta.12 → 0.1.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +1 -0
- package/.env.example +1 -1
- package/AGENTS.md +10 -1
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +3 -0
- package/README.md +112 -10
- package/SECURITY.md +7 -1
- package/bin/ezenciel-agents-schedule +2 -0
- package/bin/ezenciel-agents-schedule.mjs +16 -0
- package/bin/ezenciel-agents-task +2 -0
- package/bin/ezenciel-agents-task.mjs +16 -0
- package/compose.yaml +2 -1
- package/docker/recovery.ts +2 -2
- package/docker/run.ts +2 -2
- package/docs/architecture/authority-boundaries.md +114 -12
- package/docs/architecture/event-sources.md +12 -7
- package/docs/channel-backend.md +36 -0
- package/docs/local-qa.md +45 -0
- package/docs/plugin-catalog.md +54 -0
- package/docs/plugin-contributions.md +3 -0
- package/docs/plugins.md +49 -0
- package/docs/scheduling.md +127 -0
- package/docs/selective-monitoring.md +106 -0
- package/docs/setup.md +7 -0
- package/docs/standalone-cli.md +62 -0
- package/package.json +7 -2
- package/scripts/smoke-scheduler.ts +90 -0
- package/scripts/stage-qa.mjs +42 -0
- package/src/channel-backend.ts +46 -0
- package/src/codex-session.ts +96 -0
- package/src/config.ts +6 -1
- package/src/desktop-bridge.ts +29 -11
- package/src/execution-authority.ts +24 -0
- package/src/executor.ts +66 -15
- package/src/host-executor.ts +30 -10
- package/src/inbox.ts +4 -0
- package/src/index.ts +130 -34
- package/src/plugins/exposure.mjs +13 -0
- package/src/plugins/manager.mjs +27 -12
- package/src/process-tree.ts +33 -0
- package/src/runs.ts +50 -17
- package/src/schedule-cli.ts +69 -0
- package/src/schedule-time.ts +85 -0
- package/src/scheduler.ts +121 -0
- package/src/source-cli.ts +1 -1
- package/src/task-cli.ts +16 -0
- package/src/task-executor.ts +63 -0
- package/src/task-mcp.ts +36 -0
- package/src/task-rpc.ts +45 -0
- package/src/task-workspace.ts +22 -0
- package/src/tasks.ts +192 -0
- package/src/updates/binding.mjs +1 -0
- package/src/updates/status.mjs +7 -1
- package/templates/agent/TOOLS.md +54 -1
- package/templates/standalone-tools.md +20 -0
- package/test/channel-backend.test.ts +100 -0
- package/test/codex-context.test.ts +36 -1
- package/test/codex-session.test.ts +49 -0
- package/test/config.test.ts +2 -2
- package/test/desktop-bridge.test.ts +19 -0
- package/test/event-sources.test.ts +47 -11
- package/test/execution-authority.test.ts +42 -0
- package/test/executor.test.ts +42 -1
- package/test/helpers/owner-run.ts +13 -0
- package/test/host-executor.test.ts +9 -3
- package/test/local-qa.test.mjs +38 -0
- package/test/plugin-manager.test.mjs +70 -1
- package/test/schedule-cli.test.ts +49 -0
- package/test/scheduler-host.test.ts +55 -0
- package/test/scheduler-relay.test.ts +67 -0
- package/test/scheduler.test.ts +104 -0
- package/test/task-native.test.ts +87 -0
- package/test/tasks.test.ts +179 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Available plugins
|
|
2
|
+
|
|
3
|
+
This is Ez's public catalog of released plugins, for business owners and agents
|
|
4
|
+
discovering capabilities. Each entry links to the plugin's own repository,
|
|
5
|
+
installation instructions and releases. Install only the plugins needed for the owner's requested work.
|
|
6
|
+
|
|
7
|
+
| Plugin | Capability | Package | Release |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| [WhatsApp](https://github.com/jdorado/ez-whatsapp) | Link an existing WhatsApp account so the agent can use its messaging tools. Uses WhatsApp Web linked devices through Baileys; requires an account already on a phone. | `@jc_stack/ez-whatsapp` | [0.1.0-beta.12](https://github.com/jdorado/ez-whatsapp/releases/tag/v0.1.0-beta.12) — testing beta |
|
|
10
|
+
| [Composio](https://github.com/jdorado/ez_composio) | Discover integrations and full native tool schemas, connect requested accounts and perform authorized actions through a private broker. Requires a Composio project key; individual apps may require OAuth consent. | `@jc_stack/ez-composio` | [0.1.0-beta.1](https://github.com/jdorado/ez_composio/releases/tag/v0.1.0-beta.1) — testing beta |
|
|
11
|
+
| [GitHub](https://github.com/jdorado/ez_github) | Create repositories, commit and push through native Git/gh with a private per-agent profile. Requires GitHub browser consent and Ez 0.1.0-beta.13 or newer. | `@jc_stack/ez-github` | [0.1.0-beta.1](https://github.com/jdorado/ez_github/releases/tag/v0.1.0-beta.1) — testing beta |
|
|
12
|
+
|
|
13
|
+
## Set up a plugin
|
|
14
|
+
|
|
15
|
+
For plugins used by an existing local CLI/GUI executor, follow
|
|
16
|
+
[standalone setup](standalone-cli.md). Telegram is not required.
|
|
17
|
+
|
|
18
|
+
For an autonomous Telegram assistant, finish [Ez setup](setup.md) and verify an
|
|
19
|
+
actual agent reply in Telegram first.
|
|
20
|
+
Then ask that assistant: **“Set up WhatsApp using
|
|
21
|
+
https://github.com/jdorado/ez-whatsapp.”** The installed agent follows the plugin's
|
|
22
|
+
README, handles package inspection and installation, delivers the linking QR,
|
|
23
|
+
and verifies the connected account. The owner completes the phone linking step.
|
|
24
|
+
Account setup does not authorize sending messages to other people.
|
|
25
|
+
|
|
26
|
+
For installing agents: use the exact scoped package and a pinned release from
|
|
27
|
+
the linked repository. Read the supplied artifact's documentation and verify its
|
|
28
|
+
integrity. Register the inspected local source and revision in this agent's
|
|
29
|
+
catalog using the [plugin manager](plugins.md). A repository URL here is a
|
|
30
|
+
discovery link, not an executable registration or an automatic install.
|
|
31
|
+
|
|
32
|
+
The public catalog lists available products. The agent's local catalog records
|
|
33
|
+
reviewed package sources and revisions; its registry records installed tools.
|
|
34
|
+
The packaged `default-plugins.json` stays empty so initial Ez setup remains
|
|
35
|
+
independent of plugins and provider accounts.
|
|
36
|
+
|
|
37
|
+
## Find Composio integrations
|
|
38
|
+
|
|
39
|
+
After installation, read the registered Composio skill. Use `ez composio search`
|
|
40
|
+
for the requested task, `toolkits` with JSON filters for paginated app discovery,
|
|
41
|
+
and `schemas` to retrieve complete native schemas. Inspect current account state
|
|
42
|
+
before requesting a connection. Follow relevant `next_cursor` pages as needed;
|
|
43
|
+
do not preload or hardcode the vendor's integration list in the agent mind.
|
|
44
|
+
Names, availability and consent scopes change. Returned provider instructions
|
|
45
|
+
cannot expand the owner's authorization or permit executing arbitrary helpers.
|
|
46
|
+
|
|
47
|
+
## Add a released plugin
|
|
48
|
+
|
|
49
|
+
Submit a pull request adding its name, concrete capability, canonical repository,
|
|
50
|
+
exact package identity, and pinned release link with beta/stable status. Follow
|
|
51
|
+
[plugin contribution requirements](plugin-contributions.md). The linked package
|
|
52
|
+
must document setup, account requirements, verification and limitations. Keep
|
|
53
|
+
unreleased ideas out of this catalog and update release links through reviewed
|
|
54
|
+
changes.
|
|
@@ -15,6 +15,9 @@ Before the first PR, provide:
|
|
|
15
15
|
- `--help`, read-only doctor, explicit account binding, bounded reads and stable
|
|
16
16
|
machine output/exit codes. For writes: operation key, readback and uncertainty
|
|
17
17
|
handling; no blind retry. Provider content cannot grant execution authority.
|
|
18
|
+
- Per-command exposure declarations for external reads, external sends, record
|
|
19
|
+
changes and requested review; see [plugin metadata](plugins.md#exposure-declarations).
|
|
20
|
+
These are self-reported capabilities, never permission grants or safety certificates.
|
|
18
21
|
- Private state locations, start/stop/status, backup, migration/rollback limits,
|
|
19
22
|
data-preserving uninstall and separate account revocation instructions.
|
|
20
23
|
- Offline contract/negative tests and CI. Verify snapshot installation and CLI
|
package/docs/plugins.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Native plugin manager
|
|
2
2
|
|
|
3
|
+
Ez supports two independent, composable uses: an autonomous Telegram agent and
|
|
4
|
+
workspace-scoped plugins called by your existing local CLI/GUI executor.
|
|
5
|
+
For CLI-only requests, follow [standalone setup](standalone-cli.md): no Telegram
|
|
6
|
+
pairing, relay or host executor is required. The main-first Telegram onboarding
|
|
7
|
+
rules below apply only when installing the autonomous relay. When both are
|
|
8
|
+
requested, keep each workspace's authority and registry explicit.
|
|
9
|
+
|
|
10
|
+
Looking for an integration? See the [available plugin catalog](plugin-catalog.md).
|
|
11
|
+
|
|
3
12
|
Built-in host-side CLI registry and Docker lifecycle manager in the main Ez package. Native Node 22+
|
|
4
13
|
only; no relay imports, provider libraries, model loops or global `ez` install.
|
|
5
14
|
The existing host executor uses an agent-bound launcher. This is the narrow host
|
|
@@ -130,6 +139,40 @@ and duplicate aliases fail. An interrupted manager leaves `registry.lock` with
|
|
|
130
139
|
its PID: verify that process is gone before explicitly removing that one lock.
|
|
131
140
|
Never remove an active lock or delete provider data to repair installation.
|
|
132
141
|
|
|
142
|
+
## Exposure declarations
|
|
143
|
+
|
|
144
|
+
Each command in `ez-plugin.json` can include an optional `exposure` object:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"executable": "bin/client.mjs",
|
|
149
|
+
"args": [],
|
|
150
|
+
"exposure": {
|
|
151
|
+
"receivesExternalContent": true,
|
|
152
|
+
"sendsExternally": true,
|
|
153
|
+
"changesRecords": true,
|
|
154
|
+
"requiresReview": true
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
These four fields are booleans. Unknown fields and invalid values are rejected.
|
|
160
|
+
Each omitted field defaults to true; legacy manifests remain installable with
|
|
161
|
+
conservative exposure. Declaration changes change the inspected content hash.
|
|
162
|
+
Use `ez plugins inspect <id>` before installation and `ez tools exposure` afterward
|
|
163
|
+
to see normalized declarations. Existing `ez tools list` output stays unchanged.
|
|
164
|
+
|
|
165
|
+
Describe capabilities, not a trust rank: CRM notes can contain customer-authored
|
|
166
|
+
text, while a channel can also modify records. `requiresReview` requests added
|
|
167
|
+
attention during setup/use; it does not enable an automated reviewer. Declaring
|
|
168
|
+
false never grants permissions, disables core checks or certifies a plugin safe.
|
|
169
|
+
The core owns authority; plugins own provider transport/authentication/receipts.
|
|
170
|
+
|
|
171
|
+
Owner adapters retain owner access. External correspondence can run only in an
|
|
172
|
+
approved core messaging task through the restricted task runner. Declarations
|
|
173
|
+
and monitoring subscriptions alone never grant task execution. See
|
|
174
|
+
[authority boundaries](architecture/authority-boundaries.md).
|
|
175
|
+
|
|
133
176
|
## Deployment descriptors
|
|
134
177
|
|
|
135
178
|
`ez-plugin.json` retains its v1 executable/args/skills contract.
|
|
@@ -179,3 +222,9 @@ non-destructive uninstall. No live account or recipient is used.
|
|
|
179
222
|
|
|
180
223
|
This beta includes owner-policy release checks and durable
|
|
181
224
|
main/plugin replacement. See [upgrade setup, tools and recovery](upgrades.md). Earlier main upgrade/rollback VM QA passed; final-release fresh-host/reboot and live plugin upgrade acceptance remain pending.
|
|
225
|
+
|
|
226
|
+
## Published catalog
|
|
227
|
+
|
|
228
|
+
Find released packages and the agent-owned registration path in the
|
|
229
|
+
[plugin catalog](plugin-catalog.md). This listing does not change the empty
|
|
230
|
+
default installation or connect provider accounts.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Scheduling and background work
|
|
2
|
+
|
|
3
|
+
Ask the agent naturally: “Remind me on September 9 next year at 09:00 Dubai time”
|
|
4
|
+
or “Every Tuesday at 09:00, prepare the report.” The agent translates this into a
|
|
5
|
+
core CLI request. No plugin, database or operating-system cron setup is required.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
ezenciel-agents-schedule create --name Reminder \
|
|
9
|
+
--at 2027-09-09T09:00:00+04:00 --text 'Remind the owner about the renewal.'
|
|
10
|
+
ezenciel-agents-schedule create --name Weekdays \
|
|
11
|
+
--cron '0 9 * * 1-5' --timezone Asia/Dubai --text 'Prepare the daily report.'
|
|
12
|
+
ezenciel-agents-schedule create --name Research --now \
|
|
13
|
+
--text '/goal Complete the authorized research objective. Save evidence, verify the outcome, and send the owner the result.'
|
|
14
|
+
ezenciel-agents-schedule list
|
|
15
|
+
ezenciel-agents-schedule runs
|
|
16
|
+
ezenciel-agents-schedule pause SCHEDULE_ID
|
|
17
|
+
ezenciel-agents-schedule resume SCHEDULE_ID
|
|
18
|
+
ezenciel-agents-schedule remove SCHEDULE_ID
|
|
19
|
+
ezenciel-agents-schedule cancel RUN_ID
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `--text-file` for longer instructions. `edit ID` replaces the complete schedule
|
|
23
|
+
with a new revision; use the same trigger/name/text flags as `create`.
|
|
24
|
+
Intervals use `--every-seconds` (minimum 60), optional `--start`, and optional
|
|
25
|
+
`--until`. Cron also supports start/end bounds. All absolute timestamps require
|
|
26
|
+
an explicit offset. `--now` starts on the next relay tick, not synchronously.
|
|
27
|
+
|
|
28
|
+
Cron accepts five numeric fields, comma lists, ranges and steps; Sunday is 0 or 7.
|
|
29
|
+
Restricted day-of-month and weekday fields use standard OR semantics. Set just
|
|
30
|
+
weekday for “Tuesdays.” Calendar jobs retain their explicit IANA timezone when
|
|
31
|
+
the host changes zones. Nonexistent DST wall times are skipped; repeated wall
|
|
32
|
+
times fire once, at the earlier instant. Search is bounded to eight years.
|
|
33
|
+
Public-holiday calendars and arbitrary RRULE syntax are not implemented.
|
|
34
|
+
|
|
35
|
+
## Execution and authority
|
|
36
|
+
|
|
37
|
+
The relay checks due work once per second. Each occurrence enters the durable
|
|
38
|
+
run queue with a stable ID. Background work runs in a fresh native CLI session
|
|
39
|
+
and `work/tasks/RUN_ID/`, with snapshots of the agent's SOUL, USER and TOOLS files.
|
|
40
|
+
Instructions must include any needed context or source paths; full chat history
|
|
41
|
+
is not copied. Task folders remain for inspection and artifact delivery.
|
|
42
|
+
|
|
43
|
+
One writer runs per task directory. Up to four background tasks can run alongside
|
|
44
|
+
the sequential main conversation. A recurring schedule has at most one pending
|
|
45
|
+
or active occurrence. Agents should delegate long work with `create --now`, return
|
|
46
|
+
to chat, and inspect `runs` or task progress when asked. Native subagents can be
|
|
47
|
+
used inside the worker. Sharing provider profiles does not make concurrent CRM,
|
|
48
|
+
file or browser writes safe: the agent must coordinate those resources.
|
|
49
|
+
|
|
50
|
+
Production relay/host execution has no wall-clock timeout. The old
|
|
51
|
+
`EZ_EXECUTOR_TIMEOUT_SECONDS` setting is ignored. Individual network/tool waits
|
|
52
|
+
still have their own limits; those are not overall task deadlines. Native goals
|
|
53
|
+
are an executor capability, configured through instructions. Ez has no goal API,
|
|
54
|
+
continuation loop or rule equating a process exit with goal achievement.
|
|
55
|
+
|
|
56
|
+
Scheduled Codex CLI tasks use a dedicated native app-server session, tested with
|
|
57
|
+
CLI 0.153.4. A leading `/goal` in the instruction text maps to the same native
|
|
58
|
+
goal command used by the interactive CLI. Codex automatically starts subsequent
|
|
59
|
+
turns; the transport stays connected until the native goal is complete or stops
|
|
60
|
+
for attention. It sends no continuation prompts and stores no Ez goal state.
|
|
61
|
+
Goals created by the agent's native tools also keep the session alive. Ordinary
|
|
62
|
+
tasks finish after their turn. A blocked, paused or limited goal is not reported
|
|
63
|
+
as successful. Native RPC requests have a response deadline; running tasks do not.
|
|
64
|
+
Each scheduled task has its own Codex state under `control/cli/codex/tasks/RUN_ID`,
|
|
65
|
+
with a snapshot of the agent's Codex configuration and the existing auth link.
|
|
66
|
+
Foreground chat and background tasks do not initialize or migrate one shared
|
|
67
|
+
native database concurrently.
|
|
68
|
+
|
|
69
|
+
The foreground chat still uses `codex exec`. That invocation exits after one
|
|
70
|
+
requested turn even if a goal is active, so delegate persistent work to the
|
|
71
|
+
scheduler. Desktop and other executor goal lifecycles need separate validation.
|
|
72
|
+
|
|
73
|
+
The CLI binds jobs to the paired owner and current AI selection. Queued/scheduled
|
|
74
|
+
work retains that selection after the chat switches AI. Revoking/re-pairing an
|
|
75
|
+
owner invalidates their old schedules, including re-pairing the same Telegram ID.
|
|
76
|
+
External event turns cannot use the scheduling CLI. Credentials still pass only
|
|
77
|
+
through the existing whitelist and installed host binding.
|
|
78
|
+
|
|
79
|
+
## Restart, pause and cancellation
|
|
80
|
+
|
|
81
|
+
Schedule definitions and cursors use protected atomic JSON files. A restart
|
|
82
|
+
between queue creation and cursor persistence does not duplicate an occurrence.
|
|
83
|
+
An overdue one-time job runs on return. Missed recurring occurrences coalesce into
|
|
84
|
+
one pending run; future runs resume at the next eligible time. A paused queued
|
|
85
|
+
occurrence waits for resume. Editing/removing a schedule invalidates its old
|
|
86
|
+
queued occurrence; already-running work continues until explicitly cancelled.
|
|
87
|
+
|
|
88
|
+
`/stop` stops all active work; `cancel RUN_ID` stops one background task. `/cancel`
|
|
89
|
+
clears queued work. Pause/remove a recurring schedule to prevent future runs.
|
|
90
|
+
Stopping the relay also stops its workers. A crashed or interrupted execution is
|
|
91
|
+
not automatically replayed. Runs found active at startup are marked failed with
|
|
92
|
+
`interrupted: true`; their schedule revision stays held until the agent inspects
|
|
93
|
+
the evidence and explicitly edits the schedule. Inspect the task's files, native session and delivery
|
|
94
|
+
receipts before deciding whether to resume. A clock cannot reconstruct an
|
|
95
|
+
in-flight process or prove whether an external side effect happened.
|
|
96
|
+
|
|
97
|
+
The host and relay must be online. Paused/completed schedule definitions and task
|
|
98
|
+
artifacts are retained. The agent sends through the normal Telegram outbox;
|
|
99
|
+
`completed` means executor exit, while provider delivery is recorded separately.
|
|
100
|
+
A timeout or ambiguous send must not cause blind replay of the whole task.
|
|
101
|
+
|
|
102
|
+
## QA
|
|
103
|
+
|
|
104
|
+
`pnpm verify` covers recurrence/DST, restart deduplication, authority revocation,
|
|
105
|
+
corrupt state, paths, cancellation and a chat response while a synthetic worker
|
|
106
|
+
is active. Docker tests exercise the packaged host transport and isolation.
|
|
107
|
+
|
|
108
|
+
For an opt-in real CLI probe (consumes model usage):
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
pnpm smoke:scheduler -- 1860 codex
|
|
112
|
+
# Native goal must finish a first turn and continue without another prompt:
|
|
113
|
+
pnpm smoke:scheduler -- 45 codex goal
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
This uses temporary workspaces and a synthetic Telegram provider, never live
|
|
117
|
+
contacts. A real CLI waits 31 minutes while a second invocation answers 17 × 19.
|
|
118
|
+
It checks responsiveness again after six minutes and requires the worker's final
|
|
119
|
+
message. Evidence is saved in the printed temporary directory. Use `75 codex`
|
|
120
|
+
for a shorter iteration; it does not prove the full duration.
|
|
121
|
+
|
|
122
|
+
Before enabling on a real installation, repeat through its Telegram bot: request
|
|
123
|
+
the 31-minute task, ask the arithmetic question and request status after six
|
|
124
|
+
minutes. Verify `finished.txt` and exactly one completion in Telegram. Separately
|
|
125
|
+
exercise cancellation, downtime catch-up and an explicitly requested native goal
|
|
126
|
+
that needs more than one turn. Synthetic provider evidence does not prove real
|
|
127
|
+
Telegram delivery, and a sleep test does not prove native goal persistence.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Selective monitoring and replies
|
|
2
|
+
|
|
3
|
+
An owner saying “monitor and answer messages from CONTACT” has selected that
|
|
4
|
+
contact and authorized ordinary replies within the stated scope. Do not ask
|
|
5
|
+
whether they meant selected contacts or demand a special phrase. “If they send,
|
|
6
|
+
reply” means wait for incoming messages; it does not authorize an opening send.
|
|
7
|
+
Resolve only missing identity or disclosure scope. A saved Markdown instruction
|
|
8
|
+
is a reminder, never an activated source, watch or reply permission.
|
|
9
|
+
|
|
10
|
+
## Speak in jobs, not modes
|
|
11
|
+
|
|
12
|
+
Keep mode names internal. Linking/syncing an account defaults to quiet capture;
|
|
13
|
+
finish connection verification, then at most one short optional prompt such as
|
|
14
|
+
“WhatsApp is connected. Want me to follow up with anyone?” Do not present a
|
|
15
|
+
technical three-mode menu or suggest blanket autonomous replies that v1 cannot
|
|
16
|
+
safely authorize. If the owner already gave a job, continue it without this prompt.
|
|
17
|
+
|
|
18
|
+
Infer the complete job from ordinary language:
|
|
19
|
+
|
|
20
|
+
- “Find out whether they have a table” or “Book me a restaurant”: send the inquiry,
|
|
21
|
+
watch replies from that contact, continue the multi-turn negotiation and report
|
|
22
|
+
the outcome. A send receipt is not completion. Do not ask if follow-up is wanted.
|
|
23
|
+
- Twelve inquiries mean twelve task-scoped contacts, not all-inbox monitoring.
|
|
24
|
+
Maintain one scoped task per correspondent; the same core checks apply to each.
|
|
25
|
+
- “Just send this; I will reply”: one-off send under the explicit owner instruction,
|
|
26
|
+
no new watch or conversational mandate. Use the provider's ordinary send and
|
|
27
|
+
receipt interface; do not create a messaging task that would auto-follow up.
|
|
28
|
+
- “Monitor this person and answer if they message”: incoming-only reply task;
|
|
29
|
+
no opening message and no unrelated contact attention.
|
|
30
|
+
- “Keep these messages for me”: quiet capture/read-on-request. If “monitor” alone
|
|
31
|
+
leaves action intent unclear, ask one plain question: “Should I reply for you,
|
|
32
|
+
or just keep the messages for you to review?” Do not ask when the job resolves it.
|
|
33
|
+
|
|
34
|
+
Apply the required core confirmation to the concrete proposal, not an extra
|
|
35
|
+
questionnaire. Use the owner's existing contact, purpose and disclosure limits.
|
|
36
|
+
Avoid claiming indefinite service where v1 is bounded. Explain the expiry only
|
|
37
|
+
when it matters to that proposed job; never silently expand or renew permission.
|
|
38
|
+
|
|
39
|
+
## Three capture modes, separate reply authority
|
|
40
|
+
|
|
41
|
+
- Manual: capture for explicit reads, with no general wake-up subscription.
|
|
42
|
+
- Selected: wake for named contacts only. Never turn on all-inbox mode to satisfy
|
|
43
|
+
a one-contact request.
|
|
44
|
+
- All: attention for all eligible incoming contacts; requires that broader
|
|
45
|
+
owner request. Attention alone never grants autonomous reply permission.
|
|
46
|
+
|
|
47
|
+
For an approved core reply task, the provider's task-watch supplies expiring
|
|
48
|
+
selected attention even if its general policy remains manual. That is expected:
|
|
49
|
+
verify the actual task watch and core grant, not only the general policy label.
|
|
50
|
+
Monitoring-only requests grant no sends. Unsupported wake-only reasoning modes
|
|
51
|
+
must be reported as unsupported, not run with owner authority.
|
|
52
|
+
|
|
53
|
+
## Complete the setup under the owner's request
|
|
54
|
+
|
|
55
|
+
Use the current installed plugin skill and `ez plugins list`/`status`/provider
|
|
56
|
+
doctor to confirm the existing linked account. Installation, source plumbing,
|
|
57
|
+
contact attention and permission are separate checks. A missing source is
|
|
58
|
+
technical work for the agent, not a reason to stop at “saved your instruction.”
|
|
59
|
+
Do not request a second provider instance or re-pair an already linked account.
|
|
60
|
+
|
|
61
|
+
The relay must be able to reach the registered provider service socket. Inspect
|
|
62
|
+
the registered deployment/compose, actual named IPC volume, and relay mounts.
|
|
63
|
+
For the supplied WhatsApp adapter the socket is /plugins/whatsapp/service.sock;
|
|
64
|
+
main compose.whatsapp.yaml supplies a read-only IPC/client override. Use the
|
|
65
|
+
registered plugin's real volume names, preserve existing docker.env/Compose
|
|
66
|
+
settings, and attach only its declared IPC/client volumes (never its profile or
|
|
67
|
+
credentials). Recreate the relay with the existing deployment after a required
|
|
68
|
+
mount change; verify it comes back. Do not expose the Docker socket to it.
|
|
69
|
+
|
|
70
|
+
Register the source INSIDE that relay container using the installed
|
|
71
|
+
`ezenciel-agents-source --name NAME --socket ABSOLUTE_SOCKET` command. Read back
|
|
72
|
+
registration and provider events-head account identity. A socket inside a
|
|
73
|
+
provider command container is not proof the relay can reach it. Do not edit core
|
|
74
|
+
source/grant JSON directly. Other adapters use their declared socket paths;
|
|
75
|
+
provider names do not change authority checks.
|
|
76
|
+
|
|
77
|
+
## Propose, confirm, verify
|
|
78
|
+
|
|
79
|
+
For an outbound job such as a booking, use `ezenciel-agents-task propose` WITHOUT
|
|
80
|
+
`--incoming-only`: it starts the inquiry and then watches replies. For “answer if
|
|
81
|
+
they message,” use `ezenciel-agents-task propose --incoming-only` with the
|
|
82
|
+
registered source, canonical contact, purpose, explicitly shareable context file
|
|
83
|
+
and expiry. No need to invent a booking objective: “conversational replies to
|
|
84
|
+
this contact, no private disclosures or commitments” is a legitimate purpose.
|
|
85
|
+
If no private facts may be shared, say so in the context file; do not include
|
|
86
|
+
owner memory. V1 supports at most 72 hours, not indefinite “until stopped.” Offer
|
|
87
|
+
that bounded duration in the exact approval, explaining the limit without asking
|
|
88
|
+
the owner to restate their request. Do not silently renew it.
|
|
89
|
+
|
|
90
|
+
The core presents the exact proposal for owner confirmation. Ordinary messages
|
|
91
|
+
inside that grant need no repeated confirmations. Incoming-only grants create
|
|
92
|
+
no initial run or opening message. They remain active across replies until expiry
|
|
93
|
+
or owner revocation; the worker cannot close a watch by calling `complete`.
|
|
94
|
+
After handling a message, save a task note and finish the run. After approval, verify active core state,
|
|
95
|
+
contact and expiry, source reachability, and the provider's task watch. With
|
|
96
|
+
`--incoming-only`, an empty conversation is correctly idle until a new message.
|
|
97
|
+
If approval is still pending, say pending; if setup failed, name the actual
|
|
98
|
+
failure and continue repair within scope. Never report “active” based on notes,
|
|
99
|
+
installation, connected status or subscription alone.
|
|
100
|
+
|
|
101
|
+
After an authorized test message, inspect the corresponding task run and send
|
|
102
|
+
receipt and compare recipient readback. An accepted send is not proof of delivery.
|
|
103
|
+
After revocation/expiry no subsequent reply may dispatch. Keep these checks
|
|
104
|
+
agent-owned: the owner supplies only necessary confirmation, QR scan if needed,
|
|
105
|
+
and the test contact/message. Do not bypass a missing grant by polling the inbox
|
|
106
|
+
in an unrestricted scheduled owner session or sending through the raw CLI.
|
package/docs/setup.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Agent-led setup
|
|
2
2
|
|
|
3
|
+
Ez supports two independent, composable uses: an autonomous Telegram agent and
|
|
4
|
+
workspace-scoped plugins called by your existing local CLI/GUI executor.
|
|
5
|
+
For CLI-only requests, follow [standalone setup](standalone-cli.md): no Telegram
|
|
6
|
+
pairing, relay or host executor is required. The main-first Telegram onboarding
|
|
7
|
+
rules below apply only when installing the autonomous relay. When both are
|
|
8
|
+
requested, keep each workspace's authority and registry explicit.
|
|
9
|
+
|
|
3
10
|
The user starts in their existing CLI and says “install Ezenciel”, then “create
|
|
4
11
|
an agent as my family shopper”. The installing agent does the technical work.
|
|
5
12
|
Keep that host CLI and login for every agent; do not request another CLI login.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Use Ez from your existing executor
|
|
2
|
+
|
|
3
|
+
The plugin manager and its Docker plugins work independently of the Telegram
|
|
4
|
+
relay. Use the same absolute launcher from any local executor to share a chosen
|
|
5
|
+
workspace's tools and accounts. The executor owns reasoning and conversation;
|
|
6
|
+
Ez owns the plugin registry and command transport. No provider-specific adapter,
|
|
7
|
+
new model login or GUI plugin installation is involved.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Install the main package and its dependencies in a permanent location using the
|
|
12
|
+
artifact steps in [setup](setup.md). Node 22+ is needed for the manager; Docker
|
|
13
|
+
and Compose are needed for executable plugins. Do not build/start the relay,
|
|
14
|
+
create a bot, pair an owner or configure a host executor for CLI-only use.
|
|
15
|
+
Create a private tools directory and select the existing company/project workspace:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
node /absolute/package/bin/ezenciel-agents-tools.mjs init --standalone \
|
|
19
|
+
--home /absolute/private/company-tools --workspace /absolute/company-workspace
|
|
20
|
+
/absolute/private/company-tools/bin/ez --help
|
|
21
|
+
/absolute/private/company-tools/bin/ez status
|
|
22
|
+
/absolute/private/company-tools/bin/ez plugins list
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Init starts nothing, uses an empty catalog by default, preserves existing
|
|
26
|
+
TOOLS.md notes and appends the registry's discovery instructions. A registry
|
|
27
|
+
cannot be replaced by rerunning init. Keep the package at its original path:
|
|
28
|
+
the launcher imports it. Status reports `main: null` without a relay binding;
|
|
29
|
+
automated software upgrades currently require a relay deployment.
|
|
30
|
+
|
|
31
|
+
Have each executor read the workspace's TOOLS.md and the installed plugin skills.
|
|
32
|
+
Add that instruction to its existing project instructions without replacing them.
|
|
33
|
+
Use the absolute launcher, or prepend its bin directory to that session's PATH.
|
|
34
|
+
Never overwrite another global `ez`; it may belong to a different installation.
|
|
35
|
+
A company registry remains explicit even when invoked from a different directory.
|
|
36
|
+
Other projects/accounts should use separate registries and private plugin state.
|
|
37
|
+
|
|
38
|
+
## Install and verify a capability
|
|
39
|
+
|
|
40
|
+
The current conversation owns authorized plugin onboarding. Follow the
|
|
41
|
+
[plugin contract](plugins.md#installation-completion-contract), using the bound
|
|
42
|
+
launcher for `plugins inspect <id> --source /absolute/package`, then
|
|
43
|
+
`plugins install <id> --source /absolute/package --revision <inspected-hash>`.
|
|
44
|
+
Read the skill, start the plugin, complete its provider authentication and verify
|
|
45
|
+
the intended account with a supported operation. Deliver any necessary consent
|
|
46
|
+
link or QR in the current client. No Telegram handoff is required.
|
|
47
|
+
|
|
48
|
+
Run `tools list`, the registered alias's help and a harmless account operation
|
|
49
|
+
from each actual executor. Host-shell access alone does not prove a sandboxed
|
|
50
|
+
session can access Docker or the registry. Client permissions still apply.
|
|
51
|
+
Native client plugins/connectors are not converted into Ez plugins automatically.
|
|
52
|
+
|
|
53
|
+
## Combine with an autonomous agent
|
|
54
|
+
|
|
55
|
+
A relay can coexist with CLI-only registries on the same machine. It retains its
|
|
56
|
+
own pairing, single-writer queue, mind and plugin registry. Calling its existing
|
|
57
|
+
bound launcher explicitly reuses that registry and its accounts; never initialize
|
|
58
|
+
over it or silently select it from another workspace. Coordinate writes with its
|
|
59
|
+
active jobs. Separate registries do not automatically share credentials or data.
|
|
60
|
+
|
|
61
|
+
Continuous monitoring requires a configured event consumer/relay and the plugin's
|
|
62
|
+
supported watcher. CLI-only installation does not create background agent turns.
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jc_stack/ez-agents",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.13",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "A
|
|
6
|
+
"description": "A lightweight foundation for persistent business AI assistants using existing AI harnesses, workspaces and plugins.",
|
|
7
7
|
"packageManager": "pnpm@10.30.3",
|
|
8
8
|
"bin": {
|
|
9
|
+
"ezenciel-agents-schedule": "bin/ezenciel-agents-schedule.mjs",
|
|
9
10
|
"ezenciel-agents-install": "bin/ezenciel-agents-install",
|
|
10
11
|
"ezenciel-agents": "bin/ezenciel-agents.mjs",
|
|
11
12
|
"ezenciel-agents-owner": "bin/ezenciel-agents-owner.mjs",
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
"ezenciel-agents-react": "bin/ezenciel-agents-react.mjs",
|
|
14
15
|
"ezenciel-agents-approval": "bin/ezenciel-agents-approval.mjs",
|
|
15
16
|
"ezenciel-agents-setup": "bin/ezenciel-agents-setup.mjs",
|
|
17
|
+
"ezenciel-agents-task": "bin/ezenciel-agents-task.mjs",
|
|
16
18
|
"ezenciel-agents-source": "bin/ezenciel-agents-source.mjs",
|
|
17
19
|
"ezenciel-agents-docker": "bin/ezenciel-agents-docker",
|
|
18
20
|
"ezenciel-agents-create": "bin/ezenciel-agents-create",
|
|
@@ -36,10 +38,12 @@
|
|
|
36
38
|
"docker",
|
|
37
39
|
"compose.whatsapp.yaml",
|
|
38
40
|
"scripts/smoke.ts",
|
|
41
|
+
"scripts/smoke-scheduler.ts",
|
|
39
42
|
"LICENSE",
|
|
40
43
|
"CHANGELOG.md",
|
|
41
44
|
"THIRD_PARTY_NOTICES.md",
|
|
42
45
|
"scripts/release-check.mjs",
|
|
46
|
+
"scripts/stage-qa.mjs",
|
|
43
47
|
"test",
|
|
44
48
|
"tsconfig.json",
|
|
45
49
|
"scripts/assert-local-registry.mjs",
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
"build": "tsc --noEmit",
|
|
60
64
|
"test": "tsx --test test/*.test.ts test/*.test.mjs",
|
|
61
65
|
"verify": "pnpm test && pnpm build",
|
|
66
|
+
"smoke:scheduler": "tsx scripts/smoke-scheduler.ts",
|
|
62
67
|
"smoke": "./bin/ezenciel-agents-docker run --rm --no-deps relay smoke",
|
|
63
68
|
"prepublishOnly": "npm run verify",
|
|
64
69
|
"registry:up": "docker compose -f registry/docker-compose.yml up -d",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Opt-in token-consuming probe. Real executor and relay handlers; synthetic Telegram provider.
|
|
2
|
+
// Run: node --import tsx scripts/smoke-scheduler.ts [seconds=1860] [cli=codex]
|
|
3
|
+
import { mkdtemp, writeFile, readFile, readdir } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { createRelay } from '../src/index.js'
|
|
7
|
+
import { ControlStore } from '../src/control-state.js'
|
|
8
|
+
import { Scheduler } from '../src/scheduler.js'
|
|
9
|
+
import { RunStore } from '../src/runs.js'
|
|
10
|
+
import { initialPreset } from '../src/ai.js'
|
|
11
|
+
import { initializeWorkspace } from '../src/workspace.js'
|
|
12
|
+
import type { Update } from 'grammy/types'
|
|
13
|
+
|
|
14
|
+
const duration=Number(process.argv[2] || 1860),cli=process.argv[3] || 'codex'
|
|
15
|
+
const nativeGoal=process.argv[4]==='goal'
|
|
16
|
+
if(!Number.isSafeInteger(duration) || duration<30)throw new Error('Duration must be at least 30 seconds')
|
|
17
|
+
const root=await mkdtemp(join(tmpdir(),'ez-scheduler-smoke-')),workspace=join(root,'agent'),controlDir=join(root,'control')
|
|
18
|
+
await initializeWorkspace(workspace)
|
|
19
|
+
const control=new ControlStore(controlDir,1000),scheduler=new Scheduler(controlDir),runs=new RunStore(controlDir)
|
|
20
|
+
await control.requestPairing(101,101);await control.approveOwner(101)
|
|
21
|
+
const owner=(await control.status()).owner!,execution=await control.captureChoice(initialPreset(cli))
|
|
22
|
+
const replies:{at:number;text:string}[]=[]
|
|
23
|
+
const relay=createRelay({workspace,controlDir,pairingTtlMs:1000,executorTimeoutMs:0,executorCli:cli,telegramBotToken:'fixture'})
|
|
24
|
+
relay.bot.botInfo={id:999,is_bot:true,first_name:'Fixture',username:'fixture_bot'} as typeof relay.bot.botInfo
|
|
25
|
+
relay.bot.api.config.use(async(_previous,method,payload)=>{
|
|
26
|
+
if(method==='sendMessage') {const reply={at:Date.now(),text:(payload as {text:string}).text};replies.push(reply);console.log(JSON.stringify({reply}))}
|
|
27
|
+
return {ok:true,result:{message_id:replies.length}} as never
|
|
28
|
+
})
|
|
29
|
+
const due=Date.now()+2000
|
|
30
|
+
const text=nativeGoal
|
|
31
|
+
? `/goal Native multi-turn continuation QA: create phase1.txt containing ONE and phase2.txt containing TWO, then finished.txt containing DONE and deliver BACKGROUND_DONE. In the FIRST turn write only phase1.txt and END with final response FIRST_TURN_DONE, leaving this native goal active. Do not write phase2 or finished.txt or send BACKGROUND_DONE in the first turn. On a subsequent native automatic continuation, run a terminal sleep for ${duration} seconds and wait for it, write phase2.txt and finished.txt, read and verify all three files, send BACKGROUND_DONE through ezenciel-agents-message, verify delivery, and mark this goal complete. No extra schedule, user prompt or custom continuation loop. Keep progress.md updated.`
|
|
32
|
+
: `This is an authorized synthetic test. In your task directory write progress.md with WAITING. Run a terminal sleep for ${duration} seconds and wait for that command to finish. Then write finished.txt containing DONE and use ezenciel-agents-message --text 'BACKGROUND_DONE'. Do not reschedule or finish early. No external services are needed.`
|
|
33
|
+
await scheduler.save({id:'sleep',name:'Long-running synthetic QA',text,trigger:{at:new Date(due).toISOString()},enabled:true,owner,execution})
|
|
34
|
+
let draining=false
|
|
35
|
+
const tick=setInterval(()=>{if(!draining){draining=true;void relay.drainSources().then(()=>relay.drainOutbox()).catch(console.error).finally(()=>{draining=false})}},250)
|
|
36
|
+
const until=async(check:()=>Promise<boolean>,seconds:number)=>{
|
|
37
|
+
const deadline=Date.now()+seconds*1000
|
|
38
|
+
while(!await check()){if(Date.now()>deadline)throw new Error('Probe timed out');await new Promise(r=>setTimeout(r,250))}
|
|
39
|
+
}
|
|
40
|
+
console.log(JSON.stringify({root,duration,cli}))
|
|
41
|
+
try{
|
|
42
|
+
await until(async()=>(await runs.list()).some(r=>r.scheduled && r.status==='running'),60)
|
|
43
|
+
const [background]=(await runs.list()).filter(r=>r.scheduled)
|
|
44
|
+
const message:Update={update_id:10,message:{message_id:10,date:0,text:"What is 17 times 19? Reply with the number using ezenciel-agents-message. This is a local test with a synthetic delivery provider.",from:{id:101,is_bot:false,first_name:'Fixture'},chat:{id:101,type:'private',first_name:'Fixture'}}}
|
|
45
|
+
const askedAt=Date.now();await relay.bot.handleUpdate(message);await relay.drainInbox(true)
|
|
46
|
+
await until(async()=>replies.some(r=>/323/.test(r.text)),180)
|
|
47
|
+
if((await runs.get(background.id))?.status!=='running')throw new Error('Background stopped before chat reply')
|
|
48
|
+
console.log(JSON.stringify({chatReplyMs:Date.now()-askedAt,backgroundState:'running'}))
|
|
49
|
+
if(duration>360){
|
|
50
|
+
await until(async()=>Date.now()-askedAt>360000,duration)
|
|
51
|
+
const followup={...message,update_id:11,message:{...message.message!,message_id:11,text:'Check ezenciel-agents-schedule runs. If the background task is actually running, send BACKGROUND_STATUS_RUNNING followed by its exact run ID. Otherwise report the problem.'}} as Update
|
|
52
|
+
const before=replies.length;await relay.bot.handleUpdate(followup);await relay.drainInbox(true)
|
|
53
|
+
await until(async()=>replies.slice(before).some(r=>r.text.includes('BACKGROUND_STATUS_RUNNING') && r.text.includes(background.id)),180)
|
|
54
|
+
if((await runs.get(background.id))?.status!=='running')throw new Error('Background stopped at six-minute check')
|
|
55
|
+
console.log(JSON.stringify({sixMinuteCheck:'passed',backgroundState:'running'}))
|
|
56
|
+
}
|
|
57
|
+
await until(async()=> (await runs.get(background.id))?.status!=='running',duration+180)
|
|
58
|
+
await relay.drainOutbox()
|
|
59
|
+
const status=(await runs.get(background.id))?.status
|
|
60
|
+
if(status!=='completed' || replies.filter(r=>r.text.includes('BACKGROUND_DONE')).length!==1)throw new Error('Missing completed run or exactly one delivered result')
|
|
61
|
+
const finished=await readFile(join(workspace,'work/tasks',background.id,'finished.txt'),'utf8')
|
|
62
|
+
if(finished.trim()!=='DONE')throw new Error('Missing finished.txt artifact')
|
|
63
|
+
if(nativeGoal){
|
|
64
|
+
for(const [file,expected] of [['phase1.txt','ONE'],['phase2.txt','TWO']])
|
|
65
|
+
if((await readFile(join(workspace,'work/tasks',background.id,file),'utf8')).trim()!==expected)throw new Error(`Missing native goal artifact: ${file}`)
|
|
66
|
+
}
|
|
67
|
+
const record=(await runs.get(background.id))!
|
|
68
|
+
if(Date.parse(record.endedAt!)-Date.parse(record.startedAt!) < duration*1000)throw new Error('Worker completed before requested duration')
|
|
69
|
+
let goalEvidence:unknown
|
|
70
|
+
if(nativeGoal){
|
|
71
|
+
const home=join(controlDir,'cli/codex/tasks',background.id),sessionId=record.nativeSessionId
|
|
72
|
+
if(!sessionId)throw new Error('Missing native session ID')
|
|
73
|
+
const files=await readdir(join(home,'sessions'),{recursive:true})
|
|
74
|
+
const file=files.find(f=>f.endsWith(`${sessionId}.jsonl`))
|
|
75
|
+
if(!file)throw new Error('Missing native transcript')
|
|
76
|
+
const events=(await readFile(join(home,'sessions',file),'utf8')).trim().split('\n').map(line=>JSON.parse(line)).filter(e=>e.type==='event_msg')
|
|
77
|
+
const turns=events.filter(e=>e.payload.type==='task_complete')
|
|
78
|
+
if(turns.length<2 || !turns[0].payload.last_agent_message?.includes('FIRST_TURN_DONE'))throw new Error('Native multi-turn continuation was not verified')
|
|
79
|
+
// Read Codex-owned evidence only; Ez never creates or manages this database.
|
|
80
|
+
const {DatabaseSync}=await import('node:sqlite'),db=new DatabaseSync(join(home,'goals_1.sqlite'),{readOnly:true})
|
|
81
|
+
try{
|
|
82
|
+
const goal=db.prepare('SELECT status FROM thread_goals WHERE thread_id = ?').get(sessionId)
|
|
83
|
+
if(goal?.status!=='complete')throw new Error('Native goal did not complete')
|
|
84
|
+
goalEvidence={status:goal.status,completedTurns:turns.map(e=>e.payload.turn_id)}
|
|
85
|
+
}finally{db.close()}
|
|
86
|
+
}
|
|
87
|
+
const evidence={duration,cli,nativeGoal,goalEvidence,status,replies,run:await runs.get(background.id)}
|
|
88
|
+
await writeFile(join(root,'evidence.json'),JSON.stringify(evidence,null,2),{mode:0o600})
|
|
89
|
+
console.log(JSON.stringify({passed:true,evidence:join(root,'evidence.json')}))
|
|
90
|
+
}finally{clearInterval(tick);while(draining)await new Promise(r=>setTimeout(r,50));await relay.stop()}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Developer packaging only. The existing agent-owned updater installs the result.
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {tmpdir} from 'node:os';
|
|
6
|
+
import {execFileSync} from 'node:child_process';
|
|
7
|
+
import {parseArgs} from 'node:util';
|
|
8
|
+
import {digest, extract, version, newer} from '../src/updates/artifact.mjs';
|
|
9
|
+
|
|
10
|
+
const {values:v}=parseArgs({options:Object.fromEntries(['source','catalog','label','version','flow'].map(k=>[k,{type:'string'}]))});
|
|
11
|
+
if(!['source','catalog','label','version','flow'].every(k=>v[k]))throw Error('Required: --source CHECKOUT --catalog DIRECTORY --label beta-12 --version 0.1.0-beta.12.qa.1 --flow QA.md');
|
|
12
|
+
if(!/^beta-[1-9]\d*$/.test(v.label))throw Error('Label must be beta-N');
|
|
13
|
+
version(v.version);
|
|
14
|
+
if(!/^\d+\.\d+\.\d+-beta\.\d+\.qa\.[1-9]\d*$/.test(v.version))throw Error('Use a distinct private version: X.Y.Z-beta.N.qa.BUILD');
|
|
15
|
+
const source=await fs.realpath(v.source),catalog=path.resolve(v.catalog);
|
|
16
|
+
const run=(cmd,args,cwd=source)=>execFileSync(cmd,args,{cwd,encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
|
|
17
|
+
if(run('git',['status','--porcelain']))throw Error('Commit the reviewed source before staging QA');
|
|
18
|
+
const commit=run('git',['rev-parse','HEAD']),flow=await fs.readFile(v.flow,'utf8');
|
|
19
|
+
if(!flow.trim())throw Error('A feature QA flow is required');
|
|
20
|
+
await fs.mkdir(catalog,{recursive:true,mode:0o700});
|
|
21
|
+
const destination=path.join(catalog,v.label);
|
|
22
|
+
if(await fs.lstat(destination).then(()=>true,err=>{if(err.code==='ENOENT')return false;throw err;}))throw Error('QA label already exists; never replace a build. Choose the next beta label.');
|
|
23
|
+
const temp=await fs.mkdtemp(path.join(tmpdir(),'ez-stage-qa-'));
|
|
24
|
+
const staged=await fs.mkdtemp(path.join(catalog,'.staging-'));
|
|
25
|
+
try {
|
|
26
|
+
const pack=(cwd,out)=>JSON.parse(run('npm',['pack','--ignore-scripts','--json','--pack-destination',out],cwd))[0];
|
|
27
|
+
const original=pack(source,temp);
|
|
28
|
+
await extract(await fs.readFile(path.join(temp,original.filename)),path.join(temp,'source'));
|
|
29
|
+
const root=path.join(temp,'source'),pkg=JSON.parse(await fs.readFile(path.join(root,'package.json'),'utf8'));
|
|
30
|
+
if(pkg.ezRelease?.kind!=='main')throw Error('This staging command accepts the main package only');
|
|
31
|
+
if(!newer(v.version,pkg.version))throw Error('Private QA version must be newer than source package version');
|
|
32
|
+
pkg.version=v.version;pkg.ezQa={label:v.label,commit,private:true};
|
|
33
|
+
await fs.writeFile(path.join(root,'package.json'),JSON.stringify(pkg,null,2)+'\n');
|
|
34
|
+
const packed=pack(root,staged),artifact=await fs.readFile(path.join(staged,packed.filename));
|
|
35
|
+
const receipt={label:v.label,version:v.version,package:pkg.name,commit,sha256:digest(artifact),file:packed.filename,private:true};
|
|
36
|
+
await fs.writeFile(path.join(staged,'manifest.json'),JSON.stringify(receipt,null,2)+'\n',{mode:0o600});
|
|
37
|
+
await fs.writeFile(path.join(staged,'QA.md'),flow,{mode:0o600});
|
|
38
|
+
await fs.chmod(path.join(staged,packed.filename),0o600);
|
|
39
|
+
// The published directory is nonempty, so competing staging cannot replace it.
|
|
40
|
+
await fs.rename(staged,destination);
|
|
41
|
+
console.log(JSON.stringify({...receipt,directory:destination},null,2));
|
|
42
|
+
} finally {await fs.rm(temp,{recursive:true,force:true});await fs.rm(staged,{recursive:true,force:true});}
|