@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
|
@@ -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')
|
|
@@ -105,10 +130,10 @@ export class ApplicationChannel {
|
|
|
105
130
|
const owner = await this.sharedBinding(bindingId)
|
|
106
131
|
const controls = this.options.aiControls
|
|
107
132
|
if (!controls) throw new Error('Application controls unavailable')
|
|
108
|
-
const value = input as {action?: unknown; expectedSession?: unknown; sessionId?: unknown; presetId?: unknown; cli?: unknown; model?: unknown; effort?: unknown}
|
|
133
|
+
const value = input as {action?: unknown; expectedSession?: unknown; sessionId?: unknown; presetId?: unknown; cli?: unknown; provider?: unknown; model?: unknown; effort?: unknown}
|
|
109
134
|
if (!value || !['new','switch','select','model'].includes(String(value.action)) ||
|
|
110
135
|
!(value.expectedSession === null || (typeof value.expectedSession === 'string' && /^[a-f0-9-]{36}$/.test(value.expectedSession)))) throw new Error('Invalid application control request')
|
|
111
|
-
const fields = {new:[],switch:['sessionId'],select:['presetId'],model:['cli','model','effort']}[value.action as 'new'|'switch'|'select'|'model']!
|
|
136
|
+
const fields = {new:[],switch:['sessionId'],select:['presetId'],model:['cli','provider','model','effort']}[value.action as 'new'|'switch'|'select'|'model']!
|
|
112
137
|
if (Object.keys(value).some(key => !['action','expectedSession',...fields].includes(key))) throw new Error('Invalid application control fields')
|
|
113
138
|
const control = new ControlStore(this.options.controlDir, 900000)
|
|
114
139
|
const expected = value.expectedSession as string | null
|
|
@@ -131,7 +156,7 @@ export class ApplicationChannel {
|
|
|
131
156
|
await controls.select(preset, expected, guard)
|
|
132
157
|
}
|
|
133
158
|
if (value.action === 'model') {
|
|
134
|
-
const model = (await controls.catalog()).find(item => item.cli === value.cli && item.model === value.model)
|
|
159
|
+
const model = (await controls.catalog()).find(item => item.cli === value.cli && item.provider === value.provider && item.model === value.model)
|
|
135
160
|
if (!model || (value.effort !== undefined && (typeof value.effort !== 'string' || !model.efforts.includes(value.effort)))) throw new Error('Invalid application model selection')
|
|
136
161
|
await this.sharedBinding(bindingId)
|
|
137
162
|
const preset = await controls.saveSelection(model, value.effort as string | undefined, guard)
|
|
@@ -160,10 +185,10 @@ export class ApplicationChannel {
|
|
|
160
185
|
const owner = await this.sharedBinding(bindingId, false)
|
|
161
186
|
const controls = this.options.aiControls
|
|
162
187
|
if (!controls) throw new Error('Application controls unavailable')
|
|
163
|
-
const value = input as {action?:unknown; expectedSession?:unknown; presetId?:unknown; cli?:unknown; model?:unknown; effort?:unknown}
|
|
188
|
+
const value = input as {action?:unknown; expectedSession?:unknown; presetId?:unknown; cli?:unknown; provider?:unknown; model?:unknown; effort?:unknown}
|
|
164
189
|
if (!value || !['new','select','model'].includes(String(value.action)) ||
|
|
165
190
|
!(value.expectedSession===null || (typeof value.expectedSession==='string' && /^[a-f0-9-]{36}$/.test(value.expectedSession)))) throw new Error('Invalid application control request')
|
|
166
|
-
const fields = {new:[],select:['presetId'],model:['cli','model','effort']}[value.action as 'new'|'select'|'model']!
|
|
191
|
+
const fields = {new:[],select:['presetId'],model:['cli','provider','model','effort']}[value.action as 'new'|'select'|'model']!
|
|
167
192
|
if (Object.keys(value).some(key=>!['action','expectedSession',...fields].includes(key))) throw new Error('Invalid application control fields')
|
|
168
193
|
const hashedScope = applicationScope(bindingId,scope)
|
|
169
194
|
const guard:ControlGuard = {owner,applicationScope:hashedScope,expectedSession:value.expectedSession as string|null,
|
|
@@ -180,7 +205,7 @@ export class ApplicationChannel {
|
|
|
180
205
|
await controls.validate(preset)
|
|
181
206
|
}
|
|
182
207
|
if (value.action==='model') {
|
|
183
|
-
const model = (await controls.catalog()).find(item=>item.cli===value.cli && item.model===value.model)
|
|
208
|
+
const model = (await controls.catalog()).find(item=>item.cli===value.cli && item.provider===value.provider && item.model===value.model)
|
|
184
209
|
if (!model || model.cli==='agy' || (value.effort!==undefined && (typeof value.effort!=='string' || !model.efforts.includes(value.effort)))) throw new Error('Invalid application model selection')
|
|
185
210
|
preset = await controls.saveSelection(model,value.effort as string|undefined,guard)
|
|
186
211
|
}
|
|
@@ -195,7 +220,7 @@ export class ApplicationChannel {
|
|
|
195
220
|
if ('followTelegram' in rest) throw new Error('Invalid application request: choose one conversation option')
|
|
196
221
|
input = {...rest, followTelegram: followOwner}
|
|
197
222
|
}
|
|
198
|
-
const value = input as { requestId?: unknown; scope?: unknown; text?: unknown; attachment?: { name?: unknown; data?: unknown }; context?: Record<string, unknown>; expectedNativeSessionId?: unknown; activateTelegram?: unknown; followTelegram?: unknown; ai?: { cli?: unknown; model?: unknown; effort?: unknown } }
|
|
223
|
+
const value = input as { requestId?: unknown; scope?: unknown; text?: unknown; attachment?: { name?: unknown; data?: unknown }; context?: Record<string, unknown>; expectedNativeSessionId?: unknown; activateTelegram?: unknown; followTelegram?: unknown; ai?: { cli?: unknown; provider?: unknown; model?: unknown; effort?: unknown } }
|
|
199
224
|
if (!value || !applicationId(value.requestId) || !applicationId(value.scope) || typeof value.text !== 'string' || (!value.text.trim() && value.attachment === undefined) || value.text.length > 16000 || Object.keys(value).some(key => !['requestId','scope','text','attachment','context','expectedNativeSessionId','activateTelegram','followTelegram','ai'].includes(key))) throw new Error('Invalid application request')
|
|
200
225
|
if (value.expectedNativeSessionId !== undefined && (typeof value.expectedNativeSessionId !== 'string' || !/^[a-zA-Z0-9_-]{1,160}$/.test(value.expectedNativeSessionId))) throw new Error('Invalid application native session assertion')
|
|
201
226
|
if (value.activateTelegram !== undefined && typeof value.activateTelegram !== 'boolean') throw new Error('Invalid application Telegram activation')
|
|
@@ -204,7 +229,7 @@ export class ApplicationChannel {
|
|
|
204
229
|
let requestedPreset: AiPreset | undefined
|
|
205
230
|
if (value.ai !== undefined) {
|
|
206
231
|
const candidate = { ...value.ai, id: 'application', name: 'Application selection' }
|
|
207
|
-
if (!value.ai || Object.keys(value.ai).some(key => !['cli', 'model', 'effort'].includes(key)) || !isPreset(candidate)) throw new Error('Invalid application AI selection')
|
|
232
|
+
if (!value.ai || Object.keys(value.ai).some(key => !['cli', 'provider', 'model', 'effort'].includes(key)) || !isPreset(candidate)) throw new Error('Invalid application AI selection')
|
|
208
233
|
assertEffort(candidate.effort, candidate.model, candidate.cli)
|
|
209
234
|
requestedPreset = candidate
|
|
210
235
|
}
|
|
@@ -227,14 +252,14 @@ export class ApplicationChannel {
|
|
|
227
252
|
const existing = await this.runs.get(id)
|
|
228
253
|
if (existing) {
|
|
229
254
|
await this.bindings.authorize(existing)
|
|
230
|
-
if (requestedPreset && (existing.execution?.preset.cli !== requestedPreset.cli || existing.execution?.preset.model !== requestedPreset.model || existing.execution?.preset.effort !== requestedPreset.effort)) throw new Error('Application request ID conflicts with prior AI selection')
|
|
255
|
+
if (requestedPreset && (existing.execution?.preset.cli !== requestedPreset.cli || existing.execution?.preset.provider !== requestedPreset.provider || existing.execution?.preset.model !== requestedPreset.model || existing.execution?.preset.effort !== requestedPreset.effort)) throw new Error('Application request ID conflicts with prior AI selection')
|
|
231
256
|
if (Boolean(existing.application?.followTelegram) !== Boolean(value.followTelegram) || existing.application?.scope !== value.scope || (existing.application?.inputText ?? existing.texts[0]) !== value.text || existing.application?.attachmentHash !== attachmentHash) throw new Error('Application request ID conflicts with prior scope or text')
|
|
232
257
|
return existing // Retried context never replaces already admitted capabilities.
|
|
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)
|
|
@@ -252,7 +277,7 @@ export class ApplicationChannel {
|
|
|
252
277
|
await this.bindings.authorize(run)
|
|
253
278
|
const state = await new ControlStore(this.options.controlDir, 900000).status()
|
|
254
279
|
const session = state.activeSession?.sessionId === run.execution?.sessionId ? state.activeSession : state.sessions?.find(item => item.sessionId === run.execution?.sessionId)
|
|
255
|
-
return { ...(session ? { sessionId: session.sessionId, nativeSessionId: session.nativeSessionId, cli: session.cli } : {}), ...(run.scheduled?.originRunId ? {originRunId:run.scheduled.originRunId} : {}), id: run.id, scope: (run.application ?? run.delivery)!.scope, status: run.status, messages: await this.runs.applicationMessages(run.id), approvals:await this.runs.applicationApprovals(run.id), ...(run.status === 'failed' ? { error: 'Agent execution failed; inspect the core run' } : {}) }
|
|
280
|
+
return { ...(session ? { sessionId: session.sessionId, nativeSessionId: session.nativeSessionId, cli: session.cli } : {}), ...(run.execution?.preset ? {preset:run.execution.preset} : {}), ...(run.scheduled?.originRunId ? {originRunId:run.scheduled.originRunId} : {}), id: run.id, scope: (run.application ?? run.delivery)!.scope, status: run.status, messages: await this.runs.applicationMessages(run.id), approvals:await this.runs.applicationApprovals(run.id), ...(run.status === 'failed' ? { error: 'Agent execution failed; inspect the core run' } : {}) }
|
|
256
281
|
}
|
|
257
282
|
async deliver(run: RunRecord, item: OutboxItem): Promise<void> {
|
|
258
283
|
await this.bindings.authorize(run)
|
|
@@ -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' }))
|
package/src/approval.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
1
|
import { assertId } from './identity.js'
|
|
4
2
|
import { ownerEpoch, ownerId, type Owner } from './control-state.js'
|
|
5
3
|
|
|
@@ -34,28 +32,32 @@ const isApprovalRecord = (value: unknown): value is ApprovalRecord => {
|
|
|
34
32
|
)
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
// Stateless pipe: approvals live in relay memory. Engine children request
|
|
36
|
+
// decisions through the delivery socket (folded into the approval send);
|
|
37
|
+
// callbacks and task arbitration record them in-relay. A restart drops
|
|
38
|
+
// pending approvals by design.
|
|
39
|
+
const approvalsByControl = new Map<string, Map<string, ApprovalRecord>>()
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
41
|
+
const approvalsFor = (controlDir: string): Map<string, ApprovalRecord> => {
|
|
42
|
+
let map = approvalsByControl.get(controlDir)
|
|
43
|
+
if (!map) { map = new Map(); approvalsByControl.set(controlDir, map) }
|
|
44
|
+
return map
|
|
45
|
+
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
+
export class ApprovalStore {
|
|
48
|
+
constructor(private readonly controlDir: string) {}
|
|
47
49
|
|
|
48
|
-
private
|
|
50
|
+
private key(actionId: string): string {
|
|
49
51
|
const safe = assertId(actionId)
|
|
50
52
|
if (safe.length > 40) throw new Error('Approval identifier must be at most 40 characters')
|
|
51
|
-
return
|
|
53
|
+
return safe
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
async requestApproval(actionId: string, prompt: string, runId?: string): Promise<ApprovalRecord> {
|
|
55
57
|
if (!prompt.trim()) throw new Error('Approval prompt is empty')
|
|
56
58
|
if (runId) assertId(runId)
|
|
57
|
-
|
|
58
|
-
const existing =
|
|
59
|
+
const store = approvalsFor(this.controlDir)
|
|
60
|
+
const existing = store.get(this.key(actionId)) ?? null
|
|
59
61
|
if (existing) {
|
|
60
62
|
if (existing.runId !== runId || existing.prompt !== prompt)
|
|
61
63
|
throw new Error('Approval ID already belongs to another request')
|
|
@@ -70,10 +72,7 @@ export class ApprovalStore {
|
|
|
70
72
|
decision: 'pending',
|
|
71
73
|
createdAt: new Date().toISOString(),
|
|
72
74
|
}
|
|
73
|
-
|
|
74
|
-
const temporary = `${file}.${process.pid}.tmp`
|
|
75
|
-
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 })
|
|
76
|
-
await rename(temporary, file)
|
|
75
|
+
store.set(this.key(actionId), record)
|
|
77
76
|
return record
|
|
78
77
|
}
|
|
79
78
|
|
|
@@ -83,8 +82,8 @@ export class ApprovalStore {
|
|
|
83
82
|
decidedBy: number,
|
|
84
83
|
decisionUpdateId?: number,
|
|
85
84
|
): Promise<ApprovalRecord> {
|
|
86
|
-
|
|
87
|
-
const existing =
|
|
85
|
+
const store = approvalsFor(this.controlDir)
|
|
86
|
+
const existing = store.get(this.key(actionId)) ?? null
|
|
88
87
|
if (!existing) throw new Error('Unknown approval request')
|
|
89
88
|
if (
|
|
90
89
|
decisionUpdateId !== undefined &&
|
|
@@ -102,34 +101,25 @@ export class ApprovalStore {
|
|
|
102
101
|
decidedBy,
|
|
103
102
|
decisionUpdateId,
|
|
104
103
|
}
|
|
105
|
-
|
|
106
|
-
const temporary = `${file}.${process.pid}.tmp`
|
|
107
|
-
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 })
|
|
108
|
-
await rename(temporary, file)
|
|
104
|
+
store.set(this.key(actionId), record)
|
|
109
105
|
return record
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
async recordOwnerDecision(actionId: string, decision: 'approved' | 'denied', owner: Owner): Promise<ApprovalRecord> {
|
|
113
|
-
|
|
114
|
-
const existing =
|
|
109
|
+
const store = approvalsFor(this.controlDir)
|
|
110
|
+
const existing = store.get(this.key(actionId)) ?? null
|
|
115
111
|
if (!existing) throw new Error('Unknown approval request')
|
|
116
112
|
if (existing.decision !== 'pending') throw new Error('Approval already decided')
|
|
117
113
|
if (Date.now() - Date.parse(existing.createdAt) > 900_000) throw new Error('Approval expired')
|
|
118
114
|
const record: ApprovalRecord = {...existing,version:2,decision,decidedAt:new Date().toISOString(),decidedOwnerId:ownerId(owner),decidedOwnerEpoch:ownerEpoch(owner)}
|
|
119
|
-
|
|
120
|
-
await writeFile(temporary,`${JSON.stringify(record,null,2)}\n`,{mode:0o600});await rename(temporary,file)
|
|
115
|
+
store.set(this.key(actionId), record)
|
|
121
116
|
return record
|
|
122
117
|
}
|
|
123
118
|
|
|
124
119
|
async getDecision(actionId: string): Promise<ApprovalRecord | null> {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return parsed
|
|
130
|
-
} catch (error: unknown) {
|
|
131
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
|
|
132
|
-
throw error
|
|
133
|
-
}
|
|
120
|
+
const record = approvalsFor(this.controlDir).get(this.key(actionId)) ?? null
|
|
121
|
+
if (record && (!isApprovalRecord(record) || record.actionId !== actionId))
|
|
122
|
+
throw new Error('Invalid approval record shape')
|
|
123
|
+
return record
|
|
134
124
|
}
|
|
135
125
|
}
|
package/src/channel-backend.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
1
|
import type { Config } from './config.js'
|
|
3
2
|
import type { RunRecord } from './runs.js'
|
|
4
|
-
import {
|
|
3
|
+
import { readStagedAttachment } from './files.js'
|
|
5
4
|
|
|
6
5
|
// Application-owned jobs; no CLI state, provider credentials or business routing here.
|
|
7
6
|
export async function dispatchChannel(config: Config, run: RunRecord): Promise<string | null> {
|
|
@@ -14,7 +13,7 @@ export async function dispatchChannel(config: Config, run: RunRecord): Promise<s
|
|
|
14
13
|
for (const item of run.items ?? []) {
|
|
15
14
|
let attachment
|
|
16
15
|
if (item.attachment) {
|
|
17
|
-
const bytes = await
|
|
16
|
+
const bytes = await readStagedAttachment([config.controlDir, config.workspace], item.attachment.path)
|
|
18
17
|
if (bytes.length > 12 * 1024 * 1024) throw new Error('Channel attachment exceeds 12 MB')
|
|
19
18
|
attachment = { type: item.attachment.type, data: bytes.toString('base64') }
|
|
20
19
|
}
|
package/src/codex-session.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { spawn, type ChildProcess } from 'node:child_process'
|
|
|
3
3
|
import { createInterface } from 'node:readline'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import path from 'node:path'
|
|
6
|
-
import { terminateJob } from './executor.js'
|
|
6
|
+
import { terminateJob, validateCodexProvider, type CodexProviderBinding } from './executor.js'
|
|
7
7
|
|
|
8
|
-
type Options = {workspace:string;controlDir:string;toolsHome?:string;sharedWorkspace?:string;additionalWorkspaces?:string[];model?:string;effort?:string;prompt:string;codexSandbox?:'external'}
|
|
8
|
+
type Options = {workspace:string;controlDir:string;toolsHome?:string;sharedWorkspace?:string;additionalWorkspaces?:string[];model?:string;effort?:string;prompt:string;codexSandbox?:'external';codexProvider?:CodexProviderBinding}
|
|
9
9
|
type Message = {id?:number;method?:string;params?:any;result?:any;error?:{message:string;code?:number}}
|
|
10
10
|
|
|
11
11
|
// Keep Codex's native session alive. Codex itself starts goal continuation turns;
|
|
@@ -13,6 +13,7 @@ type Message = {id?:number;method?:string;params?:any;result?:any;error?:{messag
|
|
|
13
13
|
export async function runCodexSession(options:Options, io:{launch?:()=>ChildProcess;emit?:(line:string)=>void}={}):Promise<number> {
|
|
14
14
|
options = executionDefaults('codex', options)
|
|
15
15
|
if (options.codexSandbox !== undefined && options.codexSandbox !== 'external') throw new Error('Invalid Codex sandbox selection')
|
|
16
|
+
const provider = options.codexProvider ? validateCodexProvider(options.codexProvider) : undefined
|
|
16
17
|
const child=io.launch?.() ?? spawn('codex',['app-server','--stdio','--disable','memories','--enable','skip_host_skill_discovery'],{cwd:options.workspace,env:process.env,stdio:['pipe','pipe','pipe']})
|
|
17
18
|
const emit=io.emit ?? (line=>process.stdout.write(line+'\n'))
|
|
18
19
|
let id=0,threadId:string|undefined,activeTurn:string|undefined,finished=false,sawTurn=false,hadGoal=false
|
|
@@ -73,7 +74,8 @@ export async function runCodexSession(options:Options, io:{launch?:()=>ChildProc
|
|
|
73
74
|
const result=await request('thread/start',{
|
|
74
75
|
cwd:options.workspace,approvalPolicy:'never',sandbox:options.codexSandbox === 'external' ? 'danger-full-access' : 'workspace-write',model:options.model,
|
|
75
76
|
config:{project_root_markers:['AGENTS.md','.git'],'sandbox_workspace_write.writable_roots':[options.controlDir,...(options.toolsHome?[options.toolsHome]:[]),...(options.sharedWorkspace?[options.sharedWorkspace]:[]),...(options.additionalWorkspaces ?? [])],
|
|
76
|
-
'sandbox_workspace_write.network_access':Boolean(options.toolsHome),...(options.effort?{model_reasoning_effort:options.effort}:{})
|
|
77
|
+
'sandbox_workspace_write.network_access':Boolean(options.toolsHome),...(options.effort?{model_reasoning_effort:options.effort}:{}),
|
|
78
|
+
...(provider?{model_provider:provider.id,[`model_providers.${provider.id}`]:{name:provider.name,base_url:provider.baseUrl,env_key:provider.envKey,wire_api:'responses',supports_websockets:false}}:{})},
|
|
77
79
|
})
|
|
78
80
|
threadId=result.thread?.id
|
|
79
81
|
if(!threadId)throw new Error('Codex did not return a native thread ID')
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseWebLauncher, type WebLauncher } from './web-launcher.js'
|
|
2
2
|
import { repairEnabled } from './repair-policy.js'
|
|
3
|
+
import { isolationTransport, resolveIsolation, type IsolationClass } from './isolation.js'
|
|
3
4
|
import path from 'node:path'
|
|
4
5
|
import { homedir } from 'node:os'
|
|
5
6
|
|
|
@@ -15,6 +16,7 @@ export type Config = ControlConfig & {
|
|
|
15
16
|
telegramBotToken: string
|
|
16
17
|
workspace: string
|
|
17
18
|
executorTimeoutMs: number
|
|
19
|
+
isolation?: IsolationClass
|
|
18
20
|
codexSandbox?: 'external'
|
|
19
21
|
codexAutoCompactTokens?: number
|
|
20
22
|
executorCli: string
|
|
@@ -24,10 +26,6 @@ export type Config = ControlConfig & {
|
|
|
24
26
|
applicationHost?: string
|
|
25
27
|
geminiApiKey?: string
|
|
26
28
|
openaiApiKey?: string
|
|
27
|
-
pagerDutyRoutingKey?: string
|
|
28
|
-
pagerDutyStocksHealthUrl?: string
|
|
29
|
-
pagerDutyPollMs?: number
|
|
30
|
-
pagerDutyFailureThreshold?: number
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
const positiveInteger = (value: string | undefined, name: string, fallback?: number): number => {
|
|
@@ -47,29 +45,28 @@ export const loadControlConfig = (env: NodeJS.ProcessEnv = process.env): Control
|
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
|
|
48
|
+
export const telegramToken = (env: NodeJS.ProcessEnv = process.env): string => {
|
|
49
|
+
const token = env.TELEGRAM_BOT_TOKEN?.trim() || ''
|
|
50
|
+
if (token && !/^\d{5,}:[A-Za-z0-9_-]{20,}$/.test(token)) throw new Error('TELEGRAM_BOT_TOKEN is malformed; refusing to start Telegram transport')
|
|
51
|
+
return token
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
export const loadConfig = (env: NodeJS.ProcessEnv = process.env): Config => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const telegramBotToken =
|
|
55
|
+
const legacy = env.EZ_TELEGRAM_ENABLED
|
|
56
|
+
if (legacy !== undefined && legacy !== 'true' && legacy !== 'false') throw new Error('EZ_TELEGRAM_ENABLED must be true or false')
|
|
57
|
+
const telegramBotToken = telegramToken(env)
|
|
58
|
+
const telegramEnabled = Boolean(telegramBotToken)
|
|
59
|
+
if (legacy !== undefined && (legacy === 'false') === telegramEnabled)
|
|
60
|
+
throw new Error('EZ_TELEGRAM_ENABLED was removed; it contradicts the inferred Telegram transport (a token means enabled). Unset EZ_TELEGRAM_ENABLED, and remove any stale TELEGRAM_BOT_TOKEN to stay application-only.')
|
|
54
61
|
if (!telegramEnabled && !env.EZ_APPLICATION_PORT) throw new Error('Application-only execution requires EZ_APPLICATION_PORT')
|
|
55
|
-
if (telegramEnabled && !telegramBotToken) throw new Error('TELEGRAM_BOT_TOKEN is required')
|
|
56
62
|
|
|
57
63
|
if (env.EZ_CHANNEL_BACKEND_URL && !env.EZ_CHANNEL_BACKEND_TOKEN?.trim()) throw new Error('EZ_CHANNEL_BACKEND_TOKEN is required')
|
|
58
64
|
if (env.EZ_APPLICATION_PORT && env.EZ_CHANNEL_BACKEND_URL) throw new Error('Application input requires the native Ez executor, not a channel backend')
|
|
65
|
+
const isolation = resolveIsolation(env)
|
|
66
|
+
const transport = env.EZ_EXECUTOR_TRANSPORT?.trim() || isolationTransport(isolation)
|
|
59
67
|
const codexSandbox = env.EZ_CODEX_SANDBOX?.trim()
|
|
60
68
|
if (codexSandbox && codexSandbox !== 'external') throw new Error('EZ_CODEX_SANDBOX must be external or unset')
|
|
61
|
-
if (codexSandbox && (env.EZ_CHANNEL_BACKEND_URL ||
|
|
62
|
-
const pagerDutyRoutingKey = env.PAGERDUTY_ROUTING_KEY?.trim()
|
|
63
|
-
const pagerDutyStocksHealthUrl = env.EZ_PAGERDUTY_STOCKS_HEALTH_URL?.trim()
|
|
64
|
-
if (pagerDutyStocksHealthUrl && !pagerDutyRoutingKey)
|
|
65
|
-
throw new Error('PAGERDUTY_ROUTING_KEY is required when EZ_PAGERDUTY_STOCKS_HEALTH_URL is set')
|
|
66
|
-
if (pagerDutyStocksHealthUrl) {
|
|
67
|
-
let url: URL
|
|
68
|
-
try { url = new URL(pagerDutyStocksHealthUrl) }
|
|
69
|
-
catch { throw new Error('EZ_PAGERDUTY_STOCKS_HEALTH_URL must be an absolute HTTP(S) URL') }
|
|
70
|
-
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash)
|
|
71
|
-
throw new Error('EZ_PAGERDUTY_STOCKS_HEALTH_URL must be an absolute HTTP(S) URL without credentials or a fragment')
|
|
72
|
-
}
|
|
69
|
+
if (codexSandbox && (env.EZ_CHANNEL_BACKEND_URL || transport !== 'local')) throw new Error('External Codex sandbox requires native local execution')
|
|
73
70
|
return {
|
|
74
71
|
...loadControlConfig(env),
|
|
75
72
|
telegramEnabled,
|
|
@@ -78,6 +75,7 @@ export const loadConfig = (env: NodeJS.ProcessEnv = process.env): Config => {
|
|
|
78
75
|
repairEnabled: repairEnabled(env.EZ_REPAIR_ENABLED),
|
|
79
76
|
workspace: path.resolve(env.EZ_AGENT_WORKSPACE?.trim() || './agent'),
|
|
80
77
|
executorTimeoutMs: 0,
|
|
78
|
+
isolation,
|
|
81
79
|
codexSandbox: codexSandbox === 'external' ? 'external' : undefined,
|
|
82
80
|
codexAutoCompactTokens: !env.EZ_CODEX_AUTO_COMPACT_TOKENS?.trim() ? undefined : positiveInteger(env.EZ_CODEX_AUTO_COMPACT_TOKENS, 'EZ_CODEX_AUTO_COMPACT_TOKENS'),
|
|
83
81
|
executorCli: env.EZ_EXECUTOR_CLI?.trim() || 'agy',
|
|
@@ -87,13 +85,5 @@ export const loadConfig = (env: NodeJS.ProcessEnv = process.env): Config => {
|
|
|
87
85
|
channelBackendToken: env.EZ_CHANNEL_BACKEND_TOKEN?.trim(),
|
|
88
86
|
geminiApiKey: env.GEMINI_API_KEY?.trim(),
|
|
89
87
|
openaiApiKey: env.OPENAI_API_KEY?.trim(),
|
|
90
|
-
pagerDutyRoutingKey,
|
|
91
|
-
pagerDutyStocksHealthUrl,
|
|
92
|
-
pagerDutyPollMs: pagerDutyRoutingKey && pagerDutyStocksHealthUrl
|
|
93
|
-
? positiveInteger(env.EZ_PAGERDUTY_POLL_SECONDS, 'EZ_PAGERDUTY_POLL_SECONDS', 30) * 1_000
|
|
94
|
-
: undefined,
|
|
95
|
-
pagerDutyFailureThreshold: pagerDutyRoutingKey && pagerDutyStocksHealthUrl
|
|
96
|
-
? positiveInteger(env.EZ_PAGERDUTY_FAILURE_THRESHOLD, 'EZ_PAGERDUTY_FAILURE_THRESHOLD', 3)
|
|
97
|
-
: undefined,
|
|
98
88
|
}
|
|
99
89
|
}
|