@jc_stack/ez-agents 0.1.0-beta.33 → 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 +9 -16
- package/AGENTS.md +17 -9
- package/CHANGELOG.md +92 -0
- package/Dockerfile +11 -9
- package/README.md +32 -27
- package/THIRD_PARTY_NOTICES.md +4 -4
- 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 +42 -5
- package/docker/entrypoint.sh +1 -1
- package/docker/healthcheck.mjs +12 -4
- package/docker/run.ts +5 -6
- package/docker/smoke.mjs +32 -7
- package/docs/application-channel.md +38 -10
- package/docs/architecture/ai-selection.md +3 -1
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/channel-backend.md +4 -3
- package/docs/docker-runtime.md +106 -21
- package/docs/host-service.md +17 -6
- package/docs/local-qa.md +2 -2
- package/docs/plugin-guide.md +202 -0
- package/docs/plugins.md +63 -2
- package/docs/releasing.md +13 -0
- package/docs/repair.md +1 -1
- package/docs/responsive-channels.md +1 -1
- package/docs/scheduling.md +4 -4
- package/docs/setup.md +96 -16
- package/docs/standalone-cli.md +6 -5
- package/docs/upgrades.md +46 -23
- package/package.json +5 -5
- package/scripts/smoke-scheduler.ts +12 -4
- package/scripts/smoke.ts +14 -11
- package/src/agent-guidance.ts +6 -9
- package/src/agent-install.ts +44 -9
- package/src/ai-cli.ts +9 -7
- package/src/ai.ts +7 -7
- package/src/application-channel.ts +88 -17
- 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/codex-session.ts +5 -3
- package/src/config.ts +18 -28
- package/src/control-state.ts +187 -9
- 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 +110 -31
- package/src/files.ts +38 -11
- package/src/host-executor.ts +50 -17
- package/src/inbox.ts +15 -49
- package/src/index.ts +96 -52
- package/src/install-config.ts +2 -2
- package/src/install-tools.mjs +100 -23
- package/src/isolation.ts +25 -0
- package/src/ledger-endpoint.d.mts +2 -0
- package/src/ledger-endpoint.mjs +17 -0
- package/src/menu.ts +16 -8
- 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 +178 -26
- 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/setup.ts +11 -7
- package/src/software-status.ts +36 -4
- package/src/task-executor.ts +6 -3
- 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 +29 -9
- package/src/updates/status.mjs +25 -3
- package/src/updates/supervisor.mjs +31 -18
- package/src/version.ts +20 -0
- package/src/workspace-confine.ts +38 -0
- package/src/workspace.ts +24 -20
- package/templates/agent-purpose.md +1 -0
- package/templates/failure-review.md +1 -1
- package/templates/maintainer-purpose.md +1 -1
- package/test/agent-guidance.test.ts +18 -88
- package/test/agent-install.test.ts +48 -0
- package/test/ai-cli.test.ts +18 -0
- package/test/ai.test.ts +84 -0
- package/test/application-channel.test.ts +66 -15
- package/test/application-controls.test.ts +56 -3
- package/test/application-only.test.ts +16 -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 +63 -3
- package/test/codex-session.test.ts +6 -1
- package/test/config.test.ts +47 -37
- 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 +12 -8
- package/test/event-sources.test.ts +2 -2
- package/test/execution-authority.test.ts +9 -9
- package/test/executor.test.ts +47 -11
- package/test/failure.test.ts +18 -5
- package/test/files.test.ts +2 -2
- package/test/helpers/ledger.ts +18 -0
- package/test/host-executor.test.ts +80 -0
- package/test/inbox.test.ts +8 -10
- package/test/install-config.test.ts +18 -6
- package/test/install-tools.test.mjs +80 -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 +143 -5
- 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/scheduler.test.ts +2 -2
- 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 +102 -13
- package/test/workspace-confine.test.ts +47 -0
- package/test/workspace.test.ts +27 -11
- 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 -44
- 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 -15
- package/templates/agent/SOUL.md +0 -11
- package/templates/agent/USER.md +0 -5
- package/templates/agent-guidance.md +0 -114
- package/test/pagerduty.test.ts +0 -104
- package/test/update-attention.test.ts +0 -20
- package/test/workforce-watch.test.ts +0 -180
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import { mkdtemp, rm, open } from 'node:fs/promises'
|
|
3
|
+
import { mkdtemp, rm, open, mkdir, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { randomBytes } from 'node:crypto'
|
|
@@ -9,6 +9,41 @@ import { ControlStore } from '../src/control-state.js'
|
|
|
9
9
|
import { createAiMenu } from '../src/menu.js'
|
|
10
10
|
import { RunStore } from '../src/runs.js'
|
|
11
11
|
|
|
12
|
+
test('shared controls validate detected host presets against the host catalog', async t => {
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), 'ez-host-controls-'))
|
|
14
|
+
const previousTransport = process.env.EZ_EXECUTOR_TRANSPORT
|
|
15
|
+
const previousControl = process.env.EZ_CONTROL_DIR
|
|
16
|
+
process.env.EZ_EXECUTOR_TRANSPORT = 'host'
|
|
17
|
+
process.env.EZ_CONTROL_DIR = root
|
|
18
|
+
t.after(async () => {
|
|
19
|
+
if (previousTransport === undefined) delete process.env.EZ_EXECUTOR_TRANSPORT
|
|
20
|
+
else process.env.EZ_EXECUTOR_TRANSPORT = previousTransport
|
|
21
|
+
if (previousControl === undefined) delete process.env.EZ_CONTROL_DIR
|
|
22
|
+
else process.env.EZ_CONTROL_DIR = previousControl
|
|
23
|
+
await rm(root, {recursive:true, force:true})
|
|
24
|
+
})
|
|
25
|
+
await mkdir(join(root, 'host-executor'), {recursive:true})
|
|
26
|
+
await writeFile(join(root, 'host-executor', 'models.json'), JSON.stringify([
|
|
27
|
+
{cli:'grok', model:'grok-4.6', name:'Grok 4.6', efforts:['high']},
|
|
28
|
+
]))
|
|
29
|
+
const control = new ControlStore(root, 1000)
|
|
30
|
+
await control.requestPairing(42, 42); const owner = await control.approveOwner(42)
|
|
31
|
+
const menu = createAiMenu(control, 'codex')
|
|
32
|
+
const detected = {id:'detected_fixture', name:'Grok 4.6 High', cli:'grok', model:'grok-4.6', effort:'high'}
|
|
33
|
+
await control.aiState(menu.initial)
|
|
34
|
+
await control.savePreset(detected)
|
|
35
|
+
const initial = await control.captureChoice(menu.initial)
|
|
36
|
+
await control.selectPreset(detected.id, initial.sessionId, true)
|
|
37
|
+
const current = (await control.getActiveSession())!
|
|
38
|
+
const channel = new ApplicationChannel({controlDir:root, initial:menu.initial, aiControls:menu, wake:()=>{}, cancel:async()=>{}})
|
|
39
|
+
const binding = (await channel.bindings.register('shared', randomBytes(32).toString('base64url'), owner, true))!
|
|
40
|
+
|
|
41
|
+
const selected = await channel.changeControls(binding.bindingId, {
|
|
42
|
+
action:'select', presetId:detected.id, expectedSession:current.sessionId,
|
|
43
|
+
})
|
|
44
|
+
assert.equal(selected.ai.selectedId, detected.id)
|
|
45
|
+
})
|
|
46
|
+
|
|
12
47
|
test('private scope reset and model controls preserve running sessions and shared state', async t => {
|
|
13
48
|
const root = await mkdtemp(join(tmpdir(),'ez-private-controls-'))
|
|
14
49
|
const control = new ControlStore(root,1000)
|
|
@@ -75,6 +110,7 @@ test('application controls share native choices, protect hidden scopes and prese
|
|
|
75
110
|
await control.requestPairing(42, 42); const owner = await control.approveOwner(42)
|
|
76
111
|
const menu = createAiMenu(control, 'codex', async () => [
|
|
77
112
|
{cli:'codex', model:'gpt-6-astra', name:'Fixture model', efforts:['low','high']},
|
|
113
|
+
{cli:'codex', provider:'openrouter', model:'gpt-6-astra', name:'OpenRouter fixture', efforts:['low','high']},
|
|
78
114
|
], root, join(root, 'native-home'), async () => true)
|
|
79
115
|
const channel = new ApplicationChannel({controlDir:root, initial:menu.initial, aiControls:menu, wake:()=>{}, cancel:async()=>{}})
|
|
80
116
|
t.after(async()=>{await channel.stop(); await rm(root,{recursive:true,force:true})})
|
|
@@ -107,9 +143,11 @@ test('application controls share native choices, protect hidden scopes and prese
|
|
|
107
143
|
assert.deepEqual((await control.aiState(menu.initial)).presets,presetsBefore)
|
|
108
144
|
const selected = await request({action:'model',cli:'codex',model:'gpt-6-astra',effort:'high',expectedSession:old.sessionId})
|
|
109
145
|
assert.equal(selected.status,200)
|
|
110
|
-
|
|
146
|
+
const modelChoice = await control.captureChoice(menu.initial)
|
|
147
|
+
assert.equal(modelChoice.preset.effort,'high')
|
|
148
|
+
assert.notEqual(modelChoice.sessionId,old.sessionId)
|
|
111
149
|
assert.deepEqual((await new RunStore(root).get(run.id))!.execution,old)
|
|
112
|
-
const reset = await request({action:'new',expectedSession:
|
|
150
|
+
const reset = await request({action:'new',expectedSession:modelChoice.sessionId})
|
|
113
151
|
assert.equal(reset.status,200)
|
|
114
152
|
const next = (await control.getActiveSession())!
|
|
115
153
|
assert.notEqual(next.sessionId,old.sessionId)
|
|
@@ -119,6 +157,21 @@ test('application controls share native choices, protect hidden scopes and prese
|
|
|
119
157
|
assert.equal((await request()).status,401)
|
|
120
158
|
})
|
|
121
159
|
|
|
160
|
+
test('application model controls disambiguate the same model by provider', async t => {
|
|
161
|
+
const root=await mkdtemp(join(tmpdir(),'ez-app-provider-control-')),control=new ControlStore(root,1000)
|
|
162
|
+
await control.requestPairing(42,42);const owner=await control.approveOwner(42)
|
|
163
|
+
const menu=createAiMenu(control,'codex',async()=>[
|
|
164
|
+
{cli:'codex',model:'shared-model',name:'Native',efforts:[]},
|
|
165
|
+
{cli:'codex',provider:'openrouter',model:'shared-model',name:'OpenRouter',efforts:[]},
|
|
166
|
+
],root,join(root,'native-home'),async()=>true)
|
|
167
|
+
const channel=new ApplicationChannel({controlDir:root,initial:menu.initial,aiControls:menu,wake:()=>{},cancel:async()=>{}})
|
|
168
|
+
t.after(async()=>{await channel.stop();await rm(root,{recursive:true,force:true})})
|
|
169
|
+
const binding=(await channel.bindings.register('shared',randomBytes(32).toString('base64url'),owner,true))!
|
|
170
|
+
const current=await control.captureChoice(menu.initial)
|
|
171
|
+
await channel.changeControls(binding.bindingId,{action:'model',cli:'codex',provider:'openrouter',model:'shared-model',expectedSession:current.sessionId})
|
|
172
|
+
assert.equal((await control.captureChoice(menu.initial)).preset.provider,'openrouter')
|
|
173
|
+
})
|
|
174
|
+
|
|
122
175
|
test('atomic control mutations reject a replaced owner even when both active sessions are empty', async t => {
|
|
123
176
|
const root = await mkdtemp(join(tmpdir(),'ez-control-owner-'))
|
|
124
177
|
t.after(()=>rm(root,{recursive:true,force:true}))
|
|
@@ -13,18 +13,17 @@ import { ControlStore } from '../src/control-state.js'
|
|
|
13
13
|
import { RunStore } from '../src/runs.js'
|
|
14
14
|
import { EXECUTOR_REGISTRY } from '../src/executor.js'
|
|
15
15
|
import { ApplicationBindings } from '../src/application-channel.js'
|
|
16
|
+
import { initializeWorkspace } from '../src/workspace.js'
|
|
16
17
|
|
|
17
18
|
const waitFor = async (condition: () => Promise<boolean>) => {
|
|
18
19
|
for (let i=0;i<300;i++) { if (await condition()) return; await new Promise(r=>setTimeout(r,10)) }
|
|
19
20
|
throw new Error('Timed out')
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
test('application-only mode
|
|
23
|
-
assert.throws(()=>loadConfig({}), /
|
|
24
|
-
assert.throws(()=>loadConfig({
|
|
25
|
-
|
|
26
|
-
assert.throws(()=>loadConfig({EZ_TELEGRAM_ENABLED:'false',EZ_APPLICATION_PORT:'8110',EZ_CHANNEL_BACKEND_URL:'http://backend',EZ_CHANNEL_BACKEND_TOKEN:'secret'}), /native Ez executor/)
|
|
27
|
-
const config=loadConfig({EZ_TELEGRAM_ENABLED:'false',EZ_APPLICATION_PORT:'8110',TELEGRAM_BOT_TOKEN:'unused-secret'})
|
|
23
|
+
test('application-only mode is inferred from the absence of a bot token and requires a native listener', () => {
|
|
24
|
+
assert.throws(()=>loadConfig({}), /EZ_APPLICATION_PORT/)
|
|
25
|
+
assert.throws(()=>loadConfig({EZ_APPLICATION_PORT:'8110',EZ_CHANNEL_BACKEND_URL:'http://backend',EZ_CHANNEL_BACKEND_TOKEN:'secret'}), /native Ez executor/)
|
|
26
|
+
const config=loadConfig({EZ_APPLICATION_PORT:'8110'})
|
|
28
27
|
assert.equal(config.telegramBotToken,'')
|
|
29
28
|
assert.equal(config.telegramEnabled,false)
|
|
30
29
|
})
|
|
@@ -35,8 +34,8 @@ test('administrator bootstrap is explicit, local-only and cannot replace identit
|
|
|
35
34
|
const token=join(root,'token'); await writeFile(token,'x'.repeat(48),{mode:0o600})
|
|
36
35
|
const cli=fileURLToPath(new URL('../bin/ezenciel-agents-application.mjs',import.meta.url))
|
|
37
36
|
const args=[cli,'--id','app','--token-file',token,'--owner','42']
|
|
38
|
-
const env={...process.env,EZ_CONTROL_DIR:root,EZ_RUN_ID:'',
|
|
39
|
-
assert.notEqual(spawnSync(process.execPath,args,{env:{...env,
|
|
37
|
+
const env={...process.env,EZ_CONTROL_DIR:root,EZ_RUN_ID:'',TELEGRAM_BOT_TOKEN:''}
|
|
38
|
+
assert.notEqual(spawnSync(process.execPath,args,{env:{...env,TELEGRAM_BOT_TOKEN:'fixture'}}).status,0)
|
|
40
39
|
assert.notEqual(spawnSync(process.execPath,args,{env:{...env,EZ_RUN_ID:'r_agent'}}).status,0)
|
|
41
40
|
assert.equal((await new ControlStore(root,1000).status()).owner,null)
|
|
42
41
|
const invalidToken=join(root,'invalid-token');await writeFile(invalidToken,'short')
|
|
@@ -60,9 +59,10 @@ test('administrator bootstrap is explicit, local-only and cannot replace identit
|
|
|
60
59
|
|
|
61
60
|
test('botless daemon executes application turn and rejects Telegram-origin work/outbound', async t => {
|
|
62
61
|
const root=await mkdtemp(join(tmpdir(),'ez-app-only-runtime-'))
|
|
62
|
+
const purpose=join(root,'purpose.md');await writeFile(purpose,'Fixture application agent\n');await initializeWorkspace(root,purpose)
|
|
63
63
|
const portServer=createServer();await new Promise<void>(r=>portServer.listen(0,'127.0.0.1',r))
|
|
64
64
|
const port=(portServer.address() as {port:number}).port;await new Promise<void>(r=>portServer.close(()=>r()))
|
|
65
|
-
const config=loadConfig({
|
|
65
|
+
const config=loadConfig({EZ_APPLICATION_PORT:String(port),EZ_CONTROL_DIR:root,EZ_AGENT_WORKSPACE:root,EZ_EXECUTOR_CLI:'codex'})
|
|
66
66
|
const native='11111111-1111-1111-1111-111111111111', fixture=join(root,'engine.mjs')
|
|
67
67
|
await writeFile(fixture,`import {spawnSync} from 'node:child_process';if(process.env.TELEGRAM_BOT_TOKEN)throw Error('secret leak');console.log(JSON.stringify({type:'thread.started',thread_id:${JSON.stringify(native)}}));const r=spawnSync(process.execPath,[${JSON.stringify(fileURLToPath(new URL('../bin/ezenciel-agents-message.mjs',import.meta.url)))},'--text','Application reply'],{env:process.env});process.exit(r.status);`)
|
|
68
68
|
const original=EXECUTOR_REGISTRY.codex,require=createRequire(import.meta.url)
|
|
@@ -89,12 +89,15 @@ test('botless daemon executes application turn and rejects Telegram-origin work/
|
|
|
89
89
|
assert.equal((await runs.get(legacy.id))?.status,'queued')
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
test('Docker health accepts application readiness
|
|
92
|
+
test('Docker health accepts application readiness when the agent has no bot', async t => {
|
|
93
93
|
const root=await mkdtemp(join(tmpdir(),'ez-app-only-health-'))
|
|
94
94
|
t.after(()=>rm(root,{recursive:true,force:true}))
|
|
95
|
-
await writeFile(join(root,'heartbeat.json'),JSON.stringify({at:Date.now(),polling:false,applicationOnly:true}))
|
|
95
|
+
await writeFile(join(root,'heartbeat.json'),JSON.stringify({at:Date.now(),polling:false,applicationOnly:true,telegramConfigured:false}))
|
|
96
96
|
const command=fileURLToPath(new URL('../docker/healthcheck.mjs',import.meta.url))
|
|
97
97
|
const env={...process.env,EZ_HEALTH_RELAY_CONTROL_DIR:root,EZ_EXECUTOR_TRANSPORT:'local'}
|
|
98
|
-
assert.equal(spawnSync(process.execPath,[command],{env
|
|
99
|
-
|
|
98
|
+
assert.equal(spawnSync(process.execPath,[command],{env}).status,0)
|
|
99
|
+
await writeFile(join(root,'heartbeat.json'),JSON.stringify({at:Date.now(),polling:false,applicationOnly:false,telegramConfigured:false}))
|
|
100
|
+
assert.notEqual(spawnSync(process.execPath,[command],{env}).status,0)
|
|
101
|
+
await writeFile(join(root,'heartbeat.json'),JSON.stringify({at:Date.now(),polling:false,applicationOnly:true,telegramConfigured:true}))
|
|
102
|
+
assert.notEqual(spawnSync(process.execPath,[command],{env}).status,0)
|
|
100
103
|
})
|
|
@@ -43,7 +43,6 @@ test('structured photo transport and deterministic reply recovery', async () =>
|
|
|
43
43
|
test('backend dispatch keeps owner/private gate and never starts a CLI', async () => {
|
|
44
44
|
const { createRelay } = await import('../src/index.js')
|
|
45
45
|
const { ControlStore } = await import('../src/control-state.js')
|
|
46
|
-
const { recoverInterruptedRuns } = await import('../docker/recovery.js')
|
|
47
46
|
const root = await mkdtemp(join(tmpdir(), 'channel-gate-'))
|
|
48
47
|
const originalFetch = globalThis.fetch
|
|
49
48
|
let calls = 0
|
|
@@ -68,16 +67,14 @@ test('backend dispatch keeps owner/private gate and never starts a CLI', async (
|
|
|
68
67
|
assert.equal(calls, 1)
|
|
69
68
|
const store = new RunStore(root)
|
|
70
69
|
assert.equal((await store.get('tg_3'))?.status, 'completed')
|
|
71
|
-
await store.patch('tg_3', { status: 'running' })
|
|
72
|
-
await recoverInterruptedRuns(root, true)
|
|
73
|
-
assert.equal((await store.get('tg_3'))?.status, 'queued')
|
|
70
|
+
await store.patch('tg_3', { status: 'running', backendSubmitted: true })
|
|
74
71
|
const cancel = update(4, 42); cancel.message.text = '/cancel'
|
|
75
72
|
await relay.bot.handleUpdate(cancel)
|
|
76
|
-
assert.equal((await store.get('tg_3'))?.status, '
|
|
73
|
+
assert.equal((await store.get('tg_3'))?.status, 'running', 'cancel must retain submitted operation polling')
|
|
77
74
|
} finally { await relay.stop(); globalThis.fetch = originalFetch; await rm(root, { recursive: true, force: true }) }
|
|
78
75
|
})
|
|
79
76
|
|
|
80
|
-
test('backend mode cannot dispatch restricted, external
|
|
77
|
+
test('backend mode cannot dispatch restricted, external or scheduled runs', async () => {
|
|
81
78
|
const { createRelay } = await import('../src/index.js')
|
|
82
79
|
const { ControlStore } = await import('../src/control-state.js')
|
|
83
80
|
const root = await mkdtemp(join(tmpdir(), 'channel-authority-'))
|
|
@@ -92,7 +89,6 @@ test('backend mode cannot dispatch restricted, external, scheduled or update run
|
|
|
92
89
|
await runs.create({ ...common, id: 'restricted', taskId: `task_${'a'.repeat(32)}` })
|
|
93
90
|
await runs.create({ ...common, id: 'external', external: { sourceId: 'fixture', bindingId: 'binding', eventIds: ['1'] } })
|
|
94
91
|
await runs.create({ ...common, id: 'scheduled', scheduled: { id: 's', revision: 'r', dueAt: new Date().toISOString(), pairedAt: owner.pairedAt } })
|
|
95
|
-
await runs.create({ ...common, id: 'r_update_fixture' })
|
|
96
92
|
await relay.drainSources()
|
|
97
93
|
assert.equal(calls, 0); assert.equal(launches, 0)
|
|
98
94
|
assert.ok((await runs.list()).every(run => run.status === 'failed'))
|
|
@@ -8,11 +8,14 @@ import {ControlStore} from '../src/control-state.js'
|
|
|
8
8
|
import {RunStore} from '../src/runs.js'
|
|
9
9
|
import {authorizeDeliveryContext,captureDeliveryContext} from '../src/delivery-context.mjs'
|
|
10
10
|
import {nativeTasks,nativeTaskBinding} from '../src/plugins/native-tasks.mjs'
|
|
11
|
+
import {serveTestLedger} from './helpers/ledger.js'
|
|
11
12
|
|
|
12
13
|
test('authenticated connection sends text and files through ordinary outbox receipts without a native run',async()=>{
|
|
13
14
|
const root=await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(),'ez-channel-delivery-'))),workspace=path.join(root,'mind'),controlDir=path.join(root,'control'),home=path.join(root,'tools'),hostConfig=path.join(root,'host-executor.json')
|
|
14
15
|
for(const dir of [workspace,controlDir,home])await fs.mkdir(dir)
|
|
15
16
|
const relay=createRelay({controlDir,workspace,pairingTtlMs:1000,executorTimeoutMs:1000,executorCli:'grok',telegramBotToken:'fixture'},async()=>{throw Error('Must not launch native execution')})
|
|
17
|
+
// The message CLI child reaches the relay memory ledger through the delivery socket.
|
|
18
|
+
const ledger=await serveTestLedger(controlDir)
|
|
16
19
|
const calls:string[]=[];relay.bot.api.config.use(async(_prev,method)=>{calls.push(method);return {ok:true,result:{message_id:42}} as never})
|
|
17
20
|
try {
|
|
18
21
|
await fs.writeFile(path.join(home,'config.json'),JSON.stringify({schemaVersion:1,workspace,hostConfig}))
|
|
@@ -38,7 +41,7 @@ test('authenticated connection sends text and files through ordinary outbox rece
|
|
|
38
41
|
await relay.drainOutbox();assert.equal(calls.length,2);await assert.rejects(store.waitForDelivery(queued.id),/revoked/)
|
|
39
42
|
await assert.rejects(nativeTasks(home,['--text','denied'],{command:'message',deliveryContext:context}),/revoked/)
|
|
40
43
|
await assert.rejects(store.ownerDeliveryReceipt(context,queued.id),/revoked/)
|
|
41
|
-
} finally {await relay.stop();await fs.rm(root,{recursive:true,force:true})}
|
|
44
|
+
} finally {await relay.stop();await ledger.stop();await fs.rm(root,{recursive:true,force:true})}
|
|
42
45
|
})
|
|
43
46
|
test('delivery authority cannot revive after Telegram relink or same-time owner replacement',async t=>{
|
|
44
47
|
const root=await fs.mkdtemp(path.join(os.tmpdir(),'ez-channel-delivery-epoch-'))
|
|
@@ -16,6 +16,7 @@ import { ownsRun, isOwner } from '../src/identity.js'
|
|
|
16
16
|
import { createRelay } from '../src/index.js'
|
|
17
17
|
import { EXECUTOR_REGISTRY } from '../src/executor.js'
|
|
18
18
|
import { loadConfig } from '../src/config.js'
|
|
19
|
+
import { serveTestLedger } from './helpers/ledger.js'
|
|
19
20
|
|
|
20
21
|
const secret = () => randomBytes(32).toString('base64url')
|
|
21
22
|
const waitFor = async (check: () => Promise<boolean>) => {
|
|
@@ -87,13 +88,15 @@ test('web owner uses the standard schedule CLI, executor and outbox with Telegra
|
|
|
87
88
|
if(!scheduled){const r=spawnSync(process.execPath,[${JSON.stringify(scheduleCli)},'create','followup','--now','--name','Followup','--text','Finish the requested work'],{env:process.env,encoding:'utf8'});if(r.status)throw Error(r.stderr)}
|
|
88
89
|
const r=spawnSync(process.execPath,[${JSON.stringify(messageCli)},'--text',scheduled?'Scheduled reply':'Chat reply'],{env:process.env,encoding:'utf8'});if(r.status)throw Error(r.stderr);`)
|
|
89
90
|
EXECUTOR_REGISTRY.grok={...original,command:process.execPath,buildArgs:()=>['--import',require.resolve('tsx'),fixture]}
|
|
90
|
-
const config=loadConfig({
|
|
91
|
+
const config=loadConfig({EZ_APPLICATION_PORT:'8787',EZ_CONTROL_DIR:root,EZ_AGENT_WORKSPACE:root,EZ_EXECUTOR_CLI:'grok'})
|
|
91
92
|
const relay=createRelay(config), control=new ControlStore(root,60000), runs=new RunStore(root)
|
|
93
|
+
// Engine children reach the relay memory ledger through the delivery socket.
|
|
94
|
+
const ledger=await serveTestLedger(root)
|
|
92
95
|
const owner=await control.registerOwner('web-owner'),token=secret()
|
|
93
96
|
const binding=(await relay.applicationChannel.bindings.register('web',token,owner))!
|
|
94
97
|
const address=await relay.applicationChannel.listen(0) as {port:number}
|
|
95
98
|
const timer=setInterval(()=>{void relay.drainSources();void relay.drainOutbox()},25)
|
|
96
|
-
t.after(async()=>{clearInterval(timer);await relay.stop();EXECUTOR_REGISTRY.grok=original;await rm(root,{recursive:true,force:true})})
|
|
99
|
+
t.after(async()=>{clearInterval(timer);await relay.stop();EXECUTOR_REGISTRY.grok=original;await ledger.stop();await rm(root,{recursive:true,force:true})})
|
|
97
100
|
const first=await relay.applicationChannel.submit(binding.bindingId,{requestId:'chat',scope:'main',text:'Schedule a followup'})
|
|
98
101
|
await waitFor(async()=> (await runs.get(first.id))?.status==='completed')
|
|
99
102
|
await waitFor(async()=> (await runs.list()).some(r=>r.scheduled && r.status==='completed'))
|
|
@@ -141,7 +144,7 @@ for (const mode of ['owner','web','telegram']) test(`${mode} revocation stops it
|
|
|
141
144
|
const control=new ControlStore(root,60000),runs=new RunStore(root),scheduler=new Scheduler(root)
|
|
142
145
|
await control.requestPairing(42,42)
|
|
143
146
|
const owner=await control.approveOwner(42)
|
|
144
|
-
const relay=createRelay(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
147
|
+
const relay=createRelay(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_CONTROL_DIR:root,EZ_AGENT_WORKSPACE:root,EZ_EXECUTOR_CLI:'grok'}),async()=>({child:spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{detached:true,stdio:'pipe'}),stdout:'',cleanup:async()=>{}}))
|
|
145
148
|
relay.bot.api.config.use(async()=>({ok:true,result:true}) as never)
|
|
146
149
|
t.after(async()=>{await relay.stop();await rm(root,{recursive:true,force:true})})
|
|
147
150
|
if (mode==='telegram') {
|
|
@@ -6,15 +6,67 @@ import {tmpdir} from 'node:os'
|
|
|
6
6
|
import path from 'node:path'
|
|
7
7
|
import {startExecutorJob} from '../src/executor.js'
|
|
8
8
|
|
|
9
|
+
test('a package-bin Codex shim cannot retarget another agent home', async () => {
|
|
10
|
+
const root = await mkdtemp(path.join(tmpdir(), 'ez-codex-host-cli-'))
|
|
11
|
+
const hostBin = path.join(root, 'host-bin')
|
|
12
|
+
const packageBin = path.join(root, 'package-bin')
|
|
13
|
+
const controlDir = path.join(root, 'agent')
|
|
14
|
+
const priorHome = process.env.HOME, priorPath = process.env.PATH
|
|
15
|
+
try {
|
|
16
|
+
await mkdir(hostBin); await mkdir(packageBin); await mkdir(path.join(root, '.codex'))
|
|
17
|
+
await writeFile(path.join(root, '.codex/auth.json'), '{}')
|
|
18
|
+
await writeFile(path.join(packageBin, 'codex'), `#!${process.execPath}
|
|
19
|
+
require('fs').writeFileSync(require('path').join(process.env.HOME, 'hijacked'), 'yes'); process.exit(2)
|
|
20
|
+
`, { mode: 0o700 })
|
|
21
|
+
await writeFile(path.join(hostBin, 'codex'), `#!${process.execPath}\nconsole.log(process.env.CODEX_HOME)`, { mode: 0o700 })
|
|
22
|
+
process.env.HOME = root
|
|
23
|
+
process.env.PATH = hostBin + path.delimiter + (priorPath ?? '')
|
|
24
|
+
await ownerRun(controlDir, 'r_test')
|
|
25
|
+
const job = await startExecutorJob(['hello'], {
|
|
26
|
+
workspace: root, controlDir, binDir: packageBin, cli: 'codex', runId: 'r_test', timeoutMs: 5000,
|
|
27
|
+
})
|
|
28
|
+
let output = ''
|
|
29
|
+
job.child.stdout?.on('data', chunk => output += chunk)
|
|
30
|
+
assert.equal(await new Promise(resolve => job.child.once('close', resolve)), 0)
|
|
31
|
+
await job.cleanup()
|
|
32
|
+
assert.equal(output.trim(), path.join(controlDir, 'cli/codex'))
|
|
33
|
+
await assert.rejects(readFile(path.join(root, 'hijacked')))
|
|
34
|
+
} finally {
|
|
35
|
+
if (priorHome === undefined) delete process.env.HOME; else process.env.HOME = priorHome
|
|
36
|
+
if (priorPath === undefined) delete process.env.PATH; else process.env.PATH = priorPath
|
|
37
|
+
await rm(root, { recursive: true, force: true })
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('a package-bin Codex shim is not a fallback when the host CLI is missing', async () => {
|
|
42
|
+
const root = await mkdtemp(path.join(tmpdir(), 'ez-codex-host-cli-missing-'))
|
|
43
|
+
const packageBin = path.join(root, 'package-bin')
|
|
44
|
+
const controlDir = path.join(root, 'agent')
|
|
45
|
+
const priorPath = process.env.PATH
|
|
46
|
+
try {
|
|
47
|
+
await mkdir(packageBin)
|
|
48
|
+
await writeFile(path.join(packageBin, 'codex'), `#!${process.execPath}\nprocess.exit(91)\n`, { mode: 0o700 })
|
|
49
|
+
process.env.PATH = '/usr/bin:/bin'
|
|
50
|
+
await ownerRun(controlDir, 'r_test')
|
|
51
|
+
await assert.rejects(startExecutorJob(['hello'], {
|
|
52
|
+
workspace: root, controlDir, binDir: packageBin, cli: 'codex', runId: 'r_test', timeoutMs: 5000,
|
|
53
|
+
}), /Native CLI codex is not executable on the host PATH/)
|
|
54
|
+
} finally {
|
|
55
|
+
if (priorPath === undefined) delete process.env.PATH; else process.env.PATH = priorPath
|
|
56
|
+
await rm(root, { recursive: true, force: true })
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
9
60
|
test('Codex shares only auth through a link and keeps each agent runtime state separate',async()=>{
|
|
10
61
|
const root=await mkdtemp(path.join(tmpdir(),'ez-codex-context-'))
|
|
11
|
-
const priorHome=process.env.HOME,priorPath=process.env.PATH
|
|
62
|
+
const priorHome=process.env.HOME,priorPath=process.env.PATH,priorIsolation=process.env.EZ_ISOLATION
|
|
12
63
|
try{
|
|
13
64
|
await mkdir(path.join(root,'.codex'));await mkdir(path.join(root,'bin'))
|
|
14
65
|
await writeFile(path.join(root,'.codex/auth.json'),'{}')
|
|
15
66
|
await writeFile(path.join(root,'.codex/config.toml'),'# unrelated personal config')
|
|
16
67
|
await writeFile(path.join(root,'bin/codex'),`#!${process.execPath}\nconsole.log(process.env.CODEX_HOME)`,{mode:0o700})
|
|
17
68
|
process.env.HOME=root;process.env.PATH=path.join(root,'bin')+path.delimiter+priorPath
|
|
69
|
+
delete process.env.EZ_ISOLATION
|
|
18
70
|
for(const agent of ['one','two']){
|
|
19
71
|
const controlDir=path.join(root,agent)
|
|
20
72
|
await ownerRun(controlDir, 'r_test')
|
|
@@ -27,9 +79,17 @@ test('Codex shares only auth through a link and keeps each agent runtime state s
|
|
|
27
79
|
assert.deepEqual(await readdir(home),['auth.json'])
|
|
28
80
|
assert.equal(await readlink(path.join(home,'auth.json')),path.join(root,'.codex/auth.json'))
|
|
29
81
|
}
|
|
82
|
+
process.env.EZ_ISOLATION='isolated'
|
|
83
|
+
const isolated=path.join(root,'isolated')
|
|
84
|
+
await ownerRun(isolated, 'r_test')
|
|
85
|
+
const isolatedJob=await startExecutorJob(['hello'],{workspace:root,controlDir:isolated,binDir:path.join(root,'bin'),cli:'codex',runId:'r_test',timeoutMs:5000})
|
|
86
|
+
assert.equal(await new Promise(resolve=>isolatedJob.child.once('close',resolve)),0)
|
|
87
|
+
await isolatedJob.cleanup()
|
|
88
|
+
await assert.rejects(readlink(path.join(isolated,'cli/codex/auth.json')),{code:'ENOENT'})
|
|
30
89
|
}finally{
|
|
31
90
|
if(priorHome===undefined)delete process.env.HOME;else process.env.HOME=priorHome
|
|
32
91
|
if(priorPath===undefined)delete process.env.PATH;else process.env.PATH=priorPath
|
|
92
|
+
if(priorIsolation===undefined)delete process.env.EZ_ISOLATION;else process.env.EZ_ISOLATION=priorIsolation
|
|
33
93
|
await rm(root,{recursive:true,force:true})
|
|
34
94
|
}
|
|
35
95
|
})
|
|
@@ -50,11 +110,11 @@ if(q.method==='turn/start'){send({id:q.id,result:{turn:{id:'one'}}});send({metho
|
|
|
50
110
|
send({id:q.id,result:q.method==='thread/goal/get'?{goal:null}:{}});});setInterval(()=>{},1000);
|
|
51
111
|
`,{mode:0o700})
|
|
52
112
|
process.env.HOME=root;process.env.PATH=bin+path.delimiter+priorPath
|
|
53
|
-
await assert.rejects(startExecutorJob(['test'],{workspace:root,controlDir,binDir:bin,cli:'codex',runId:'r_schedule_/../../escape',timeoutMs:0}),/Invalid native task run ID/)
|
|
113
|
+
await assert.rejects(startExecutorJob(['test'],{workspace:root,controlDir,binDir:bin,cli:'codex',runId:'r_schedule_/../../escape',timeoutMs:0,nativeSession:true}),/Invalid native task run ID/)
|
|
54
114
|
await ownerRun(controlDir, 'r_pair_fixture')
|
|
55
115
|
await Promise.all(['r_schedule_one','r_schedule_two'].map(async runId=>{
|
|
56
116
|
await ownerRun(controlDir, runId)
|
|
57
|
-
const job=await startExecutorJob(['test'],{workspace:root,controlDir,binDir:bin,cli:'codex',runId,timeoutMs:0})
|
|
117
|
+
const job=await startExecutorJob(['test'],{workspace:root,controlDir,binDir:bin,cli:'codex',runId,timeoutMs:0,nativeSession:true})
|
|
58
118
|
assert.equal(await new Promise(resolve=>job.child.once('close',resolve)),0);await job.cleanup()
|
|
59
119
|
const home=path.join(controlDir,'cli/codex/tasks',runId)
|
|
60
120
|
assert.equal(JSON.parse(await readFile(path.join(home,'observed.json'),'utf8')).home,home)
|
|
@@ -43,7 +43,8 @@ if(q.method==='thread/goal/get'){
|
|
|
43
43
|
child.stdin.write=((chunk:any,...args:any[])=>{try{const q=JSON.parse(String(chunk));if(q.method==='turn/start'){turnPrompt=q.params.input[0].text;turnPolicy=q.params.sandboxPolicy}requests.push(q.method);if(q.method==='thread/start'){threadConfig=q.params.config;threadSandbox=q.params.sandbox}}catch{};return (write as any)(chunk,...args)}) as typeof child.stdin.write
|
|
44
44
|
return child
|
|
45
45
|
}
|
|
46
|
-
const
|
|
46
|
+
const codexProvider=mode==='tool-goal'?{id:'openrouter',name:'OpenRouter',baseUrl:'https://openrouter.ai/api/v1',envKey:'OPENROUTER_API_KEY',models:['open/model']}:undefined
|
|
47
|
+
const result=await runCodexSession({workspace:'/tmp',controlDir:'/tmp/control',sharedWorkspace:'/canonical',additionalWorkspaces:['/agent/mind/work'],prompt,codexProvider,model:codexProvider?'open/model':undefined,
|
|
47
48
|
...(mode==='external'?{codexSandbox:'external' as const}:{})},{launch,emit:line=>output.push(line)})
|
|
48
49
|
assert.equal(threadSandbox!,mode==='external'?'danger-full-access':'workspace-write')
|
|
49
50
|
assert.deepEqual(turnPolicy,mode==='external'?{type:'externalSandbox',networkAccess:'enabled'}:undefined)
|
|
@@ -52,6 +53,10 @@ if(q.method==='thread/goal/get'){
|
|
|
52
53
|
if(mode==='long-goal')assert.ok(prompt.length>4000)
|
|
53
54
|
assert.ok(threadConfig['sandbox_workspace_write.writable_roots'].includes('/canonical'))
|
|
54
55
|
assert.ok(threadConfig['sandbox_workspace_write.writable_roots'].includes('/agent/mind/work'))
|
|
56
|
+
if(codexProvider){
|
|
57
|
+
assert.equal(threadConfig.model_provider,'openrouter')
|
|
58
|
+
assert.deepEqual(threadConfig['model_providers.openrouter'],{name:'OpenRouter',base_url:'https://openrouter.ai/api/v1',env_key:'OPENROUTER_API_KEY',wire_api:'responses',supports_websockets:false})
|
|
59
|
+
}
|
|
55
60
|
assert.equal(result,['external','plain','goal','long-goal','tool-goal'].includes(mode)?0:1)
|
|
56
61
|
assert.equal(requests.filter(x=>x==='turn/start').length,1,'transport must not send goal continuation prompts')
|
|
57
62
|
assert.equal(requests.filter(x=>x==='thread/goal/set').length,0)
|
package/test/config.test.ts
CHANGED
|
@@ -2,13 +2,13 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import test from 'node:test'
|
|
3
3
|
import { loadConfig } from '../src/config.js'
|
|
4
4
|
|
|
5
|
-
test('requires
|
|
6
|
-
assert.throws(() => loadConfig({}), /
|
|
5
|
+
test('requires an application listener when no Telegram bot token is present', () => {
|
|
6
|
+
assert.throws(() => loadConfig({}), /EZ_APPLICATION_PORT/)
|
|
7
7
|
})
|
|
8
8
|
|
|
9
9
|
test('uses a relative agent workspace and protected control state defaults', () => {
|
|
10
10
|
const config = loadConfig({
|
|
11
|
-
TELEGRAM_BOT_TOKEN: '
|
|
11
|
+
TELEGRAM_BOT_TOKEN: '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',
|
|
12
12
|
EZ_AGENT_WORKSPACE: './fixture-agent',
|
|
13
13
|
EZ_CONTROL_DIR: './fixture-control',
|
|
14
14
|
})
|
|
@@ -19,58 +19,68 @@ test('uses a relative agent workspace and protected control state defaults', ()
|
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
test('rejects malformed timeouts', () => {
|
|
22
|
-
assert.equal(loadConfig({ TELEGRAM_BOT_TOKEN: '
|
|
23
|
-
assert.throws(() => loadConfig({ TELEGRAM_BOT_TOKEN: '
|
|
22
|
+
assert.equal(loadConfig({ TELEGRAM_BOT_TOKEN: '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY', EZ_EXECUTOR_TIMEOUT_SECONDS: '300' }).executorTimeoutMs, 0)
|
|
23
|
+
assert.throws(() => loadConfig({ TELEGRAM_BOT_TOKEN: '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY', EZ_PAIRING_TTL_SECONDS: 'bad' }), /positive integer/)
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
test('loads executor CLI configuration with agy fallback', () => {
|
|
27
|
-
const defaultConfig = loadConfig({ TELEGRAM_BOT_TOKEN: '
|
|
27
|
+
const defaultConfig = loadConfig({ TELEGRAM_BOT_TOKEN: '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY' })
|
|
28
28
|
assert.equal(defaultConfig.executorCli, 'agy')
|
|
29
29
|
|
|
30
|
-
const customConfig = loadConfig({ TELEGRAM_BOT_TOKEN: '
|
|
30
|
+
const customConfig = loadConfig({ TELEGRAM_BOT_TOKEN: '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY', EZ_EXECUTOR_CLI: 'claude' })
|
|
31
31
|
assert.equal(customConfig.executorCli, 'claude')
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('Codex context limit is configurable and rejects invalid values', () => {
|
|
35
|
-
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
36
|
-
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
35
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY'}).codexAutoCompactTokens,undefined)
|
|
36
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_CODEX_AUTO_COMPACT_TOKENS:'32000'}).codexAutoCompactTokens,32000)
|
|
37
37
|
for(const value of ['0','-1','bad','1.5','9007199254740992'])
|
|
38
|
-
assert.throws(()=>loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
38
|
+
assert.throws(()=>loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_CODEX_AUTO_COMPACT_TOKENS:value}),/positive integer/)
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
test('PagerDuty Stocks monitoring requires a routing key and validates its target', () => {
|
|
42
|
-
assert.throws(
|
|
43
|
-
() => loadConfig({ TELEGRAM_BOT_TOKEN: 'test', EZ_PAGERDUTY_STOCKS_HEALTH_URL: 'http://stocks.test/health/critical' }),
|
|
44
|
-
/PAGERDUTY_ROUTING_KEY is required/,
|
|
45
|
-
)
|
|
46
|
-
assert.throws(
|
|
47
|
-
() => loadConfig({ TELEGRAM_BOT_TOKEN: 'test', PAGERDUTY_ROUTING_KEY: 'key', EZ_PAGERDUTY_STOCKS_HEALTH_URL: 'file:///private/health' }),
|
|
48
|
-
/absolute HTTP\(S\) URL/,
|
|
49
|
-
)
|
|
50
|
-
const config = loadConfig({
|
|
51
|
-
TELEGRAM_BOT_TOKEN: 'test',
|
|
52
|
-
PAGERDUTY_ROUTING_KEY: 'pagerduty-key',
|
|
53
|
-
EZ_PAGERDUTY_STOCKS_HEALTH_URL: 'http://stocks.test/health/critical',
|
|
54
|
-
EZ_PAGERDUTY_POLL_SECONDS: '45',
|
|
55
|
-
EZ_PAGERDUTY_FAILURE_THRESHOLD: '4',
|
|
56
|
-
})
|
|
57
|
-
assert.equal(config.pagerDutyRoutingKey, 'pagerduty-key')
|
|
58
|
-
assert.equal(config.pagerDutyStocksHealthUrl, 'http://stocks.test/health/critical')
|
|
59
|
-
assert.equal(config.pagerDutyPollMs, 45_000)
|
|
60
|
-
assert.equal(config.pagerDutyFailureThreshold, 4)
|
|
61
|
-
})
|
|
62
41
|
|
|
63
42
|
test('external Codex isolation requires explicit native local deployment', () => {
|
|
64
|
-
const env = {
|
|
43
|
+
const env = {EZ_APPLICATION_PORT:'8110', EZ_EXECUTOR_TRANSPORT:'local', EZ_CODEX_SANDBOX:'external'}
|
|
65
44
|
assert.equal(loadConfig(env).codexSandbox, 'external')
|
|
66
|
-
assert.equal(loadConfig({...env,
|
|
67
|
-
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
68
|
-
|
|
69
|
-
|
|
45
|
+
assert.equal(loadConfig({...env,TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY'}).codexSandbox, 'external')
|
|
46
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY'}).codexSandbox, undefined)
|
|
47
|
+
assert.equal(loadConfig({...env,EZ_EXECUTOR_TRANSPORT:''}).isolation, 'isolated')
|
|
48
|
+
assert.throws(() => loadConfig({...env,EZ_EXECUTOR_TRANSPORT:'host'}), /sandbox|SANDBOX|Isolation/)
|
|
49
|
+
assert.throws(() => loadConfig({...env,EZ_CODEX_SANDBOX:'danger-full-access'}), /sandbox|SANDBOX/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('isolation class is isolated by default and rejects host transport mismatch', () => {
|
|
53
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY'}).isolation, 'isolated')
|
|
54
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_ISOLATION:'isolated',EZ_EXECUTOR_TRANSPORT:'local'}).isolation, 'isolated')
|
|
55
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_EXECUTOR_TRANSPORT:'host'}).isolation, 'host-capable')
|
|
56
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_ISOLATION:'host-capable',EZ_EXECUTOR_TRANSPORT:'host'}).isolation, 'host-capable')
|
|
57
|
+
assert.throws(() => loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_ISOLATION:'isolated',EZ_EXECUTOR_TRANSPORT:'host'}), /Isolation isolated requires EZ_EXECUTOR_TRANSPORT=local/)
|
|
58
|
+
assert.throws(() => loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_ISOLATION:'host-capable',EZ_EXECUTOR_TRANSPORT:'local'}), /Isolation host-capable requires EZ_EXECUTOR_TRANSPORT=host/)
|
|
59
|
+
assert.throws(() => loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_ISOLATION:'seatbelt'}), /isolated or host-capable/)
|
|
60
|
+
assert.throws(() => loadConfig({
|
|
61
|
+
TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_ISOLATION:'host-capable',EZ_EXECUTOR_TRANSPORT:'host',
|
|
62
|
+
EZ_CHANNEL_BACKEND_URL:'https://backend.example',EZ_CHANNEL_BACKEND_TOKEN:'secret',
|
|
63
|
+
}), /Host-capable isolation requires the host native CLI/)
|
|
70
64
|
})
|
|
71
65
|
|
|
72
66
|
test('optional web launcher preserves reserved commands and accepts only HTTPS without secrets', () => {
|
|
73
|
-
const config = (value: unknown) => loadConfig({ TELEGRAM_BOT_TOKEN: '
|
|
67
|
+
const config = (value: unknown) => loadConfig({ TELEGRAM_BOT_TOKEN: '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY', EZ_TELEGRAM_WEB_APP: JSON.stringify(value) })
|
|
74
68
|
assert.deepEqual(config({command:'voice',label:'Voice',url:'https://voice.example/'}).webLauncher,{command:'voice',label:'Voice',url:'https://voice.example/'})
|
|
75
69
|
for (const value of [{command:'stop',label:'Voice',url:'https://voice.example/'},{command:'voice',label:'Voice',url:'http://voice.example/'},{command:'voice',label:'Voice',url:'https://voice.example/#secret'},{command:'voice',label:'Voice',url:'https://user:pass@voice.example/'},{command:'voice',label:'Voice',url:'https://voice.example/?token=secret'}]) assert.throws(()=>config(value))
|
|
76
70
|
})
|
|
71
|
+
|
|
72
|
+
test('Telegram transport is inferred from a valid token; legacy flag contradictions fail closed', () => {
|
|
73
|
+
const token = '110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY'
|
|
74
|
+
const enabled = loadConfig({ TELEGRAM_BOT_TOKEN: token, EZ_APPLICATION_PORT: '8110' })
|
|
75
|
+
assert.equal(enabled.telegramEnabled, true)
|
|
76
|
+
assert.equal(enabled.telegramBotToken, token)
|
|
77
|
+
const appOnly = loadConfig({ TELEGRAM_BOT_TOKEN: ' ', EZ_APPLICATION_PORT: '8110' })
|
|
78
|
+
assert.equal(appOnly.telegramEnabled, false)
|
|
79
|
+
assert.equal(appOnly.telegramBotToken, '')
|
|
80
|
+
assert.throws(() => loadConfig({ TELEGRAM_BOT_TOKEN: 'garbage', EZ_APPLICATION_PORT: '8110' }), /malformed/)
|
|
81
|
+
assert.throws(() => loadConfig({ TELEGRAM_BOT_TOKEN: token, EZ_TELEGRAM_ENABLED: 'false', EZ_APPLICATION_PORT: '8110' }), /contradicts/)
|
|
82
|
+
assert.throws(() => loadConfig({ EZ_TELEGRAM_ENABLED: 'true', EZ_APPLICATION_PORT: '8110' }), /contradicts/)
|
|
83
|
+
assert.throws(() => loadConfig({ TELEGRAM_BOT_TOKEN: token, EZ_TELEGRAM_ENABLED: 'sometimes' }), /must be true or false/)
|
|
84
|
+
assert.equal(loadConfig({ EZ_TELEGRAM_ENABLED: 'false', EZ_APPLICATION_PORT: '8110' }).telegramEnabled, false)
|
|
85
|
+
assert.equal(loadConfig({ TELEGRAM_BOT_TOKEN: token, EZ_TELEGRAM_ENABLED: 'true' }).telegramEnabled, true)
|
|
86
|
+
})
|