@jc_stack/ez-agents 0.1.0-beta.34 → 0.1.0-beta.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +1 -0
- package/.env.example +5 -16
- package/AGENTS.md +17 -9
- package/CHANGELOG.md +67 -0
- package/Dockerfile +7 -9
- package/README.md +10 -13
- package/bin/ez +97 -0
- package/bin/ezenciel-agents-plugin-broker.mjs +16 -0
- package/bin/ezenciel-agents-provision-telegram.mjs +13 -0
- package/bin/ezenciel-agents.mjs +1 -1
- package/compose.yaml +40 -4
- package/docker/entrypoint.sh +1 -1
- package/docker/healthcheck.mjs +12 -4
- package/docker/run.ts +5 -6
- package/docker/smoke.mjs +28 -6
- package/docs/application-channel.md +32 -10
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/channel-backend.md +4 -3
- package/docs/docker-runtime.md +79 -11
- package/docs/local-qa.md +2 -2
- package/docs/plugin-guide.md +202 -0
- package/docs/plugins.md +48 -2
- package/docs/releasing.md +13 -0
- package/docs/responsive-channels.md +1 -1
- package/docs/scheduling.md +4 -4
- package/docs/setup.md +11 -5
- package/docs/upgrades.md +46 -23
- package/package.json +5 -5
- package/scripts/smoke-scheduler.ts +10 -3
- package/scripts/smoke.ts +14 -11
- package/src/agent-guidance.ts +6 -9
- package/src/agent-install.ts +28 -7
- package/src/application-channel.ts +78 -7
- package/src/application-cli.ts +2 -2
- package/src/approval-cli.ts +14 -7
- package/src/approval.ts +27 -37
- package/src/channel-backend.ts +2 -3
- package/src/config.ts +12 -27
- package/src/control-state.ts +175 -8
- package/src/conversation-menu.ts +6 -17
- package/src/delivery-socket-client.d.mts +4 -0
- package/src/delivery-socket-client.mjs +104 -0
- package/src/delivery-socket.ts +354 -0
- package/src/execution-authority.ts +21 -5
- package/src/executor.ts +20 -23
- package/src/files.ts +38 -11
- package/src/host-executor.ts +28 -14
- package/src/inbox.ts +15 -49
- package/src/index.ts +95 -52
- package/src/install-tools.mjs +97 -22
- package/src/ledger-endpoint.d.mts +2 -0
- package/src/ledger-endpoint.mjs +17 -0
- package/src/menu.ts +7 -2
- package/src/message-history.ts +13 -18
- package/src/message.ts +35 -18
- package/src/plugin-broker.mjs +396 -0
- package/src/plugins/connection.mjs +2 -2
- package/src/plugins/manager.mjs +132 -23
- package/src/plugins/native-tasks.mjs +2 -2
- package/src/prompt-suffix.ts +12 -0
- package/src/react.ts +6 -4
- package/src/runs.ts +156 -243
- package/src/schedule-cli.ts +29 -10
- package/src/scheduled-tasks.ts +53 -10
- package/src/scheduler.ts +20 -7
- package/src/service.ts +3 -3
- package/src/software-status.ts +36 -4
- package/src/task-executor.ts +2 -2
- package/src/task-workspace.ts +4 -3
- package/src/tasks.ts +1 -1
- package/src/telegram-provisioning-cli.ts +22 -0
- package/src/telegram-provisioning.ts +214 -0
- package/src/updates/control.mjs +77 -7
- package/src/updates/runtime.mjs +28 -8
- package/src/updates/status.mjs +25 -3
- package/src/updates/supervisor.mjs +15 -16
- package/src/version.ts +20 -0
- package/src/workspace.ts +3 -9
- package/templates/failure-review.md +1 -1
- package/templates/maintainer-purpose.md +1 -1
- package/test/agent-guidance.test.ts +14 -80
- package/test/agent-install.test.ts +14 -0
- package/test/ai.test.ts +36 -0
- package/test/application-channel.test.ts +61 -13
- package/test/application-only.test.ts +14 -13
- package/test/channel-backend.test.ts +3 -7
- package/test/channel-delivery.test.ts +4 -1
- package/test/channel-owner.test.ts +6 -3
- package/test/codex-context.test.ts +2 -2
- package/test/config.test.ts +38 -43
- package/test/control-state.test.ts +83 -4
- package/test/conversation-menu.test.ts +2 -4
- package/test/delivery-transport.test.ts +72 -0
- package/test/docker-runtime.test.ts +27 -14
- package/test/engine-handoff.test.ts +7 -6
- package/test/event-sources.test.ts +2 -2
- package/test/execution-authority.test.ts +9 -9
- package/test/executor.test.ts +10 -11
- package/test/failure.test.ts +12 -5
- package/test/files.test.ts +2 -2
- package/test/helpers/ledger.ts +18 -0
- package/test/host-executor.test.ts +74 -0
- package/test/inbox.test.ts +8 -10
- package/test/install-config.test.ts +1 -0
- package/test/install-tools.test.mjs +74 -7
- package/test/intake-relay.test.ts +58 -11
- package/test/message-history.test.ts +39 -47
- package/test/native-tasks.test.ts +1 -1
- package/test/plugin-broker.test.mjs +172 -0
- package/test/plugin-manager.test.mjs +119 -2
- package/test/reaction.test.ts +4 -0
- package/test/relay.test.ts +5 -7
- package/test/repair-policy.test.ts +1 -4
- package/test/schedule-cli.test.ts +14 -9
- package/test/scheduled-tasks.test.ts +29 -6
- package/test/scheduler-host.test.ts +5 -5
- package/test/scheduler-relay.test.ts +15 -9
- package/test/security.test.ts +6 -3
- package/test/software-status.test.ts +21 -3
- package/test/telegram-provisioning.test.ts +160 -0
- package/test/updates.test.mjs +93 -13
- package/test/workspace.test.ts +4 -1
- package/bin/ezenciel-agents-watch.mjs +0 -8
- package/compose.workforce-watch.yaml +0 -33
- package/docker/recovery.ts +0 -11
- package/docs/pagerduty.md +0 -42
- package/docs/workforce-watch.md +0 -101
- package/src/pagerduty.ts +0 -109
- package/src/reply-context.ts +0 -43
- package/src/update-attention.ts +0 -18
- package/src/workforce-watch-cli.ts +0 -14
- package/src/workforce-watch.ts +0 -155
- package/templates/agent/AGENTS.md +0 -5
- package/templates/agent-guidance.md +0 -17
- package/test/pagerduty.test.ts +0 -104
- package/test/update-attention.test.ts +0 -20
- package/test/workforce-watch.test.ts +0 -180
package/docs/upgrades.md
CHANGED
|
@@ -16,9 +16,9 @@ The agent owns release review, policy decisions and communication. The host
|
|
|
16
16
|
supervisor owns interruption-safe replacement. There is one host service per
|
|
17
17
|
agent, not a second agent or an additional updater daemon. Its replaceable child
|
|
18
18
|
runs the normal CLI transport. It checks npm every six hours while running and
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
records automatic-channel changes; discovery never wakes the agent or injects
|
|
20
|
+
wrapper state into a session. Updates stay owner-invoked through `ez updates`.
|
|
21
|
+
Checks use the installed scoped npm identity; failures are
|
|
22
22
|
visible in `updates check` and private `tools/updates/available.json`. Plugins marked
|
|
23
23
|
`private: true` in their package metadata are reported as local-source updates
|
|
24
24
|
only, without querying public npm; this does not mean they are up to date.
|
|
@@ -144,7 +144,7 @@ rollback is a recovery operation for the previous installation only.
|
|
|
144
144
|
queuing it.** It must not wait for completion in that same turn. The supervisor
|
|
145
145
|
pauses new run admission, drains already-started work, then consumes the job.
|
|
146
146
|
One job at a time; an unresolved recovery blocks new upgrades. The agent processes
|
|
147
|
-
multiple requested packages over successive
|
|
147
|
+
multiple requested packages over successive update requests.
|
|
148
148
|
|
|
149
149
|
The exact archive is reverified and re-extracted before execution; editing a
|
|
150
150
|
prepared tree cannot substitute code. Main dependencies install with the frozen
|
|
@@ -164,24 +164,43 @@ running. Failed activation restores the previous code/configuration and checks i
|
|
|
164
164
|
If recovery itself fails, inspect and fix the reported infrastructure problem,
|
|
165
165
|
then use `ez updates recover <job-id>` and finish the turn. This retries only the
|
|
166
166
|
saved code/configuration recovery, not provider operations.
|
|
167
|
-
|
|
167
|
+
An update never wakes the agent or injects wrapper state into its session; the
|
|
168
|
+
owner or agent inspects receipts with `ez updates` when asked.
|
|
168
169
|
|
|
169
170
|
Backups contain credentials and must stay private. Main backups cover the
|
|
170
171
|
canonical mind/control and deployment files. Plugins back up existing named
|
|
171
172
|
volumes while their services are stopped, using the previous image's `tar` and
|
|
172
173
|
read-only volume mounts; archive output is written by the host with mode 0600.
|
|
173
|
-
No backup deletes live data or revokes credentials.
|
|
174
|
-
|
|
174
|
+
No backup deletes live data or revokes credentials. After a successful update, the
|
|
175
|
+
supervisor retains the newest completed backup per target and removes backup
|
|
176
|
+
directories from older terminal receipts (`completed`, `failed` or
|
|
177
|
+
`rolled-back`). Queued, applying and recovery-required jobs are never pruned. Old
|
|
178
|
+
package roots and images remain available until they are no longer needed for the
|
|
179
|
+
active or immediate rollback path; backup cleanup does not remove them. A cleanup
|
|
180
|
+
failure is recorded on the completed receipt and does not turn a successful
|
|
181
|
+
update into a rollback. The supervisor repeats this reconciliation at startup so
|
|
182
|
+
a release restart also cleans backups left by a crash window or by the release
|
|
183
|
+
that installed the policy.
|
|
175
184
|
|
|
176
185
|
The updater accepts only matching state-schema and protocol contracts and an
|
|
177
|
-
unchanged deployment layout.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
unchanged effective deployment layout. The reviewed beta.34 runtime migration
|
|
187
|
+
allows the optional empty `EZ_ISOLATION` passthrough and a changed purpose-file
|
|
188
|
+
fallback when the deployment already binds `EZ_AGENT_PURPOSE_FILE`; the job
|
|
189
|
+
receipt records that migration. Plugins may add command routes to existing
|
|
190
|
+
services; existing routes and all services, privileges, volumes and other
|
|
191
|
+
deployment configuration must remain unchanged. Incompatible changes fail
|
|
192
|
+
before replacement, even for an explicit candidate, and need a separately
|
|
193
|
+
reviewed migration rather than an override flag. Release authors must truthfully
|
|
194
|
+
declare schema compatibility. Code rollback **does not rewind private state**,
|
|
195
|
+
provider cursors or operation receipts; uncertain actions are never replayed.
|
|
196
|
+
Docker health is not proof of live provider identity or delivery.
|
|
197
|
+
|
|
198
|
+
The updater also records the exact Library beta14 command migration: it removes
|
|
199
|
+
the retired `library-document` route and changes `library-query` from
|
|
200
|
+
`query-context` to `search`, while requiring every other manifest and deployment
|
|
201
|
+
field to remain unchanged. A completed main upgrade exits the active supervisor
|
|
202
|
+
after publishing its receipt; the retained bootstrap is then restarted by the
|
|
203
|
+
host service and loads the new package root.
|
|
185
204
|
|
|
186
205
|
## Contributor acceptance
|
|
187
206
|
|
|
@@ -214,12 +233,16 @@ policy selects non-deprecated stable versions, even when latest is a prerelease.
|
|
|
214
233
|
No eligible version is reported as available:null with newer:false. Older installed
|
|
215
234
|
updaters need an exact-version core update to adopt this discovery behavior.
|
|
216
235
|
|
|
217
|
-
##
|
|
236
|
+
## Operating-agent instructions
|
|
218
237
|
|
|
219
|
-
Setup and
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
Setup and the main upgrade step strip any obsolete `<!-- ez shared guidance -->`
|
|
239
|
+
handbook from workspace `AGENTS.md` (and existing `AGENTS.override.md`).
|
|
240
|
+
Personal text outside that block is preserved byte-for-byte. Do not reinsert
|
|
241
|
+
coding, KISS, contribution or repair loops into operating minds.
|
|
242
|
+
|
|
243
|
+
The package-owned locator is the managed `<!-- ez tools -->` block: the bound
|
|
244
|
+
`ez` launcher, `ez --help`, and `ez tools list --details`. Inventory is
|
|
245
|
+
generated on read. Plugin registry init and update binding refresh that locator.
|
|
223
246
|
Malformed markers and symlinks fail visibly rather than overwriting personal work.
|
|
224
247
|
|
|
225
248
|
Codex, agy, Grok and OpenCode discover workspace instructions natively. Claude
|
|
@@ -236,9 +259,9 @@ preserved. Restricted contact tasks retain their separate
|
|
|
236
259
|
bounded instruction scopes. For delivered busy replies absent from the native
|
|
237
260
|
conversation, the owner engine can call `ezenciel-agents-schedule context`.
|
|
238
261
|
|
|
239
|
-
Keep
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
Keep identity and purpose in the agent's own `AGENTS.md`. Instructions cannot
|
|
263
|
+
grant permissions; explicit owner requests take precedence within existing
|
|
264
|
+
execution authority.
|
|
242
265
|
|
|
243
266
|
Update discovery runs independently of active host work. A discovery failure is
|
|
244
267
|
logged locally and retried at the next regular six-hour check; it does not stop
|
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.36",
|
|
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.",
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
"ezenciel-agents-host": "bin/ezenciel-agents-host",
|
|
22
22
|
"ezenciel-agents-tools": "bin/ezenciel-agents-tools.mjs",
|
|
23
23
|
"ezenciel-agents-ai": "bin/ezenciel-agents-ai.mjs",
|
|
24
|
-
"ezenciel-agents-
|
|
25
|
-
"ezenciel-agents-
|
|
24
|
+
"ezenciel-agents-application": "bin/ezenciel-agents-application.mjs",
|
|
25
|
+
"ezenciel-agents-provision-telegram": "bin/ezenciel-agents-provision-telegram.mjs",
|
|
26
|
+
"ezenciel-agents-plugin-broker": "bin/ezenciel-agents-plugin-broker.mjs"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"default-plugins.json",
|
|
@@ -37,7 +38,6 @@
|
|
|
37
38
|
"CONTRIBUTING.md",
|
|
38
39
|
"Dockerfile",
|
|
39
40
|
"compose.yaml",
|
|
40
|
-
"compose.workforce-watch.yaml",
|
|
41
41
|
"docker",
|
|
42
42
|
"compose.whatsapp.yaml",
|
|
43
43
|
"scripts/smoke.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"scripts": {
|
|
62
62
|
"setup": "tsx --env-file-if-exists=.env src/setup.ts",
|
|
63
63
|
"dev": "EZ_DEVELOPMENT=1 tsx watch --clear-screen=false --exclude 'agent/**' --exclude 'test/**' --env-file=.env src/index.ts",
|
|
64
|
-
"start": "./bin/ezenciel-agents-docker up -d --wait
|
|
64
|
+
"start": "./bin/ezenciel-agents-docker up -d --wait",
|
|
65
65
|
"owner": "tsx --env-file=.env src/owner.ts",
|
|
66
66
|
"message": "tsx src/message.ts",
|
|
67
67
|
"react": "tsx src/react.ts",
|
|
@@ -7,6 +7,8 @@ import { createRelay } from '../src/index.js'
|
|
|
7
7
|
import { ControlStore } from '../src/control-state.js'
|
|
8
8
|
import { Scheduler } from '../src/scheduler.js'
|
|
9
9
|
import { RunStore } from '../src/runs.js'
|
|
10
|
+
import { serveDeliverySocket, createLedgerHandler } from '../src/delivery-socket.js'
|
|
11
|
+
import { packageVersion } from '../src/version.js'
|
|
10
12
|
import { initialPreset } from '../src/ai.js'
|
|
11
13
|
import { initializeWorkspace } from '../src/workspace.js'
|
|
12
14
|
import type { Update } from 'grammy/types'
|
|
@@ -22,6 +24,11 @@ await control.requestPairing(101,101);await control.approveOwner(101)
|
|
|
22
24
|
const owner=(await control.status()).owner!,execution=await control.captureChoice(initialPreset(cli))
|
|
23
25
|
const replies:{at:number;text:string}[]=[]
|
|
24
26
|
const relay=createRelay({workspace,controlDir,pairingTtlMs:1000,executorTimeoutMs:0,executorCli:cli,telegramBotToken:'fixture'})
|
|
27
|
+
// Engine children reach this process's memory ledger through the socket.
|
|
28
|
+
const ledger=await serveDeliverySocket(controlDir,createLedgerHandler(controlDir,{
|
|
29
|
+
wake:()=>{void relay.drainOutbox()},
|
|
30
|
+
status:()=>({polling:false,applicationOnly:!relay.telegramEnabled,telegramConfigured:true,version:packageVersion}),
|
|
31
|
+
}))
|
|
25
32
|
relay.bot.botInfo={id:999,is_bot:true,first_name:'Fixture',username:'fixture_bot'} as typeof relay.bot.botInfo
|
|
26
33
|
relay.bot.api.config.use(async(_previous,method,payload)=>{
|
|
27
34
|
if(method==='sendMessage') {const reply={at:Date.now(),text:(payload as {text:string}).text};replies.push(reply);console.log(JSON.stringify({reply}))}
|
|
@@ -59,11 +66,11 @@ try{
|
|
|
59
66
|
await relay.drainOutbox()
|
|
60
67
|
const status=(await runs.get(background.id))?.status
|
|
61
68
|
if(status!=='completed' || replies.filter(r=>r.text.includes('BACKGROUND_DONE')).length!==1)throw new Error('Missing completed run or exactly one delivered result')
|
|
62
|
-
const finished=await readFile(join(
|
|
69
|
+
const finished=await readFile(join(controlDir,'work/tasks',background.id,'finished.txt'),'utf8')
|
|
63
70
|
if(finished.trim()!=='DONE')throw new Error('Missing finished.txt artifact')
|
|
64
71
|
if(nativeGoal){
|
|
65
72
|
for(const [file,expected] of [['phase1.txt','ONE'],['phase2.txt','TWO']])
|
|
66
|
-
if((await readFile(join(
|
|
73
|
+
if((await readFile(join(controlDir,'work/tasks',background.id,file),'utf8')).trim()!==expected)throw new Error(`Missing native goal artifact: ${file}`)
|
|
67
74
|
}
|
|
68
75
|
const record=(await runs.get(background.id))!
|
|
69
76
|
if(Date.parse(record.endedAt!)-Date.parse(record.startedAt!) < duration*1000)throw new Error('Worker completed before requested duration')
|
|
@@ -88,4 +95,4 @@ try{
|
|
|
88
95
|
const evidence={duration,cli,nativeGoal,goalEvidence,status,replies,run:await runs.get(background.id)}
|
|
89
96
|
await writeFile(join(root,'evidence.json'),JSON.stringify(evidence,null,2),{mode:0o600})
|
|
90
97
|
console.log(JSON.stringify({passed:true,evidence:join(root,'evidence.json')}))
|
|
91
|
-
}finally{clearInterval(tick);while(draining)await new Promise(r=>setTimeout(r,50));await relay.stop()}
|
|
98
|
+
}finally{clearInterval(tick);while(draining)await new Promise(r=>setTimeout(r,50));await relay.stop();await ledger.stop()}
|
package/scripts/smoke.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path'
|
|
2
|
-
import { readdir, readFile } from 'node:fs/promises'
|
|
3
2
|
import { fileURLToPath } from 'node:url'
|
|
4
3
|
import { randomUUID } from 'node:crypto'
|
|
5
4
|
import { loadConfig } from '../src/config.js'
|
|
6
5
|
import { ControlStore } from '../src/control-state.js'
|
|
7
6
|
import { startExecutorJob, terminateJob } from '../src/executor.js'
|
|
8
|
-
import { RunStore } from '../src/runs.js'
|
|
7
|
+
import { RunStore, sentOutbox } from '../src/runs.js'
|
|
9
8
|
import { createRelay } from '../src/index.js'
|
|
9
|
+
import { serveDeliverySocket, createLedgerHandler } from '../src/delivery-socket.js'
|
|
10
|
+
import { packageVersion } from '../src/version.js'
|
|
10
11
|
|
|
11
12
|
// Outbound integration test, not proof of Telegram intake or UI interactions.
|
|
12
13
|
// Run while the polling relay is stopped; the owner must already be paired.
|
|
@@ -39,6 +40,11 @@ async function main() {
|
|
|
39
40
|
texts: [prompt],
|
|
40
41
|
})
|
|
41
42
|
await runs.patch(run.id, { status: 'running', startedAt: new Date().toISOString() })
|
|
43
|
+
// The engine child reaches the smoke-process memory ledger through the socket.
|
|
44
|
+
const ledger = await serveDeliverySocket(config.controlDir, createLedgerHandler(config.controlDir, {
|
|
45
|
+
wake: () => { void relay.drainOutbox(run.id) },
|
|
46
|
+
status: () => ({ polling: false, applicationOnly: !relay.telegramEnabled, telegramConfigured: config.telegramBotToken !== '', version: packageVersion }),
|
|
47
|
+
}))
|
|
42
48
|
const job = await startExecutorJob(run.texts, {
|
|
43
49
|
workspace: config.workspace,
|
|
44
50
|
timeoutMs: config.executorTimeoutMs,
|
|
@@ -72,18 +78,14 @@ async function main() {
|
|
|
72
78
|
status: code === 0 ? 'completed' : 'failed',
|
|
73
79
|
endedAt: new Date().toISOString(),
|
|
74
80
|
})
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
for (const name of names.filter((name) => name.endsWith('.sent.json'))) {
|
|
80
|
-
const item = JSON.parse(await readFile(join(config.controlDir, 'outbox', name), 'utf8'))
|
|
81
|
-
if (item.receipt) receipts.push(item.receipt)
|
|
82
|
-
}
|
|
81
|
+
const receipts = sentOutbox(config.controlDir)
|
|
82
|
+
.filter((item) => item.runId === run.id && item.receipt)
|
|
83
|
+
.map((item) => item.receipt)
|
|
84
|
+
const delivery = await runs.deliveryStatus()
|
|
83
85
|
console.log(
|
|
84
86
|
JSON.stringify({ runId: run.id, executor: config.executorCli, model: choice?.model, effort: choice?.effort, exitCode: code, receipts }, null, 2),
|
|
85
87
|
)
|
|
86
|
-
if (code !== 0 || !receipts.length ||
|
|
88
|
+
if (code !== 0 || !receipts.length || (await runs.pendingOutbox()).length || delivery.failed || delivery.unknown) {
|
|
87
89
|
throw new Error('Smoke failed: executor failure, missing receipt, or undelivered outbox item')
|
|
88
90
|
}
|
|
89
91
|
console.log('Outbound smoke passed. Telegram intake and button/media UI still require a live chat test.')
|
|
@@ -93,6 +95,7 @@ async function main() {
|
|
|
93
95
|
process.removeListener('SIGTERM', stop)
|
|
94
96
|
await job.cleanup()
|
|
95
97
|
await relay.stop()
|
|
98
|
+
await ledger.stop()
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
package/src/agent-guidance.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { randomUUID } from 'node:crypto'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import { readFileSync } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
// Resolve against the installed package, never the agent's editable workspace.
|
|
7
|
-
export const agentGuidance = (): string =>
|
|
8
|
-
readFileSync(new URL('../templates/agent-guidance.md', import.meta.url), 'utf8').trim()
|
|
9
4
|
|
|
10
5
|
const start = '<!-- ez shared guidance: begin -->'
|
|
11
6
|
const end = '<!-- ez shared guidance: end -->'
|
|
12
7
|
|
|
13
|
-
//
|
|
14
|
-
// Personal
|
|
8
|
+
// Drop the obsolete package-owned handbook. Discovery lives in the managed
|
|
9
|
+
// ez tools locator. Personal text outside the markers stays byte-for-byte.
|
|
15
10
|
export async function installAgentGuidance(workspace: string): Promise<void> {
|
|
16
11
|
for (const name of ['AGENTS.md', 'AGENTS.override.md']) {
|
|
17
12
|
const file = path.join(workspace, name)
|
|
@@ -23,11 +18,13 @@ export async function installAgentGuidance(workspace: string): Promise<void> {
|
|
|
23
18
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
24
19
|
throw error
|
|
25
20
|
}
|
|
26
|
-
const block = `${start}\n${agentGuidance()}\n${end}\n\n`
|
|
27
21
|
const from = original.indexOf(start), to = original.indexOf(end)
|
|
28
22
|
if ((from < 0) !== (to < 0) || (from >= 0 && (to < from || original.indexOf(start, from + start.length) >= 0 || original.indexOf(end, to + end.length) >= 0)))
|
|
29
23
|
throw new Error(`Malformed shared guidance block: ${file}`)
|
|
30
|
-
|
|
24
|
+
if (from < 0) continue
|
|
25
|
+
let rest = original.slice(to + end.length)
|
|
26
|
+
if (from === 0 && rest.startsWith('\n')) rest = rest.slice(1)
|
|
27
|
+
const updated = original.slice(0, from) + rest
|
|
31
28
|
if (updated === original) continue
|
|
32
29
|
const temporary = `${file}.${randomUUID()}.tmp`
|
|
33
30
|
try {
|
package/src/agent-install.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, rm, readdir } from 'node:fs/promises'
|
|
2
|
-
import {
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile, rm, readdir } from 'node:fs/promises'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { isAbsolute, join, resolve, dirname, delimiter } from 'node:path'
|
|
3
4
|
import { parseArgs } from 'node:util'
|
|
4
5
|
import { executorKey } from './executor.js'
|
|
5
6
|
import { isolationTransport, parseIsolationClass, type IsolationClass } from './isolation.js'
|
|
7
|
+
import { ledgerOverlayName, ledgerOverlayYaml } from './ledger-endpoint.mjs'
|
|
8
|
+
import { initializeWorkspace } from './workspace.js'
|
|
6
9
|
import { fileURLToPath } from 'node:url'
|
|
7
10
|
import { validateCodexProvider, type CodexProviderBinding } from './executor.js'
|
|
8
11
|
|
|
@@ -32,7 +35,7 @@ export const installationCli = async (root: string, installerCli?: string): Prom
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export const createAgent = async (options: {
|
|
35
|
-
root: string; hostRoot: string; composeFile: string; name: string; purpose: string; token: string; cli?: string; image?: string; isolation?: string; codexProvider?: CodexProviderBinding
|
|
38
|
+
root: string; hostRoot: string; composeFile: string; name: string; purpose: string; token: string; cli?: string; image?: string; isolation?: string; codexProvider?: CodexProviderBinding; ledgerPort?: number
|
|
36
39
|
}) => {
|
|
37
40
|
const { root, hostRoot, composeFile, name, purpose, token } = options
|
|
38
41
|
const requestedIsolation = options.isolation?.trim()
|
|
@@ -49,6 +52,11 @@ export const createAgent = async (options: {
|
|
|
49
52
|
const isolation: IsolationClass = requestedIsolation ? parseIsolationClass(requestedIsolation) : cli === 'codex' ? 'isolated' : 'host-capable'
|
|
50
53
|
if (isolation === 'isolated' && cli !== 'codex') throw new Error(`Isolated execution is unavailable for ${cli}; use host-capable isolation`)
|
|
51
54
|
const transport = isolationTransport(isolation)
|
|
55
|
+
const ledgerPort = isolation === 'host-capable'
|
|
56
|
+
? options.ledgerPort ?? 20000 + (createHash('sha256').update(name).digest().readUInt16BE(0) % 10000)
|
|
57
|
+
: undefined
|
|
58
|
+
if (isolation === 'host-capable' && (!Number.isSafeInteger(ledgerPort) || ledgerPort! < 1024 || ledgerPort! > 65535))
|
|
59
|
+
throw new Error('Host-capable agents require a free loopback ledger port for host CLI access')
|
|
52
60
|
const codexProvider = options.codexProvider ? validateCodexProvider(options.codexProvider) : undefined
|
|
53
61
|
if (codexProvider && cli !== 'codex') throw new Error('A Codex provider requires the Codex installation CLI')
|
|
54
62
|
const directory = join(root, name), deploymentDir = join(hostRoot, name)
|
|
@@ -56,28 +64,41 @@ export const createAgent = async (options: {
|
|
|
56
64
|
await mkdir(directory, {mode:0o700})
|
|
57
65
|
try {
|
|
58
66
|
const project = `ez-agent-${name}`
|
|
67
|
+
const ledgerOverlay = join(deploymentDir, ledgerOverlayName)
|
|
59
68
|
const values = {
|
|
60
69
|
COMPOSE_PROJECT_NAME: project,
|
|
61
|
-
COMPOSE_FILE: composeFile,
|
|
70
|
+
COMPOSE_FILE: [composeFile, ...(ledgerPort ? [ledgerOverlay] : [])].join(delimiter),
|
|
62
71
|
EZ_RELAY_IMAGE: image,
|
|
63
72
|
EZ_RELAY_ENV_FILE: join(deploymentDir, 'relay.env'),
|
|
64
73
|
EZ_AGENT_PURPOSE_FILE: join(deploymentDir, 'purpose.md'),
|
|
65
74
|
EZ_EXECUTOR_CLI: cli,
|
|
66
75
|
EZ_ISOLATION: isolation,
|
|
67
76
|
EZ_EXECUTOR_TRANSPORT: transport,
|
|
77
|
+
...(ledgerPort ? { EZ_DELIVERY_TCP_PORT: String(ledgerPort) } : {}),
|
|
68
78
|
...(isolation === 'isolated' && cli === 'codex' ? { EZ_CODEX_SANDBOX: 'external' } : {}),
|
|
69
79
|
EZ_AGENT_WORKSPACE: join(deploymentDir, 'mind'),
|
|
70
80
|
EZ_CONTROL_DIR: join(deploymentDir, 'control'),
|
|
81
|
+
EZ_TOOLS_HOME: join(deploymentDir, 'tools'),
|
|
82
|
+
EZ_PLUGIN_BROKER_SOCKET: join(deploymentDir, 'control', 'plugin-broker.sock'),
|
|
83
|
+
EZ_PLUGIN_BROKER_HOST_CONFIG: join(deploymentDir, 'host-executor.json'),
|
|
84
|
+
...(isolation === 'isolated' ? { COMPOSE_PROFILES: 'isolated' } : {}),
|
|
71
85
|
EZ_WHATSAPP_IPC_VOLUME: `${project}-whatsapp-ipc`,
|
|
72
86
|
EZ_WHATSAPP_CLIENT_VOLUME: `${project}-whatsapp-client`,
|
|
73
87
|
}
|
|
74
88
|
await writeFile(join(directory, 'docker.env'), Object.entries(values).map(([k,v]) => `${k}='${v}'\n`).join(''), {mode:0o600, flag:'wx'})
|
|
89
|
+
if (ledgerPort) await writeFile(join(directory, ledgerOverlayName), ledgerOverlayYaml(), {mode:0o600, flag:'wx'})
|
|
75
90
|
await writeFile(join(directory, 'relay.env'), `TELEGRAM_BOT_TOKEN=${token}\n`, {mode:0o600, flag:'wx'})
|
|
76
91
|
await writeFile(join(directory, 'purpose.md'), purpose.trim()+'\n', {mode:0o644, flag:'wx'})
|
|
77
92
|
await mkdir(join(directory,'mind'),{mode:0o700})
|
|
93
|
+
// Install-time seed: the relay never writes the workspace at startup.
|
|
94
|
+
// Readable beyond the installer UID so a mismatched runtime UID still
|
|
95
|
+
// loads its own purpose; the 0700 deployment directory keeps it private.
|
|
96
|
+
const seeded = await initializeWorkspace(join(directory,'mind'), join(directory,'purpose.md'))
|
|
97
|
+
if (seeded.includes('AGENTS.md')) await chmod(join(directory,'mind','AGENTS.md'), 0o644)
|
|
78
98
|
await mkdir(join(directory,'control'),{mode:0o700})
|
|
99
|
+
await mkdir(join(directory,'tools'),{mode:0o700})
|
|
79
100
|
const agent = {name, project, deploymentDir, purpose:purpose.trim(), executor:cli, isolation}
|
|
80
|
-
const host = {cli,isolation,agents:[{name,workspace:join(deploymentDir,'mind'),controlDir:join(deploymentDir,'control'),binDir:join(dirname(composeFile),'bin'),...(codexProvider?{codexProviders:[codexProvider]}:{})}]}
|
|
101
|
+
const host = {cli,isolation,agents:[{name,workspace:join(deploymentDir,'mind'),controlDir:join(deploymentDir,'control'),binDir:join(dirname(composeFile),'bin'),...(isolation === 'isolated' ? {toolsHome:join(deploymentDir,'tools')} : {}),...(codexProvider?{codexProviders:[codexProvider]}:{})}]}
|
|
81
102
|
await writeFile(join(directory,'host-executor.json'),JSON.stringify(host,null,2)+'\n',{mode:0o600,flag:'wx'})
|
|
82
103
|
await writeFile(join(directory, 'agent.json'), JSON.stringify(agent,null,2)+'\n', {mode:0o600, flag:'wx'})
|
|
83
104
|
return agent
|
|
@@ -96,7 +117,7 @@ export const listAgents = async (root: string) => {
|
|
|
96
117
|
|
|
97
118
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
98
119
|
try {
|
|
99
|
-
const {values:v} = parseArgs({options:{name:{type:'string'},purpose:{type:'string'},isolation:{type:'string'},'host-root':{type:'string'},'compose-file':{type:'string'},'relay-image':{type:'string'},list:{type:'boolean'},cli:{type:'string'},'register-cli':{type:'string'},'codex-provider':{type:'string'},'codex-provider-name':{type:'string'},'codex-base-url':{type:'string'},'codex-env-key':{type:'string'},'codex-model':{type:'string',multiple:true}}})
|
|
120
|
+
const {values:v} = parseArgs({options:{name:{type:'string'},purpose:{type:'string'},isolation:{type:'string'},'host-root':{type:'string'},'compose-file':{type:'string'},'relay-image':{type:'string'},'ledger-port':{type:'string'},list:{type:'boolean'},cli:{type:'string'},'register-cli':{type:'string'},'codex-provider':{type:'string'},'codex-provider-name':{type:'string'},'codex-base-url':{type:'string'},'codex-env-key':{type:'string'},'codex-model':{type:'string',multiple:true}}})
|
|
100
121
|
if (v['register-cli']) console.log(JSON.stringify({cli:await installationCli('/installations',v['register-cli'])}))
|
|
101
122
|
else if (v.list) console.log(JSON.stringify(await listAgents('/installations')))
|
|
102
123
|
else {
|
|
@@ -104,7 +125,7 @@ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.ur
|
|
|
104
125
|
for await (const chunk of process.stdin) { token+=chunk; if(token.length>512) throw new Error('Token input is too long.') }
|
|
105
126
|
const providerValues=[v['codex-provider'],v['codex-provider-name'],v['codex-base-url'],v['codex-env-key'],v['codex-model']]
|
|
106
127
|
const codexProvider=providerValues.some(Boolean)?validateCodexProvider({id:v['codex-provider']||'',name:v['codex-provider-name']||v['codex-provider']||'',baseUrl:v['codex-base-url']||'',envKey:v['codex-env-key']||'',models:v['codex-model']||[]}):undefined
|
|
107
|
-
console.log(JSON.stringify(await createAgent({root:'/installations',hostRoot:v['host-root']||'',composeFile:v['compose-file']||'',name:v.name||'',purpose:v.purpose||'',token:token.trim(),cli:v.cli||'',image:v['relay-image'],isolation:v.isolation,codexProvider})))
|
|
128
|
+
console.log(JSON.stringify(await createAgent({root:'/installations',hostRoot:v['host-root']||'',composeFile:v['compose-file']||'',name:v.name||'',purpose:v.purpose||'',token:token.trim(),cli:v.cli||'',image:v['relay-image'],isolation:v.isolation,codexProvider,ledgerPort:v['ledger-port']===undefined?undefined:Number(v['ledger-port'])})))
|
|
108
129
|
}
|
|
109
130
|
} catch (error) { console.error((error as Error).message); process.exitCode=1 }
|
|
110
131
|
}
|
|
@@ -2,7 +2,7 @@ import { createServer, type Server, type IncomingMessage, type ServerResponse }
|
|
|
2
2
|
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto'
|
|
3
3
|
import { mkdir, readFile, writeFile, rename, open, unlink } from 'node:fs/promises'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
|
-
import { ControlStore, sessionTitle, type ControlGuard, type Owner, sameOwner, validOwner, ownerId, ownerEpoch } from './control-state.js'
|
|
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'
|
|
7
7
|
import { ownsRun } from './identity.js'
|
|
8
8
|
import { applicationId, validApplicationOrigin } from './application-origin.js'
|
|
@@ -74,6 +74,8 @@ export class ApplicationChannel {
|
|
|
74
74
|
controlDir: string; workspace?: string; initial: AiPreset
|
|
75
75
|
wake: () => void
|
|
76
76
|
cancel: (id: string) => Promise<void>
|
|
77
|
+
createTelegramPairing?: (bindingId: string, owner: Owner) => Promise<{ connected: true } | { connected: false; url: string; expiresAt: string }>
|
|
78
|
+
telegramAvailable?: () => boolean
|
|
77
79
|
aiControls?: {
|
|
78
80
|
catalog: () => Promise<ModelChoice[]>
|
|
79
81
|
select: (preset: AiPreset, expectedSession: string | null, guard?: ControlGuard) => Promise<unknown>
|
|
@@ -88,6 +90,29 @@ export class ApplicationChannel {
|
|
|
88
90
|
if (!binding || (shared && !binding.shareTelegram) || !sameOwner(binding.owner, state.owner)) throw new Error('Application authority does not permit controls')
|
|
89
91
|
return binding.owner
|
|
90
92
|
}
|
|
93
|
+
private async telegramConnection(bindingId: string) {
|
|
94
|
+
await this.sharedBinding(bindingId)
|
|
95
|
+
const owner = (await new ControlStore(this.options.controlDir, 900000).status()).owner
|
|
96
|
+
const connected = Boolean(telegramOwner(owner))
|
|
97
|
+
return this.options.telegramAvailable
|
|
98
|
+
? { connected, ready: this.options.telegramAvailable() }
|
|
99
|
+
: { connected }
|
|
100
|
+
}
|
|
101
|
+
private async createTelegramConnection(bindingId: string) {
|
|
102
|
+
const owner = await this.sharedBinding(bindingId)
|
|
103
|
+
const current = (await new ControlStore(this.options.controlDir, 900000).status()).owner
|
|
104
|
+
if (telegramOwner(current)) return { connected: true } as const
|
|
105
|
+
if (!this.options.createTelegramPairing) throw new Error('Telegram connection is unavailable')
|
|
106
|
+
return this.options.createTelegramPairing(bindingId, owner)
|
|
107
|
+
}
|
|
108
|
+
async claimTelegramConnection(token: string, telegramUserId: number, telegramChatId: number): Promise<boolean> {
|
|
109
|
+
const control = new ControlStore(this.options.controlDir, 900000)
|
|
110
|
+
const owner = await control.claimApplicationTelegramPairing(token, telegramUserId, telegramChatId, async (bindingId, expectedOwner) => {
|
|
111
|
+
const binding = (await this.bindings.list()).find(item => item.bindingId === bindingId)
|
|
112
|
+
return Boolean(binding?.shareTelegram && sameOwner(binding.owner, expectedOwner))
|
|
113
|
+
})
|
|
114
|
+
return Boolean(owner)
|
|
115
|
+
}
|
|
91
116
|
async controls(bindingId: string) {
|
|
92
117
|
await this.sharedBinding(bindingId)
|
|
93
118
|
if (!this.options.aiControls) throw new Error('Application controls unavailable')
|
|
@@ -233,8 +258,8 @@ export class ApplicationChannel {
|
|
|
233
258
|
}
|
|
234
259
|
const control = new ControlStore(this.options.controlDir, 900000)
|
|
235
260
|
if (!sameOwner(binding.owner, (await control.status()).owner)) throw new Error('Application authority revoked')
|
|
236
|
-
if (bytes && !this.options.
|
|
237
|
-
const staged = bytes ? await stageChatAttachment(this.options.
|
|
261
|
+
if (bytes && !this.options.controlDir) throw new Error('Invalid application attachment: control unavailable')
|
|
262
|
+
const staged = bytes ? await stageChatAttachment(this.options.controlDir, value.attachment!.name as string, bytes, value.text) : undefined
|
|
238
263
|
let run: RunRecord
|
|
239
264
|
try {
|
|
240
265
|
const execution = value.followTelegram ? await control.captureChoice(this.options.initial) : await control.captureApplicationChoice(this.options.initial, applicationScope(bindingId, value.scope), binding.shareTelegram === true && value.activateTelegram === true, requestedPreset, value.expectedNativeSessionId as string | undefined)
|
|
@@ -270,13 +295,33 @@ export class ApplicationChannel {
|
|
|
270
295
|
if (request.method === 'GET' && path === '/v1/registration') {
|
|
271
296
|
send(200, {ownerId: ownerId(binding.owner), bindingId: binding.bindingId, channel: binding.id}); return
|
|
272
297
|
}
|
|
298
|
+
if (path === '/v1/telegram' && request.method === 'GET') {
|
|
299
|
+
if ([...url.searchParams.keys()].length) throw new Error('Invalid application Telegram request')
|
|
300
|
+
if (!binding.shareTelegram) { send(403, { error: 'Application authority does not permit Telegram connection' }); return }
|
|
301
|
+
send(200, await this.telegramConnection(binding.bindingId)); return
|
|
302
|
+
}
|
|
303
|
+
if (path === '/v1/telegram/link' && request.method === 'POST') {
|
|
304
|
+
const chunks: Buffer[] = []; let size = 0
|
|
305
|
+
for await (const chunk of request) { size += chunk.length; if (size > 1024) throw new Error('Invalid application Telegram request'); chunks.push(chunk) }
|
|
306
|
+
if (Buffer.concat(chunks).toString('utf8').trim()) throw new Error('Invalid application Telegram request')
|
|
307
|
+
if (!binding.shareTelegram) { send(403, { error: 'Application authority does not permit Telegram connection' }); return }
|
|
308
|
+
send(200, await this.createTelegramConnection(binding.bindingId)); return
|
|
309
|
+
}
|
|
273
310
|
if (request.method === 'GET' && path === '/v1/runs') {
|
|
274
|
-
const
|
|
311
|
+
const owned = await this.runs.list()
|
|
312
|
+
const all = owned.filter(run => (run.application ?? run.delivery)?.bindingId === binding.bindingId)
|
|
313
|
+
const currentOwner = binding.shareTelegram ? telegramOwner((await new ControlStore(this.options.controlDir, 900000).status()).owner) : null
|
|
314
|
+
const telegram = currentOwner
|
|
315
|
+
? owned.filter(run => run.application === undefined && run.delivery === undefined && run.taskId === undefined && run.external === undefined && run.scheduled === undefined && run.telegramUserId === currentOwner.telegramUserId && run.ownerId === ownerId(currentOwner) && run.ownerEpoch === ownerEpoch(currentOwner) && (run.telegramApplication === undefined || run.telegramApplication.bindingId === binding.bindingId) && !all.some(item => item.id === run.id))
|
|
316
|
+
: []
|
|
317
|
+
const visible = [...all, ...telegram]
|
|
275
318
|
const before = url.searchParams.get('before')
|
|
276
|
-
const end = before ?
|
|
319
|
+
const end = before ? visible.findIndex(run => run.id === before) : visible.length
|
|
277
320
|
if (end < 0) throw new Error('Invalid application inbox cursor')
|
|
278
|
-
const records =
|
|
279
|
-
send(200, {runs: await Promise.all(records.map(run =>
|
|
321
|
+
const records = visible.slice(Math.max(0,end-100),end)
|
|
322
|
+
send(200, {runs: await Promise.all(records.map(run => (run.application ?? run.delivery)
|
|
323
|
+
? this.snapshot(binding.bindingId, run.id)
|
|
324
|
+
: Promise.resolve({id: run.id, scope: 'telegram', status: run.status, texts: run.texts, telegramUserId: run.telegramUserId, createdAt: run.createdAt}))), nextCursor:end>100?records[0].id:null}); return
|
|
280
325
|
}
|
|
281
326
|
if (path==='/v1/scope-control' && ['GET','POST'].includes(request.method ?? '')) {
|
|
282
327
|
const scope = url.searchParams.get('scope')
|
|
@@ -314,6 +359,32 @@ export class ApplicationChannel {
|
|
|
314
359
|
if(!await new Tasks(this.options.controlDir).decide(approvalMatch[1]))throw new Error('Unknown application approval')
|
|
315
360
|
send(200,{id:approvalMatch[1],decision:input.decision});return
|
|
316
361
|
}
|
|
362
|
+
const attachMatch = path.match(/^\/v1\/runs\/([A-Za-z0-9_-]{1,160})\/application$/)
|
|
363
|
+
if (attachMatch && request.method === 'POST') {
|
|
364
|
+
if (!binding.shareTelegram) { send(403, { error: 'Application authority does not permit Telegram admission' }); return }
|
|
365
|
+
const chunks: Buffer[] = []; let size = 0
|
|
366
|
+
for await (const chunk of request) { size += chunk.length; if (size > 65536) throw new Error('Invalid application admission size'); chunks.push(chunk) }
|
|
367
|
+
const input = JSON.parse(Buffer.concat(chunks).toString('utf8')) as { context?: unknown; scope?: unknown }
|
|
368
|
+
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => key !== 'context' && key !== 'scope')) throw new Error('Invalid application admission')
|
|
369
|
+
const scope = input.scope === undefined ? 'telegram' : input.scope
|
|
370
|
+
if (typeof scope !== 'string') throw new Error('Invalid application admission')
|
|
371
|
+
const target = await this.runs.get(attachMatch[1])
|
|
372
|
+
if (target?.telegramApplication?.bindingId === binding.bindingId) {
|
|
373
|
+
const same = target.telegramApplication.scope === scope &&
|
|
374
|
+
JSON.stringify(target.telegramApplication.context ?? null) === JSON.stringify(input.context ?? null)
|
|
375
|
+
if (same) { send(200, { id: target.id, scope: target.telegramApplication.scope, status: target.status }); return }
|
|
376
|
+
throw new Error('Application admission conflicts with prior context')
|
|
377
|
+
}
|
|
378
|
+
if (!target || target.application || target.telegramApplication || target.taskId || target.external || target.scheduled || target.telegramUserId === undefined) throw new Error('Unknown application run')
|
|
379
|
+
const state = await new ControlStore(this.options.controlDir, 900000).status()
|
|
380
|
+
const linked = telegramOwner(state.owner)
|
|
381
|
+
if (!linked || target.telegramUserId !== linked.telegramUserId || !sameOwner(binding.owner, state.owner)) throw new Error('Application authority does not permit Telegram admission')
|
|
382
|
+
if (target.status !== 'queued' && target.status !== 'running') throw new Error('Application admission conflicts with finished run')
|
|
383
|
+
const application = { bindingId: binding.bindingId, scope, requestId: target.id, context: input.context as Record<string, unknown> }
|
|
384
|
+
if (!validApplicationOrigin(application)) throw new Error('Invalid application admission')
|
|
385
|
+
const admitted = await this.runs.attachTelegramApplication(target.id, application)
|
|
386
|
+
send(200, { id: admitted.id, scope, status: admitted.status }); return
|
|
387
|
+
}
|
|
317
388
|
const match = path.match(/^\/v1\/runs\/(r_(?:app|schedule)_[a-f0-9]{64})(\/cancel)?$/)
|
|
318
389
|
if (match && ((!match[2] && request.method === 'GET') || (match[2] && request.method === 'POST'))) {
|
|
319
390
|
await this.snapshot(binding.bindingId, match[1])
|
package/src/application-cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { loadControlConfig } from './config.js'
|
|
3
|
+
import { loadControlConfig, telegramToken } from './config.js'
|
|
4
4
|
import { ControlStore, sameOwner, ownerId } from './control-state.js'
|
|
5
5
|
import { ApplicationBindings, applicationScope, validateApplicationRegistration } from './application-channel.js'
|
|
6
6
|
import { initialPreset } from './ai.js'
|
|
@@ -17,7 +17,7 @@ async function main() {
|
|
|
17
17
|
const config = loadControlConfig(), control = new ControlStore(config.controlDir, config.pairingTtlMs)
|
|
18
18
|
if (values.owner && values['owner-id']) throw new Error('Choose one owner registration form')
|
|
19
19
|
if (values['owner-id'] && (!values.id || !values['token-file'] || values.revoke || values.list || values['import-scope'])) throw new Error('--owner-id requires channel registration')
|
|
20
|
-
if (values.owner && (process.env
|
|
20
|
+
if (values.owner && (telegramToken(process.env) || !values.id || !values['token-file'] || values.revoke || values.list || values['import-scope'])) throw new Error('--owner bootstrap requires an agent without a Telegram bot')
|
|
21
21
|
const registrationToken = values['token-file'] ? (await readFile(values['token-file'], 'utf8')).trim() : null
|
|
22
22
|
if (values.owner || values['owner-id']) validateApplicationRegistration(values.id!, registrationToken)
|
|
23
23
|
const owner = values['owner-id'] ? await control.registerOwner(values['owner-id']) : values.owner ? await control.bootstrapApplicationOwner(Number(values.owner)) : (await control.status()).owner
|
package/src/approval-cli.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { loadControlConfig } from './config.js'
|
|
2
|
-
import {
|
|
3
|
-
import { RunStore } from './runs.js'
|
|
2
|
+
import { callDeliverySocket, socketPathFor } from './delivery-socket.js'
|
|
4
3
|
|
|
5
4
|
const args = process.argv.slice(2).filter((arg) => arg !== '--')
|
|
6
5
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -27,10 +26,12 @@ for (let i = 0; i < args.length; i++) {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
const config = loadControlConfig()
|
|
30
|
-
const
|
|
29
|
+
const socketPath = socketPathFor(config.controlDir)
|
|
31
30
|
|
|
32
31
|
if (checkId) {
|
|
33
|
-
const record = await
|
|
32
|
+
const record = await callDeliverySocket(socketPath, { op: 'approvalCheck', payload: { actionId: checkId } }) as {
|
|
33
|
+
actionId: string; decision: string; decidedAt?: string; decidedBy?: number
|
|
34
|
+
}
|
|
34
35
|
console.log(JSON.stringify({
|
|
35
36
|
ok: true,
|
|
36
37
|
actionId: checkId,
|
|
@@ -53,7 +54,13 @@ if (!runId) {
|
|
|
53
54
|
process.exit(1)
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const item = await
|
|
57
|
+
// The relay records the approval request and delivers its card; the decision
|
|
58
|
+
// arrives later through Telegram and is read back with --check.
|
|
59
|
+
const item = await callDeliverySocket(socketPath, {
|
|
60
|
+
op: 'enqueue',
|
|
61
|
+
payload: {
|
|
62
|
+
kind: 'approval', runId, approvalPrompt: prompt, approvalActionId: actionId,
|
|
63
|
+
...(replyTo !== undefined ? { replyToMessageId: replyTo } : {}),
|
|
64
|
+
},
|
|
65
|
+
}) as { outbox_id: string; id: string }
|
|
59
66
|
console.log(JSON.stringify({ ok: true, run: runId, actionId, outbox_id: item.id, status: 'requested' }))
|