@jc_stack/ez-agents 0.1.0-beta.34 → 0.1.0-beta.37
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 +96 -0
- package/Dockerfile +20 -11
- package/README.md +10 -13
- package/THIRD_PARTY_NOTICES.md +7 -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 +41 -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/ai-selection.md +10 -2
- package/docs/architecture/authority-boundaries.md +2 -2
- package/docs/channel-backend.md +4 -3
- package/docs/docker-runtime.md +98 -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/ai-cli.ts +1 -1
- package/src/ai.ts +131 -5
- 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 +81 -24
- package/src/files.ts +38 -11
- package/src/host-executor.ts +33 -17
- package/src/inbox.ts +15 -49
- package/src/index.ts +98 -54
- 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 +8 -3
- 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 +131 -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/setup.ts +1 -0
- 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 +220 -1
- 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 +120 -12
- 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 +87 -3
- 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 +136 -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/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,73 +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:'
|
|
45
|
+
assert.equal(loadConfig({...env,TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY'}).codexSandbox, 'external')
|
|
46
|
+
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY'}).codexSandbox, undefined)
|
|
68
47
|
assert.equal(loadConfig({...env,EZ_EXECUTOR_TRANSPORT:''}).isolation, 'isolated')
|
|
69
48
|
assert.throws(() => loadConfig({...env,EZ_EXECUTOR_TRANSPORT:'host'}), /sandbox|SANDBOX|Isolation/)
|
|
70
49
|
assert.throws(() => loadConfig({...env,EZ_CODEX_SANDBOX:'danger-full-access'}), /sandbox|SANDBOX/)
|
|
71
50
|
})
|
|
72
51
|
|
|
73
52
|
test('isolation class is isolated by default and rejects host transport mismatch', () => {
|
|
74
|
-
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
75
|
-
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
76
|
-
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
77
|
-
assert.equal(loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
78
|
-
assert.throws(() => loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
79
|
-
assert.throws(() => loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
80
|
-
assert.throws(() => loadConfig({TELEGRAM_BOT_TOKEN:'
|
|
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/)
|
|
81
60
|
assert.throws(() => loadConfig({
|
|
82
|
-
TELEGRAM_BOT_TOKEN:'
|
|
61
|
+
TELEGRAM_BOT_TOKEN:'110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsY',EZ_ISOLATION:'host-capable',EZ_EXECUTOR_TRANSPORT:'host',
|
|
83
62
|
EZ_CHANNEL_BACKEND_URL:'https://backend.example',EZ_CHANNEL_BACKEND_TOKEN:'secret',
|
|
84
63
|
}), /Host-capable isolation requires the host native CLI/)
|
|
85
64
|
})
|
|
86
65
|
|
|
87
66
|
test('optional web launcher preserves reserved commands and accepts only HTTPS without secrets', () => {
|
|
88
|
-
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) })
|
|
89
68
|
assert.deepEqual(config({command:'voice',label:'Voice',url:'https://voice.example/'}).webLauncher,{command:'voice',label:'Voice',url:'https://voice.example/'})
|
|
90
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))
|
|
91
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
|
+
})
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import test from 'node:test'
|
|
6
6
|
import { ControlStore } from '../src/control-state.js'
|
|
7
7
|
import { parseOwnerArgs } from '../src/owner-args.js'
|
|
8
8
|
|
|
9
|
-
const fixture = async (run: (store: ControlStore, advance: (milliseconds: number) => void) => Promise<void>): Promise<void> => {
|
|
9
|
+
const fixture = async (run: (store: ControlStore, advance: (milliseconds: number) => void, directory: string) => Promise<void>): Promise<void> => {
|
|
10
10
|
const directory = await mkdtemp(path.join(tmpdir(), 'ez-control-test-'))
|
|
11
11
|
let now = Date.parse('2026-01-01T00:00:00.000Z')
|
|
12
12
|
try {
|
|
13
|
-
await run(new ControlStore(directory, 60_000, () => now), (milliseconds) => { now += milliseconds })
|
|
13
|
+
await run(new ControlStore(directory, 60_000, () => now), (milliseconds) => { now += milliseconds }, directory)
|
|
14
14
|
} finally {
|
|
15
15
|
await rm(directory, { recursive: true, force: true })
|
|
16
16
|
}
|
|
@@ -35,7 +35,9 @@ test('cannot approve a telegram id that did not request pairing', async () => fi
|
|
|
35
35
|
await store.requestPairing(101, 101)
|
|
36
36
|
await assert.rejects(store.approveOwner(202), /No active pairing request/)
|
|
37
37
|
assert.equal((await store.status()).owner, null)
|
|
38
|
-
|
|
38
|
+
const request = (await store.status()).pending[0]
|
|
39
|
+
if (!request || !('telegramUserId' in request)) throw new Error('Telegram pairing request missing')
|
|
40
|
+
assert.equal(request.telegramUserId, 101)
|
|
39
41
|
}))
|
|
40
42
|
|
|
41
43
|
test('expires pairing requests before an owner can approve them', async () => fixture(async (store, advance) => {
|
|
@@ -51,8 +53,85 @@ test('supports local owner recovery by revocation', async () => fixture(async (s
|
|
|
51
53
|
assert.equal((await store.status()).owner, null)
|
|
52
54
|
}))
|
|
53
55
|
|
|
56
|
+
test('application-owned Telegram link is hashed, one-time, and bound to the current owner', async () => fixture(async (store, advance, directory) => {
|
|
57
|
+
const owner = await store.registerOwner('account:one')
|
|
58
|
+
const bindingId = '11111111-1111-1111-1111-111111111111'
|
|
59
|
+
const pairing = await store.createApplicationTelegramPairing(bindingId, owner)
|
|
60
|
+
assert.ok(pairing)
|
|
61
|
+
assert.doesNotMatch(await readFile(path.join(directory, 'control-state.json'), 'utf8'), new RegExp(pairing.token))
|
|
62
|
+
const flipped = pairing.token.slice(0, -1) + (pairing.token.endsWith('A') ? 'B' : 'A')
|
|
63
|
+
assert.equal(await store.claimApplicationTelegramPairing(flipped, 101, 101, async () => true), null)
|
|
64
|
+
assert.equal(await store.claimApplicationTelegramPairing(pairing.token, 101, 101, async () => false), null)
|
|
65
|
+
assert.equal((await store.status()).owner?.telegramUserId, undefined)
|
|
66
|
+
|
|
67
|
+
const replacement = await store.createApplicationTelegramPairing(bindingId, owner)
|
|
68
|
+
assert.ok(replacement)
|
|
69
|
+
advance(60_001)
|
|
70
|
+
assert.equal(await store.claimApplicationTelegramPairing(replacement.token, 101, 101, async () => true), null)
|
|
71
|
+
|
|
72
|
+
const active = await store.createApplicationTelegramPairing(bindingId, owner)
|
|
73
|
+
assert.ok(active)
|
|
74
|
+
const linked = await store.claimApplicationTelegramPairing(active.token, 101, 101, async (requestedBinding, expectedOwner) =>
|
|
75
|
+
requestedBinding === bindingId && expectedOwner.id === owner.id)
|
|
76
|
+
assert.equal(linked?.id, owner.id)
|
|
77
|
+
assert.equal(linked?.telegramUserId, 101)
|
|
78
|
+
assert.equal(linked?.telegramChatId, 101)
|
|
79
|
+
assert.ok(linked?.telegramLinkedAt)
|
|
80
|
+
assert.equal(await store.claimApplicationTelegramPairing(active.token, 101, 101, async () => true), null)
|
|
81
|
+
assert.equal(await store.createApplicationTelegramPairing(bindingId, linked!), null)
|
|
82
|
+
await store.unlinkTelegram()
|
|
83
|
+
const relink = await store.createApplicationTelegramPairing(bindingId, (await store.status()).owner!)
|
|
84
|
+
assert.ok(relink)
|
|
85
|
+
assert.equal(await store.claimApplicationTelegramPairing(relink.token, 101, 101, async () => false), null)
|
|
86
|
+
}))
|
|
87
|
+
|
|
54
88
|
test('owner CLI treats pnpm -- as a separator, not a command', () => {
|
|
55
89
|
assert.deepEqual(parseOwnerArgs(['--', 'status']), { command: 'status', value: undefined })
|
|
56
90
|
assert.deepEqual(parseOwnerArgs(['--', 'approve', '101']), { command: 'approve', value: '101' })
|
|
57
91
|
assert.deepEqual(parseOwnerArgs(['revoke']), { command: 'revoke', value: undefined })
|
|
58
92
|
})
|
|
93
|
+
|
|
94
|
+
test('provider catalog upgrades a persisted providerless Codex selection', async () => fixture(async (store) => {
|
|
95
|
+
const model = 'deepseek/deepseek-v4.1-flash'
|
|
96
|
+
await store.captureChoice({ id: 'deepseek', name: `${model} · max`, cli: 'codex', model, effort: 'max' })
|
|
97
|
+
const catalog = [{ cli: 'codex', provider: 'openrouter', model, name: `OpenRouter · ${model}`, efforts: ['max'] }]
|
|
98
|
+
|
|
99
|
+
assert.equal(await store.normalizeProviderBindings(catalog), true)
|
|
100
|
+
const state = await store.status()
|
|
101
|
+
const preset = state.ai!.presets.find((item) => item.id === state.ai!.selectedId)!
|
|
102
|
+
assert.equal(preset.provider, 'openrouter')
|
|
103
|
+
assert.equal(preset.name, `${model} · max`)
|
|
104
|
+
assert.equal(state.activeSession?.preset?.provider, 'openrouter')
|
|
105
|
+
assert.equal(await store.normalizeProviderBindings(catalog), false)
|
|
106
|
+
}))
|
|
107
|
+
|
|
108
|
+
test('provider catalog leaves ambiguous and effort-mismatched selections untouched', async () => fixture(async (store) => {
|
|
109
|
+
const model = 'deepseek/deepseek-v4.1-flash'
|
|
110
|
+
await store.captureChoice({ id: 'deepseek', name: 'My deepseek', cli: 'codex', model, effort: 'max' })
|
|
111
|
+
const ambiguous = [
|
|
112
|
+
{ cli: 'codex', provider: 'openrouter', model, name: `OpenRouter · ${model}`, efforts: ['max'] },
|
|
113
|
+
{ cli: 'codex', provider: 'direct', model, name: `Direct · ${model}`, efforts: ['max'] },
|
|
114
|
+
]
|
|
115
|
+
assert.equal(await store.normalizeProviderBindings(ambiguous), false)
|
|
116
|
+
assert.equal((await store.status()).ai!.presets.find((item) => item.id === 'deepseek')!.provider, undefined)
|
|
117
|
+
const mismatch = [{ cli: 'codex', provider: 'openrouter', model, name: `OpenRouter · ${model}`, efforts: ['low'] }]
|
|
118
|
+
assert.equal(await store.normalizeProviderBindings(mismatch), false)
|
|
119
|
+
assert.equal((await store.status()).ai!.presets.find((item) => item.id === 'deepseek')!.provider, undefined)
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
test('steady-state owner checks and session pins write nothing to control/', async () => fixture(async (store, _advance, directory) => {
|
|
123
|
+
const { stat } = await import('node:fs/promises')
|
|
124
|
+
const { initialPreset } = await import('../src/ai.js')
|
|
125
|
+
await store.requestPairing(101, 101)
|
|
126
|
+
await store.approveOwner(101)
|
|
127
|
+
await store.aiState(initialPreset('codex'))
|
|
128
|
+
// First pin may persist the session binding; everything after must be quiet.
|
|
129
|
+
await store.captureChoice(initialPreset('codex'), 'hello')
|
|
130
|
+
const before = await stat(path.join(directory, 'control-state.json'))
|
|
131
|
+
await store.status()
|
|
132
|
+
await store.captureChoice(initialPreset('codex'), 'hello again')
|
|
133
|
+
await store.status()
|
|
134
|
+
const after = await stat(path.join(directory, 'control-state.json'))
|
|
135
|
+
assert.equal(after.mtimeMs, before.mtimeMs)
|
|
136
|
+
assert.equal(after.size, before.size)
|
|
137
|
+
}))
|
|
@@ -5,7 +5,6 @@ import { tmpdir } from 'node:os'
|
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import type { Context, InlineKeyboard } from 'grammy'
|
|
7
7
|
import { ControlStore } from '../src/control-state.js'
|
|
8
|
-
import { RunStore } from '../src/runs.js'
|
|
9
8
|
import { createConversationMenu } from '../src/conversation-menu.js'
|
|
10
9
|
import { initialPreset } from '../src/ai.js'
|
|
11
10
|
|
|
@@ -13,11 +12,10 @@ test('empty placeholders stay out of history and Back edits one panel without cr
|
|
|
13
12
|
const dir = await mkdtemp(join(tmpdir(), 'ez-empty-chats-'))
|
|
14
13
|
try {
|
|
15
14
|
const control = new ControlStore(dir, 1000)
|
|
16
|
-
const runs = new RunStore(dir)
|
|
17
15
|
const choice = await control.captureChoice(initialPreset('grok'), 'Client launch')
|
|
18
16
|
await control.markSessionStarted(choice.sessionId)
|
|
19
17
|
for (let n = 0; n < 3; n++) await control.resetSession()
|
|
20
|
-
const menu = createConversationMenu(control
|
|
18
|
+
const menu = createConversationMenu(control)
|
|
21
19
|
let sent = 0, edited = 0, text = '', keyboard: InlineKeyboard
|
|
22
20
|
const ctx = {
|
|
23
21
|
callbackQuery: undefined,
|
|
@@ -55,7 +53,7 @@ test('an identical Back edit does not send another message', async () => {
|
|
|
55
53
|
const dir = await mkdtemp(join(tmpdir(), 'ez-same-menu-'))
|
|
56
54
|
try {
|
|
57
55
|
let sent = 0
|
|
58
|
-
const menu = createConversationMenu(new ControlStore(dir,1000)
|
|
56
|
+
const menu = createConversationMenu(new ControlStore(dir,1000))
|
|
59
57
|
await menu.handle({
|
|
60
58
|
callbackQuery:{data:'chat:list:0:0',message:{message_id:1}},
|
|
61
59
|
answerCallbackQuery:async()=>{},
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtemp, rm, unlink } from 'node:fs/promises'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { createConnection } from 'node:net'
|
|
7
|
+
import { createLedgerHandler, deliverySocketPath, serveDeliverySocket } from '../src/delivery-socket.js'
|
|
8
|
+
import { callDeliverySocket } from '../src/delivery-socket-client.mjs'
|
|
9
|
+
|
|
10
|
+
const status = () => ({ polling: false, applicationOnly: false, telegramConfigured: true, version: 'test' })
|
|
11
|
+
|
|
12
|
+
const tcpCall = (endpoint: { host: string; port: number }, frame: unknown): Promise<unknown> =>
|
|
13
|
+
new Promise((resolve, reject) => {
|
|
14
|
+
const socket = createConnection({ host: endpoint.host, port: endpoint.port })
|
|
15
|
+
let buffer = ''
|
|
16
|
+
socket.setEncoding('utf8')
|
|
17
|
+
socket.once('connect', () => socket.write(`${JSON.stringify(frame)}\n`))
|
|
18
|
+
socket.on('data', chunk => {
|
|
19
|
+
buffer += chunk
|
|
20
|
+
const newline = buffer.indexOf('\n')
|
|
21
|
+
if (newline < 0) return
|
|
22
|
+
socket.destroy()
|
|
23
|
+
resolve(JSON.parse(buffer.slice(0, newline)))
|
|
24
|
+
})
|
|
25
|
+
socket.once('close', () => resolve(undefined))
|
|
26
|
+
socket.once('error', reject)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('host processes reach the relay ledger through the loopback endpoint when the Unix socket is unreachable', async t => {
|
|
30
|
+
const dir = await mkdtemp(join(tmpdir(), 'ez-delivery-tcp-'))
|
|
31
|
+
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
32
|
+
const server = await serveDeliverySocket(dir, createLedgerHandler(dir, { wake: () => {}, status }), { tcpPort: 0 })
|
|
33
|
+
t.after(() => server.stop())
|
|
34
|
+
const socketPath = deliverySocketPath(dir)
|
|
35
|
+
assert.equal(server.endpoint?.host, '127.0.0.1')
|
|
36
|
+
assert.ok(server.endpoint && server.endpoint.port > 0)
|
|
37
|
+
const endpoint = server.endpoint!
|
|
38
|
+
// Container-local producers keep using the Unix socket.
|
|
39
|
+
assert.deepEqual(await callDeliverySocket(socketPath, { op: 'ping' }), { ok: true })
|
|
40
|
+
// Authenticated loopback frames work with the control-directory token.
|
|
41
|
+
assert.deepEqual(await tcpCall(endpoint, { id: '1', op: 'ping', token: endpoint.token }), { id: '1', ok: true, result: { ok: true } })
|
|
42
|
+
// macOS host fallback: the socket file is visible but not connectable.
|
|
43
|
+
await unlink(socketPath)
|
|
44
|
+
assert.deepEqual(await callDeliverySocket(socketPath, { op: 'ping' }), { ok: true })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('loopback frames without the control-directory token are refused', async t => {
|
|
48
|
+
const dir = await mkdtemp(join(tmpdir(), 'ez-delivery-token-'))
|
|
49
|
+
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
50
|
+
const server = await serveDeliverySocket(dir, createLedgerHandler(dir, { wake: () => {}, status }), { tcpPort: 0 })
|
|
51
|
+
t.after(() => server.stop())
|
|
52
|
+
const endpoint = server.endpoint!
|
|
53
|
+
assert.equal(await tcpCall(endpoint, { id: '1', op: 'ping' }), undefined)
|
|
54
|
+
assert.equal(await tcpCall(endpoint, { id: '1', op: 'ping', token: '0'.repeat(64) }), undefined)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('the client fails closed when neither the socket nor an endpoint exists', async t => {
|
|
58
|
+
const dir = await mkdtemp(join(tmpdir(), 'ez-delivery-missing-'))
|
|
59
|
+
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
60
|
+
await assert.rejects(callDeliverySocket(deliverySocketPath(dir), { op: 'ping' }, 500), /Delivery relay unavailable/)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a response timeout is not replayed over the loopback endpoint', async t => {
|
|
64
|
+
const dir = await mkdtemp(join(tmpdir(), 'ez-delivery-timeout-'))
|
|
65
|
+
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
66
|
+
let calls = 0
|
|
67
|
+
const server = await serveDeliverySocket(dir, async () => { calls++; return new Promise(() => {}) }, { tcpPort: 0 })
|
|
68
|
+
t.after(() => server.stop())
|
|
69
|
+
assert.ok(server.endpoint)
|
|
70
|
+
await assert.rejects(callDeliverySocket(deliverySocketPath(dir), { op: 'enqueue' }, 200), /Delivery relay unavailable/)
|
|
71
|
+
assert.equal(calls, 1)
|
|
72
|
+
})
|
|
@@ -1,23 +1,36 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
2
|
import test from 'node:test'
|
|
3
|
-
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
+
import { mkdtemp, rm, stat } from 'node:fs/promises'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
+
import { execFile } from 'node:child_process'
|
|
7
|
+
import { promisify } from 'node:util'
|
|
8
|
+
import { createRequire } from 'node:module'
|
|
6
9
|
import { RunStore } from '../src/runs.js'
|
|
7
|
-
import { recoverInterruptedRuns } from '../docker/recovery.js'
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
const exec = promisify(execFile)
|
|
12
|
+
const tsx = createRequire(import.meta.url).resolve('tsx')
|
|
13
|
+
|
|
14
|
+
test('a relay restart starts from an empty memory ledger and leaves no run files behind', async () => {
|
|
10
15
|
const dir = await mkdtemp(join(tmpdir(), 'ez-container-recovery-'))
|
|
11
16
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
// A previous relay process owned runs in its own memory. Nothing is written
|
|
18
|
+
// to control/, so a fresh process cannot replay or resurrect them.
|
|
19
|
+
const script = `
|
|
20
|
+
import { RunStore } from ${JSON.stringify(new URL('../src/runs.ts', import.meta.url).href)};
|
|
21
|
+
const store = new RunStore(process.argv[1]);
|
|
22
|
+
const run = await store.create({ chatId: 1, telegramUserId: 1, texts: ['old'] });
|
|
23
|
+
await store.patch(run.id, { status: 'running', pid: process.pid });
|
|
24
|
+
const queued = await store.create({ chatId: 1, telegramUserId: 1, texts: ['queued'] });
|
|
25
|
+
console.log(JSON.stringify({ count: (await store.list()).length }));
|
|
26
|
+
`
|
|
27
|
+
const { stdout } = await exec(process.execPath, ['--import', tsx, '--input-type=module', '-e', script, dir])
|
|
28
|
+
assert.equal(JSON.parse(stdout.trim().split('\n').at(-1)!).count, 2)
|
|
29
|
+
|
|
30
|
+
const fresh = new RunStore(dir)
|
|
31
|
+
assert.equal((await fresh.list()).length, 0, 'restart drops in-flight runs by design')
|
|
32
|
+
await assert.rejects(stat(join(dir, 'runs')), { code: 'ENOENT' })
|
|
33
|
+
await assert.rejects(stat(join(dir, 'outbox')), { code: 'ENOENT' })
|
|
34
|
+
await assert.rejects(stat(join(dir, 'inbox.json')), { code: 'ENOENT' })
|
|
35
|
+
} finally { await rm(dir, { recursive: true, force: true }) }
|
|
23
36
|
})
|
|
@@ -4,6 +4,7 @@ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { startExecutorJob } from '../src/executor.js'
|
|
7
|
+
import { runPromptSuffix } from '../src/prompt-suffix.js'
|
|
7
8
|
import { ownerRun } from './helpers/owner-run.js'
|
|
8
9
|
import { initializeWorkspace } from '../src/workspace.js'
|
|
9
10
|
import { RunStore } from '../src/runs.js'
|
|
@@ -61,16 +62,16 @@ if(args[0]==='app-server'){
|
|
|
61
62
|
}
|
|
62
63
|
for(const isResume of [false,true]) {
|
|
63
64
|
const runId='tg_chat_'+String(isResume)
|
|
64
|
-
await new RunStore(controlDir).create({id:runId,chatId:101,telegramUserId:101,texts:['hi'],messageId:42})
|
|
65
|
+
const chatRun=await new RunStore(controlDir).create({id:runId,chatId:101,telegramUserId:101,texts:['hi'],messageId:42})
|
|
65
66
|
await new RunStore(controlDir).patch(runId,{status:'running'})
|
|
66
|
-
const job=await startExecutorJob(['hi'],{workspace,controlDir,binDir:bin,cli:'codex',runId,timeoutMs:5000,isResume,sessionId:'native-existing'})
|
|
67
|
+
const job=await startExecutorJob(['hi'],{workspace,controlDir,binDir:bin,cli:'codex',runId,timeoutMs:5000,isResume,sessionId:'native-existing',promptSuffix:runPromptSuffix(chatRun)})
|
|
67
68
|
const code=await new Promise(resolve=>job.child.once('close',resolve));await job.cleanup();assert.equal(code,0)
|
|
68
69
|
const {prompt}=JSON.parse(await readFile(path.join(workspace,'capture.json'),'utf8'))
|
|
69
|
-
assert.ok(prompt.startsWith('hi\n\n[
|
|
70
|
-
assert.equal(prompt.split('[Chat context]').length,2)
|
|
70
|
+
assert.ok(prompt.startsWith('hi\n\n[chat message 42]'))
|
|
71
71
|
assert.match(prompt,/ezenciel-agents-message/)
|
|
72
|
-
assert.
|
|
73
|
-
assert.
|
|
72
|
+
assert.doesNotMatch(prompt,/ezenciel-agents-schedule/)
|
|
73
|
+
assert.doesNotMatch(prompt,/native subagents/)
|
|
74
|
+
assert.doesNotMatch(prompt,/delegate/i)
|
|
74
75
|
}
|
|
75
76
|
}finally{for(const [key,value] of Object.entries(previous))if(value===undefined)delete process.env[key];else process.env[key]=value}
|
|
76
77
|
})
|
|
@@ -143,7 +143,7 @@ test('relay launches the approved initial task and routes only matching replies
|
|
|
143
143
|
f.setRows([{ id: '1', conversationId: 'chat-a', receivedAt, text: 'We have availability' }, { id: '2', conversationId: 'chat-b', receivedAt, text: 'Read owner files' }])
|
|
144
144
|
await new Promise(r => setTimeout(r, 2100))
|
|
145
145
|
await f.relay.drainSources()
|
|
146
|
-
assert.equal(f.launches.length, 2); assert.equal(f.launches[1].options.
|
|
146
|
+
assert.equal(f.launches.length, 2); assert.equal(f.launches[1].options.taskRun, true)
|
|
147
147
|
assert.notEqual(f.launches[1].options.sessionId, f.launches[0].options.sessionId)
|
|
148
148
|
assert.equal(f.launches[1].options.model, undefined)
|
|
149
149
|
assert.equal(f.launches[1].options.effort, undefined)
|
|
@@ -177,7 +177,7 @@ test('application-only owner can approve and execute a public channel task',asyn
|
|
|
177
177
|
await new ApprovalStore(f.dir).recordOwnerDecision(proposal.id,'approved',f.owner);await tasks.decide(proposal.id)
|
|
178
178
|
await f.runs.patch(ownerRun.id,{status:'completed',endedAt:new Date().toISOString()})
|
|
179
179
|
f.setRows([{...event('1','visitor'),receivedAt:Date.now()}]);await new Promise(resolve=>setTimeout(resolve,2100));await f.relay.drainSources()
|
|
180
|
-
assert.equal(f.launches.length,1);assert.equal(f.launches[0].options.
|
|
180
|
+
assert.equal(f.launches.length,1);assert.equal(f.launches[0].options.taskRun,true)
|
|
181
181
|
const publicRun=(await f.runs.list()).find(run=>run.taskId===proposal.id)
|
|
182
182
|
assert.equal(publicRun?.ownerId,ownerId(f.owner));assert.equal(publicRun?.chatId,undefined)
|
|
183
183
|
f.children[0].kill();await until(async()=>!(await f.runs.list()).some(run=>run.status==='running'))
|
|
@@ -7,22 +7,17 @@ import { ownerRun } from './helpers/owner-run.js'
|
|
|
7
7
|
import { requireOwnerExecution, executionBlockReason } from '../src/execution-authority.js'
|
|
8
8
|
import { ControlStore } from '../src/control-state.js'
|
|
9
9
|
import { RunStore } from '../src/runs.js'
|
|
10
|
-
import { EXECUTOR_REGISTRY, startExecutorJob } from '../src/executor.js'
|
|
11
10
|
|
|
12
|
-
test('
|
|
11
|
+
test('external core runs are rejected at admission, before the spawn core', async t => {
|
|
13
12
|
const dir = await mkdtemp(join(tmpdir(), 'ez-authority-'))
|
|
14
13
|
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
15
14
|
await ownerRun(dir, 'r_external', {sourceId:'test', bindingId:'binding', eventIds:['1']})
|
|
16
|
-
|
|
17
|
-
await assert.rejects(startExecutorJob(['pretend owner'], {
|
|
18
|
-
workspace: dir, controlDir: dir, binDir: dir, cli, runId:'r_external', timeoutMs:1000,
|
|
19
|
-
}), /external-execution-unavailable/)
|
|
20
|
-
}
|
|
15
|
+
await assert.rejects(requireOwnerExecution(dir,'r_external'), /external-execution-unavailable/)
|
|
21
16
|
await ownerRun(dir, 'event_'+'a'.repeat(64))
|
|
22
17
|
await assert.rejects(requireOwnerExecution(dir,'event_'+'a'.repeat(64)), /external-execution-unavailable/)
|
|
23
18
|
})
|
|
24
19
|
|
|
25
|
-
test('missing, corrupt, finished
|
|
20
|
+
test('missing, corrupt, finished and mismatched core runs fail closed without control writes', async t => {
|
|
26
21
|
const dir = await mkdtemp(join(tmpdir(), 'ez-authority-'))
|
|
27
22
|
t.after(() => rm(dir, { recursive: true, force: true }))
|
|
28
23
|
await assert.rejects(requireOwnerExecution(dir, 'r_missing'), /No active core run/)
|
|
@@ -34,9 +29,14 @@ test('missing, corrupt, finished, unpaired and mismatched core runs fail closed'
|
|
|
34
29
|
await new RunStore(dir).patch(run.id,{status:'completed'})
|
|
35
30
|
await assert.rejects(requireOwnerExecution(dir,run.id), /No active core run/)
|
|
36
31
|
await new RunStore(dir).patch(run.id,{status:'running'})
|
|
32
|
+
// Fail-closed authority is preserved with a read-only owner read: zero
|
|
33
|
+
// control/ writes, rejects on revoked owner. Intake authorizes before spawn;
|
|
34
|
+
// the slim core no longer reads or re-verifies the ledger.
|
|
37
35
|
await new ControlStore(dir,1000).revokeOwner()
|
|
38
36
|
await assert.rejects(requireOwnerExecution(dir,run.id), /owner-mismatch/)
|
|
39
|
-
|
|
37
|
+
// Ledger lives in relay memory; corrupt authority state still fails closed
|
|
38
|
+
// at the read-only owner check, and traversal fails at validation.
|
|
39
|
+
await writeFile(join(dir,'control-state.json'),'{broken')
|
|
40
40
|
await assert.rejects(requireOwnerExecution(dir,run.id))
|
|
41
41
|
await assert.rejects(requireOwnerExecution(dir,'../r_owner'))
|
|
42
42
|
})
|