evolcore 0.0.29 → 0.0.30

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/agents/codex-runner.js +61 -14
  3. package/dist/aun/aid/agentmd.js +224 -19
  4. package/dist/aun/aid/index.js +2 -1
  5. package/dist/aun/aid/managed-operation.js +70 -4
  6. package/dist/aun/group/index.js +1 -0
  7. package/dist/{core → aun}/group/invite-service.js +4 -4
  8. package/dist/channels/daemon.js +1 -1
  9. package/dist/cli/agent-command.js +7 -7
  10. package/dist/cli/aun-commands.js +97 -37
  11. package/dist/cli/data-command.js +2 -1
  12. package/dist/cli/managed-command-guard.js +2 -2
  13. package/dist/cli/model.js +1 -1
  14. package/dist/cli/response.js +1 -1
  15. package/dist/cli/trigger-command.js +52 -29
  16. package/dist/config/boot-log.js +95 -1
  17. package/dist/config/builtin-roles.js +1 -0
  18. package/dist/config/peer-role-resolver.js +21 -0
  19. package/dist/core/agent-ipc-registration.js +409 -0
  20. package/dist/core/agent-runtime.js +435 -0
  21. package/dist/core/auth/delegation-authorization.js +58 -0
  22. package/dist/core/auth/operation-catalog.js +8 -0
  23. package/dist/core/bootstrap-service.js +65 -1
  24. package/dist/core/channel-runtime.js +435 -0
  25. package/dist/core/command/command-handler.js +4 -1
  26. package/dist/core/daemon-bootstrap.js +151 -0
  27. package/dist/core/daemon-control.js +205 -0
  28. package/dist/core/daemon-lifecycle.js +225 -0
  29. package/dist/core/data-migration.js +4 -1
  30. package/dist/core/handoff/ipc-registration.js +156 -0
  31. package/dist/core/ipc-registration.js +457 -0
  32. package/dist/core/memory/consolidation-trigger.js +8 -0
  33. package/dist/core/memory/consolidation.js +838 -0
  34. package/dist/core/message/aun-ipc-sender.js +226 -0
  35. package/dist/core/message/outbound.js +165 -0
  36. package/dist/core/permission/approval-gateway.js +13 -0
  37. package/dist/core/permission/ec-command-parser.js +80 -35
  38. package/dist/core/permission/sandbox-runtime.js +23 -2
  39. package/dist/core/permission/tool-error-code.js +1 -0
  40. package/dist/core/permission/tool-policy.js +61 -24
  41. package/dist/core/session/session-idle-monitor.js +17 -0
  42. package/dist/index.js +352 -4841
  43. package/dist/ipc.js +2 -1
  44. package/dist/stats/db.js +24 -0
  45. package/dist/stats/index.js +1 -1
  46. package/dist/stats/writer.js +25 -1
  47. package/dist/trigger/application-service.js +157 -0
  48. package/dist/trigger/authorization.js +371 -0
  49. package/dist/trigger/ipc-service.js +406 -0
  50. package/dist/trigger/local-files.js +133 -0
  51. package/dist/trigger/manager.js +235 -22
  52. package/dist/trigger/scheduler.js +30 -5
  53. package/dist/trigger/script-executor.js +176 -39
  54. package/dist/trigger/script-sandbox.js +458 -0
  55. package/dist/trigger/system-definitions.js +188 -0
  56. package/dist/trigger/validation.js +0 -3
  57. package/dist/utils/aid-bind.js +1 -1
  58. package/dist/utils/evolcore-version.js +74 -0
  59. package/dist/utils/logger.js +16 -0
  60. package/kits/docs/evolcore/INDEX.md +1 -1
  61. package/kits/docs/evolcore/aid.md +9 -1
  62. package/kits/docs/evolcore/model.md +3 -2
  63. package/kits/docs/evolcore/rpc.md +4 -0
  64. package/kits/docs/evolcore/trigger.md +15 -1
  65. package/kits/schemas/relation-config.schema.9.json +1 -1
  66. package/kits/templates/system-fragments/commands.md +0 -1
  67. package/package.json +1 -1
  68. package/dist/core/memory/launch-consolidation.js +0 -395
@@ -0,0 +1,406 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getEventCatalog } from '../core/event-catalog.js';
4
+ import { authorizeOperation, canApprovePersistentFullAccessTrigger, isCrossAgentTriggerOperationAllowed, } from '../core/auth/auth-gateway.js';
5
+ import { auditFullAccessEvent } from '../core/auth/authorization-audit.js';
6
+ import { assertTriggerLocalFileAuthority, daemonPrivilegedTriggerReason, } from './authorization.js';
7
+ import { decodeTriggerScriptFile, resolveTriggerLocalPath } from './local-files.js';
8
+ import { applyTriggerPatch } from './patch.js';
9
+ import { definitionRevision, normalizeTriggerDefinition, splitScriptCommand } from './validation.js';
10
+ export function createTriggerBaseagentResolver({ daemonTriggerOwnerAid, getAgentName, getDaemonAgentName, agentMap, }) {
11
+ return agentAid => {
12
+ const ownerName = getAgentName(agentAid)
13
+ ?? (agentAid === daemonTriggerOwnerAid ? getDaemonAgentName() : undefined);
14
+ if (!ownerName)
15
+ return [];
16
+ const prefix = `${ownerName}::`;
17
+ return [...agentMap.keys()]
18
+ .filter(key => key.startsWith(prefix))
19
+ .map(key => key.slice(prefix.length));
20
+ };
21
+ }
22
+ export function registerTriggerIpcExecutor(ipcServer, dependencies) {
23
+ ipcServer.setTriggerExecutor(createTriggerIpcExecutor(dependencies));
24
+ }
25
+ export function materializeTriggerCreateBaseagent(input, agentAid, dependencies) {
26
+ const rawExecution = input.execution;
27
+ if (!rawExecution || typeof rawExecution !== 'object' || Array.isArray(rawExecution))
28
+ return input;
29
+ const execution = rawExecution;
30
+ if (execution.type !== 'trigger_session') {
31
+ if (execution.baseagent !== undefined) {
32
+ throw new Error('execution.baseagent is only allowed when execution.type=trigger_session');
33
+ }
34
+ return input;
35
+ }
36
+ if (execution.baseagent !== undefined
37
+ && (typeof execution.baseagent !== 'string' || !execution.baseagent.trim())) {
38
+ throw new Error('execution.baseagent must be a non-empty string');
39
+ }
40
+ const baseagent = typeof execution.baseagent === 'string'
41
+ ? execution.baseagent.trim()
42
+ : dependencies.getAgentBaseagent(agentAid)
43
+ ?? (agentAid === dependencies.daemonTriggerOwnerAid ? dependencies.daemonTriggerBaseagent : undefined);
44
+ if (!baseagent)
45
+ throw new Error('unable to resolve Trigger session baseagent');
46
+ const available = dependencies.getAvailableBaseagents(agentAid);
47
+ if (!available.includes(baseagent)) {
48
+ throw new Error(`Trigger baseagent unavailable for ${agentAid}: ${baseagent} (available: ${available.join(', ') || 'none'})`);
49
+ }
50
+ return { ...input, execution: { ...execution, baseagent } };
51
+ }
52
+ export function createTriggerIpcExecutor(dependencies) {
53
+ return async (cmd) => {
54
+ const schedulerFor = (agentAid) => {
55
+ const scheduler = dependencies.getScheduler(agentAid);
56
+ if (!scheduler)
57
+ throw new Error(`trigger scheduler not found for agent: ${agentAid}`);
58
+ return scheduler;
59
+ };
60
+ const requireAgent = (agentAid) => {
61
+ if (typeof agentAid !== 'string' || !agentAid)
62
+ throw new Error('missing agentAid');
63
+ if (agentAid === dependencies.daemonTriggerOwnerAid)
64
+ return agentAid;
65
+ if (!dependencies.hasAgent(agentAid))
66
+ throw new Error(`agent not found: ${agentAid}`);
67
+ return agentAid;
68
+ };
69
+ const auditFullAccessTriggerConfiguration = (event, definition, actor, reason) => {
70
+ auditFullAccessEvent({
71
+ event,
72
+ source: 'trigger',
73
+ actorId: actor.origin.peerId,
74
+ processRole: actor.daemonOwner ? 'daemon-owner' : actor.subject.processRole,
75
+ authorizedBy: definition.authorizedBy,
76
+ agentAid: definition.agentAid,
77
+ triggerId: definition.id,
78
+ reason,
79
+ });
80
+ };
81
+ const rejectUnapprovedFullAccessConfiguration = (definition, actor) => {
82
+ const approvalSource = actor.control ? 'daemon-service' : 'agent-delegation';
83
+ if (definition.execution.permissionMode === 'fullaccess'
84
+ && !actor.fullAccess
85
+ && !canApprovePersistentFullAccessTrigger({
86
+ daemonOwner: actor.daemonOwner,
87
+ source: approvalSource,
88
+ })) {
89
+ throw new Error('persistent fullaccess Trigger requires an explicit user or management-channel action');
90
+ }
91
+ };
92
+ const withFullAccessConfigurationProvenance = (definition, actor) => {
93
+ if (definition.execution.permissionMode !== 'fullaccess') {
94
+ const { authorizedBy: _authorizedBy, ...deescalated } = definition;
95
+ return deescalated;
96
+ }
97
+ const authorizedBy = actor.fullAccess
98
+ ? actor.subject.authorizedBy
99
+ : actor.subject.principalId ?? actor.origin.peerId;
100
+ if (!authorizedBy)
101
+ throw new Error('fullaccess Trigger approval provenance is unavailable');
102
+ return { ...definition, authorizedBy };
103
+ };
104
+ const authorizeTrigger = async (agentAid, operation, triggerId, contextOnly = false) => {
105
+ const actor = await dependencies.authenticateActor(agentAid, cmd.actorSessionId, cmd.delegationToken, cmd.delegationCommandHash, cmd.controlToken, contextOnly);
106
+ if (actor.permissionMode === 'readonly' && !actor.fullAccess
107
+ && !['trigger.list', 'trigger.show', 'trigger.history', 'trigger.eventCatalog'].includes(operation)) {
108
+ throw new Error('readonly tasks cannot modify or run Triggers');
109
+ }
110
+ const isCrossAgent = !actor.control && actor.selfAid !== agentAid;
111
+ if (!isCrossAgentTriggerOperationAllowed({
112
+ control: actor.control,
113
+ daemonOwner: actor.daemonOwner || actor.fullAccess,
114
+ taskAgentAid: actor.selfAid,
115
+ targetAgentAid: agentAid,
116
+ targetManagement: actor.management,
117
+ operation,
118
+ })) {
119
+ if (isCrossAgent)
120
+ throw new Error('cross-agent Trigger operations require DaemonOwner');
121
+ throw new Error('trigger agent does not match authenticated task agent');
122
+ }
123
+ const operationDecision = authorizeOperation({
124
+ source: actor.control ? 'control' : 'agent-tool',
125
+ subject: actor.subject,
126
+ intent: {
127
+ operation,
128
+ scope: 'relation',
129
+ source: actor.control ? 'control' : 'agent-tool',
130
+ args: {
131
+ self: agentAid,
132
+ taskAgentAid: actor.selfAid,
133
+ targetAgentAid: agentAid,
134
+ peer: actor.origin.peerId,
135
+ peerKey: actor.subject.peerKey,
136
+ ...(triggerId ? { triggerId } : {}),
137
+ },
138
+ },
139
+ });
140
+ if (!operationDecision.allow)
141
+ throw new Error(operationDecision.reason);
142
+ if (!triggerId)
143
+ return actor;
144
+ const definition = schedulerFor(agentAid).list({ all: true }).find(trigger => trigger.id === triggerId);
145
+ const privilegedReason = definition ? daemonPrivilegedTriggerReason(definition) : undefined;
146
+ const persistedFullAccessApproval = privilegedReason === 'persistent fullaccess Trigger';
147
+ const deescalationOperation = operation === 'trigger.update'
148
+ || operation === 'trigger.setEnabled'
149
+ || operation === 'trigger.cancel'
150
+ || operation === 'trigger.delete';
151
+ if (privilegedReason && !persistedFullAccessApproval && !actor.daemonPrivileged && !deescalationOperation) {
152
+ throw new Error(`${privilegedReason} requires DaemonOwner`);
153
+ }
154
+ if (actor.management)
155
+ return actor;
156
+ if (!definition
157
+ || definition.origin?.peerId !== actor.origin?.peerId
158
+ || definition.origin?.channelKey !== actor.origin?.channelKey) {
159
+ throw new Error('trigger not found or access denied');
160
+ }
161
+ return actor;
162
+ };
163
+ switch (cmd.type) {
164
+ case 'trigger.authorizeLocalFiles': {
165
+ if (cmd.operation !== 'trigger.create' && cmd.operation !== 'trigger.update') {
166
+ throw new Error('local file authorization only supports Trigger create/update');
167
+ }
168
+ const agentAid = requireAgent(cmd.agentAid);
169
+ if (cmd.operation === 'trigger.update' && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
170
+ throw new Error('missing triggerId');
171
+ }
172
+ if (typeof cmd.actorSessionId !== 'string' || !cmd.actorSessionId) {
173
+ throw new Error('local Trigger file authorization requires a managed task');
174
+ }
175
+ const actor = await authorizeTrigger(agentAid, cmd.operation, cmd.triggerId, true);
176
+ assertTriggerLocalFileAuthority(actor);
177
+ if (actor.fullAccess)
178
+ return { ok: true, fullAccess: true };
179
+ const session = await dependencies.getSessionById(cmd.actorSessionId);
180
+ if (!session)
181
+ throw new Error('Trigger actor session is unavailable');
182
+ if (typeof cmd.cwd !== 'string' || !path.isAbsolute(cmd.cwd))
183
+ throw new Error('missing absolute command cwd');
184
+ const fileScope = {
185
+ selfAid: agentAid,
186
+ projectPath: fs.realpathSync.native(path.resolve(session.projectPath)),
187
+ sessionRuntimeDir: actor.sessionRuntimeDir,
188
+ cwd: cmd.cwd,
189
+ };
190
+ fileScope.cwd = resolveTriggerLocalPath(cmd.cwd, fileScope);
191
+ if (!fs.statSync(fileScope.cwd).isDirectory())
192
+ throw new Error('command cwd must be a directory');
193
+ return { ok: true, fullAccess: false, fileScope };
194
+ }
195
+ case 'trigger.list': {
196
+ const agentAid = requireAgent(cmd.agentAid);
197
+ const actor = await authorizeTrigger(agentAid, 'trigger.list');
198
+ const scheduler = schedulerFor(agentAid);
199
+ const definitions = scheduler.list({ all: cmd.all === true }).filter(trigger => ((actor.daemonPrivileged
200
+ || !daemonPrivilegedTriggerReason(trigger)
201
+ || daemonPrivilegedTriggerReason(trigger) === 'persistent fullaccess Trigger')
202
+ && (actor.management
203
+ || (trigger.origin?.peerId === actor.origin?.peerId && trigger.origin?.channelKey === actor.origin?.channelKey))));
204
+ return { ok: true, triggers: scheduler.listItems(definitions) };
205
+ }
206
+ case 'trigger.show': {
207
+ const agentAid = requireAgent(cmd.agentAid);
208
+ if (!cmd.triggerId)
209
+ throw new Error('missing triggerId');
210
+ await authorizeTrigger(agentAid, 'trigger.show', cmd.triggerId);
211
+ return {
212
+ ok: true,
213
+ ...schedulerFor(agentAid).show(cmd.triggerId, {
214
+ includeScriptPreview: cmd.includeScriptPreview !== false,
215
+ }),
216
+ };
217
+ }
218
+ case 'trigger.history': {
219
+ const agentAid = requireAgent(cmd.agentAid);
220
+ const limit = cmd.limit === undefined ? 100 : Number(cmd.limit);
221
+ if (!Number.isInteger(limit) || limit <= 0 || limit > 10_000)
222
+ throw new Error('invalid history limit');
223
+ if (cmd.triggerId !== undefined && (typeof cmd.triggerId !== 'string' || !cmd.triggerId)) {
224
+ throw new Error('invalid triggerId');
225
+ }
226
+ let actor;
227
+ if (cmd.triggerId)
228
+ await authorizeTrigger(agentAid, 'trigger.history', cmd.triggerId);
229
+ else
230
+ actor = await authorizeTrigger(agentAid, 'trigger.history');
231
+ if (!cmd.triggerId && !actor?.management) {
232
+ throw new Error('triggerId is required for non-management history access');
233
+ }
234
+ const scheduler = schedulerFor(agentAid);
235
+ const events = scheduler.history(cmd.triggerId, limit);
236
+ if (cmd.triggerId || actor?.daemonPrivileged)
237
+ return { ok: true, events };
238
+ const privilegedIds = new Set(scheduler.list({ all: true })
239
+ .filter(trigger => !!daemonPrivilegedTriggerReason(trigger))
240
+ .map(trigger => trigger.id));
241
+ return { ok: true, events: events.filter((event) => !privilegedIds.has(String(event.triggerId ?? ''))) };
242
+ }
243
+ case 'trigger.eventCatalog': {
244
+ const agentAid = requireAgent(cmd.agentAid);
245
+ const actor = await authorizeTrigger(agentAid, 'trigger.eventCatalog');
246
+ if (cmd.includeInternal === true && !actor.daemonPrivileged) {
247
+ throw new Error('internal Trigger event catalog requires DaemonOwner');
248
+ }
249
+ return { ok: true, ...getEventCatalog({ includeInternal: cmd.includeInternal === true }) };
250
+ }
251
+ case 'trigger.create': {
252
+ const rawDefinition = cmd.definition;
253
+ if (!rawDefinition || typeof rawDefinition !== 'object' || Array.isArray(rawDefinition)) {
254
+ throw new Error('trigger definition must be an object');
255
+ }
256
+ const agentAid = requireAgent(rawDefinition.agentAid);
257
+ const actor = await authorizeTrigger(agentAid, 'trigger.create');
258
+ const materialized = materializeTriggerCreateBaseagent({ ...rawDefinition, origin: actor.origin }, agentAid, dependencies);
259
+ let definition = normalizeTriggerDefinition(materialized);
260
+ rejectUnapprovedFullAccessConfiguration(definition, actor);
261
+ definition = withFullAccessConfigurationProvenance(definition, actor);
262
+ dependencies.validateTriggerDefinitionForActor(definition, actor);
263
+ requireAgent(definition.agentAid);
264
+ dependencies.validateTriggerFeedbackChannels(definition);
265
+ const trigger = schedulerFor(definition.agentAid).create(definition, cmd.files ?? [], { enable: cmd.enable });
266
+ if (trigger.execution.permissionMode === 'fullaccess') {
267
+ auditFullAccessTriggerConfiguration('fullaccess.trigger.configured', trigger, actor, 'fullaccess Trigger created through IPC');
268
+ }
269
+ return { ok: true, trigger };
270
+ }
271
+ case 'trigger.update': {
272
+ const agentAid = requireAgent(cmd.agentAid);
273
+ if (!cmd.triggerId)
274
+ throw new Error('missing triggerId');
275
+ const actor = await authorizeTrigger(agentAid, 'trigger.update', cmd.triggerId);
276
+ const scheduler = schedulerFor(agentAid);
277
+ const existing = scheduler.list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
278
+ if (!existing)
279
+ throw new Error(`trigger not found: ${cmd.triggerId}`);
280
+ const rawPatch = cmd.patch && typeof cmd.patch === 'object' && !Array.isArray(cmd.patch)
281
+ ? cmd.patch
282
+ : {};
283
+ const rawScriptFile = rawPatch.scriptFile;
284
+ if (rawScriptFile !== undefined && rawScriptFile !== true) {
285
+ throw new Error('scriptFile must be true');
286
+ }
287
+ const patchScriptFile = rawScriptFile === true;
288
+ const { scriptFile: _scriptFile, ...definitionPatch } = rawPatch;
289
+ const scriptFileBase64 = cmd.scriptFileBase64;
290
+ if (patchScriptFile && scriptFileBase64 === undefined) {
291
+ throw new Error('--script-file requires scriptFileBase64');
292
+ }
293
+ if (!patchScriptFile && scriptFileBase64 !== undefined) {
294
+ throw new Error('scriptFileBase64 requires --script-file');
295
+ }
296
+ if (scriptFileBase64 !== undefined) {
297
+ if (typeof scriptFileBase64 !== 'string') {
298
+ throw new Error('scriptFileBase64 must be a base64 string');
299
+ }
300
+ if (!actor.daemonPrivileged && actor.role !== 'owner') {
301
+ throw new Error('updating an Agent-scoped script Trigger requires the current Agent owner');
302
+ }
303
+ if (existing.execution.type !== 'script' || !existing.execution.script) {
304
+ throw new Error('--script-file only applies to script Triggers');
305
+ }
306
+ decodeTriggerScriptFile(scriptFileBase64);
307
+ }
308
+ const currentRevision = definitionRevision(existing);
309
+ if (cmd.expectedRevision !== undefined && cmd.expectedRevision !== currentRevision) {
310
+ throw new Error(`trigger revision conflict: expected ${cmd.expectedRevision}, current ${currentRevision}`);
311
+ }
312
+ let definition = Object.keys(definitionPatch).length > 0
313
+ ? applyTriggerPatch(existing, definitionPatch, { fromPromptFile: cmd.promptFile === true })
314
+ : existing;
315
+ rejectUnapprovedFullAccessConfiguration(definition, actor);
316
+ definition = withFullAccessConfigurationProvenance(definition, actor);
317
+ dependencies.validateTriggerDefinitionForActor(definition, actor);
318
+ dependencies.validateTriggerFeedbackChannels(definition);
319
+ const files = scriptFileBase64 !== undefined && existing.execution.script
320
+ ? [{
321
+ relativePath: splitScriptCommand(existing.execution.script.path).path,
322
+ contentBase64: scriptFileBase64,
323
+ }]
324
+ : [];
325
+ const trigger = scheduler.update(cmd.triggerId, definition, files);
326
+ if (trigger.execution.permissionMode === 'fullaccess') {
327
+ auditFullAccessTriggerConfiguration('fullaccess.trigger.configured', trigger, actor, existing.execution.permissionMode === 'fullaccess'
328
+ ? 'fullaccess Trigger updated through IPC'
329
+ : 'Trigger elevated to fullaccess through IPC');
330
+ }
331
+ else if (existing.execution.permissionMode === 'fullaccess') {
332
+ auditFullAccessTriggerConfiguration('fullaccess.trigger.deescalated', existing, actor, 'Trigger permissionMode changed from fullaccess through IPC');
333
+ }
334
+ return { ok: true, trigger: scheduler.listItem(trigger), revision: definitionRevision(trigger) };
335
+ }
336
+ case 'trigger.setEnabled': {
337
+ const agentAid = requireAgent(cmd.agentAid);
338
+ if (!cmd.triggerId)
339
+ throw new Error('missing triggerId');
340
+ if (typeof cmd.enabled !== 'boolean')
341
+ throw new Error('missing enabled');
342
+ const actor = await authorizeTrigger(agentAid, 'trigger.setEnabled', cmd.triggerId);
343
+ const scheduler = schedulerFor(agentAid);
344
+ const existing = scheduler.list({ all: true }).find(trigger => trigger.id === cmd.triggerId);
345
+ if (!existing)
346
+ throw new Error(`trigger not found: ${cmd.triggerId}`);
347
+ if (cmd.enabled && existing.execution.permissionMode === 'fullaccess') {
348
+ rejectUnapprovedFullAccessConfiguration(existing, actor);
349
+ if (!actor.daemonOwner && !actor.fullAccess) {
350
+ throw new Error('enabling a fullaccess Trigger requires a DaemonOwner or fullaccess-run');
351
+ }
352
+ }
353
+ if (cmd.enabled && existing.execution.permissionMode === 'fullaccess') {
354
+ scheduler.update(cmd.triggerId, withFullAccessConfigurationProvenance(existing, actor));
355
+ }
356
+ const trigger = scheduler.setEnabled(cmd.triggerId, cmd.enabled);
357
+ if (cmd.enabled && trigger.execution.permissionMode === 'fullaccess') {
358
+ auditFullAccessTriggerConfiguration('fullaccess.trigger.configured', trigger, actor, 'fullaccess Trigger enabled through IPC');
359
+ }
360
+ return { ok: true, trigger };
361
+ }
362
+ case 'trigger.cancel': {
363
+ const agentAid = requireAgent(cmd.agentAid);
364
+ if (!cmd.triggerId)
365
+ throw new Error('missing triggerId');
366
+ await authorizeTrigger(agentAid, 'trigger.cancel', cmd.triggerId);
367
+ const trigger = schedulerFor(agentAid).cancel(cmd.triggerId);
368
+ return { ok: true, trigger };
369
+ }
370
+ case 'trigger.delete': {
371
+ const agentAid = requireAgent(cmd.agentAid);
372
+ if (!cmd.triggerId)
373
+ throw new Error('missing triggerId');
374
+ const actor = await authorizeTrigger(agentAid, 'trigger.delete', cmd.triggerId);
375
+ const trigger = schedulerFor(agentAid).delete(cmd.triggerId);
376
+ if (trigger.execution.permissionMode === 'fullaccess') {
377
+ auditFullAccessTriggerConfiguration('fullaccess.trigger.deescalated', trigger, actor, 'fullaccess Trigger deleted through IPC');
378
+ }
379
+ return { ok: true, trigger };
380
+ }
381
+ case 'trigger.run': {
382
+ const agentAid = requireAgent(cmd.agentAid);
383
+ if (!cmd.triggerId)
384
+ throw new Error('missing triggerId');
385
+ await authorizeTrigger(agentAid, 'trigger.run', cmd.triggerId);
386
+ const result = await schedulerFor(agentAid).run(cmd.triggerId, {
387
+ dryRun: cmd.dryRun === true,
388
+ ...(cmd.eventPayload !== undefined ? { eventPayload: cmd.eventPayload } : {}),
389
+ });
390
+ return {
391
+ ok: result.ok,
392
+ result,
393
+ runId: result.runId,
394
+ triggerId: result.triggerId,
395
+ status: result.status,
396
+ reason: result.reason,
397
+ conflictRunId: result.conflictRunId,
398
+ error: result.error,
399
+ audit: result.audit,
400
+ };
401
+ }
402
+ default:
403
+ return { ok: false, error: `unknown trigger command: ${cmd.type}` };
404
+ }
405
+ };
406
+ }
@@ -0,0 +1,133 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveRoot } from '../paths.js';
4
+ import { classifyProtectedPath, isSameOrDescendant } from '../core/permission/protected-paths.js';
5
+ export const TRIGGER_LOCAL_FILE_LIMIT = 4 * 1024 * 1024;
6
+ export function decodeTriggerScriptFile(contentBase64) {
7
+ if (typeof contentBase64 !== 'string'
8
+ || contentBase64.length > 4 * Math.ceil(TRIGGER_LOCAL_FILE_LIMIT / 3)
9
+ || contentBase64.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(contentBase64)) {
10
+ throw new Error('Trigger file content must be canonical base64 under 4 MiB');
11
+ }
12
+ const contents = Buffer.from(contentBase64, 'base64');
13
+ if (contents.length > TRIGGER_LOCAL_FILE_LIMIT || contents.toString('base64') !== contentBase64) {
14
+ throw new Error('Trigger file content must be canonical base64 under 4 MiB');
15
+ }
16
+ return contents;
17
+ }
18
+ export function resolveTriggerLocalPath(input, scope) {
19
+ if (!input || /[\0\r\n]/.test(input) || hasUnsafeWindowsPathSyntax(input))
20
+ throw new Error('invalid Trigger input path');
21
+ const normalizedInput = input.replace(/\\/g, '/');
22
+ if (normalizedInput.split('/').includes('..'))
23
+ throw new Error('Trigger input path cannot traverse parent directories');
24
+ const lexical = path.resolve(scope?.cwd ?? process.cwd(), input);
25
+ if (!scope)
26
+ return fs.realpathSync.native(lexical);
27
+ const project = fs.realpathSync.native(scope.projectPath);
28
+ const runtime = scope.sessionRuntimeDir
29
+ ? fs.realpathSync.native(scope.sessionRuntimeDir)
30
+ : undefined;
31
+ assertSessionRuntimeScope(lexical, runtime);
32
+ const inProjectLexically = isSameOrDescendant(lexical, project);
33
+ const inSessionTempLexically = !!runtime && isSameOrDescendant(lexical, runtime);
34
+ if (!inProjectLexically && !inSessionTempLexically) {
35
+ throw new Error('Trigger input must be inside the current project or session TMPDIR');
36
+ }
37
+ let current = lexical;
38
+ while (true) {
39
+ const stat = fs.lstatSync(current);
40
+ if (stat.isSymbolicLink()) {
41
+ throw new Error('Trigger input must not traverse a symbolic link or junction');
42
+ }
43
+ if (current === project || current === runtime)
44
+ break;
45
+ const parent = path.dirname(current);
46
+ if (parent === current)
47
+ break;
48
+ current = parent;
49
+ }
50
+ const resolved = fs.realpathSync.native(lexical);
51
+ assertSessionRuntimeScope(resolved, runtime);
52
+ const inProject = isSameOrDescendant(lexical, project) && isSameOrDescendant(resolved, project);
53
+ const inSessionTemp = !!runtime
54
+ && isSameOrDescendant(lexical, runtime)
55
+ && isSameOrDescendant(resolved, runtime);
56
+ if (!inProject && !inSessionTemp) {
57
+ throw new Error('Trigger input must be inside the current project or session TMPDIR');
58
+ }
59
+ const root = resolveRoot();
60
+ const lexicalClass = classifyProtectedPath(lexical, { root, selfAid: scope.selfAid });
61
+ const resolvedClass = classifyProtectedPath(resolved, { root, selfAid: scope.selfAid });
62
+ if (isProtectedTriggerInputClass(lexicalClass) || isProtectedTriggerInputClass(resolvedClass)) {
63
+ throw new Error('Trigger input references a protected path');
64
+ }
65
+ return resolved;
66
+ }
67
+ function isProtectedTriggerInputClass(value) {
68
+ return value === 'h-file' || value === 'h-directory' || value === 'l-path' || value === 'unknown';
69
+ }
70
+ function hasUnsafeWindowsPathSyntax(input) {
71
+ if (process.platform !== 'win32')
72
+ return false;
73
+ const normalized = input.replace(/\\/g, '/');
74
+ const withoutDrive = /^[A-Za-z]:\//.test(normalized) ? normalized.slice(2) : normalized;
75
+ return withoutDrive.includes(':') || withoutDrive.split('/').some(segment => (/[. ]$/.test(segment) && segment !== '.' && segment !== '..') || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment));
76
+ }
77
+ function assertSessionRuntimeScope(candidate, runtime) {
78
+ if (!runtime)
79
+ return;
80
+ const parent = path.dirname(runtime);
81
+ if (!isSameOrDescendant(candidate, parent))
82
+ return;
83
+ const firstSegment = path.relative(parent, candidate).split(path.sep)[0];
84
+ if (/^ec-session-[a-f0-9]{24}$/i.test(firstSegment)
85
+ && !isSameOrDescendant(candidate, runtime)) {
86
+ throw new Error('Trigger input must not reference another session TMPDIR');
87
+ }
88
+ }
89
+ export function readTriggerLocalFile(input, scope) {
90
+ const resolved = resolveTriggerLocalPath(input, scope);
91
+ const expected = fs.lstatSync(resolved);
92
+ if (!expected.isFile())
93
+ throw new Error('Trigger input must be a regular file');
94
+ if (scope && expected.nlink !== 1)
95
+ throw new Error('Trigger input must not be hard-linked');
96
+ const noFollow = fs.constants.O_NOFOLLOW ?? 0;
97
+ const descriptor = fs.openSync(resolved, fs.constants.O_RDONLY | noFollow);
98
+ try {
99
+ const opened = fs.fstatSync(descriptor);
100
+ if (!opened.isFile() || opened.dev !== expected.dev || opened.ino !== expected.ino
101
+ || (scope && opened.nlink !== 1)
102
+ || resolveTriggerLocalPath(input, scope) !== resolved) {
103
+ throw new Error('Trigger input changed during validation');
104
+ }
105
+ if (opened.size > TRIGGER_LOCAL_FILE_LIMIT)
106
+ throw new Error('Trigger input exceeds the 4 MiB limit');
107
+ const openedMtime = opened.mtimeMs;
108
+ const openedCtime = opened.ctimeMs;
109
+ const buffer = Buffer.alloc(Math.min(opened.size + 1, TRIGGER_LOCAL_FILE_LIMIT + 1));
110
+ let length = 0;
111
+ while (length < buffer.length) {
112
+ const count = fs.readSync(descriptor, buffer, length, buffer.length - length, length);
113
+ if (!count)
114
+ break;
115
+ length += count;
116
+ }
117
+ const afterRead = fs.fstatSync(descriptor);
118
+ const afterPath = fs.lstatSync(resolved);
119
+ if (length !== opened.size || afterRead.size !== opened.size
120
+ || afterRead.mtimeMs !== openedMtime || afterRead.ctimeMs !== openedCtime
121
+ || afterPath.dev !== opened.dev || afterPath.ino !== opened.ino
122
+ || !afterPath.isFile() || afterPath.isSymbolicLink()
123
+ || afterPath.size !== opened.size || afterPath.mtimeMs !== openedMtime || afterPath.ctimeMs !== openedCtime
124
+ || (scope && (afterRead.nlink !== 1 || afterPath.nlink !== 1))
125
+ || resolveTriggerLocalPath(input, scope) !== resolved) {
126
+ throw new Error('Trigger input changed during reading');
127
+ }
128
+ return buffer.subarray(0, length);
129
+ }
130
+ finally {
131
+ fs.closeSync(descriptor);
132
+ }
133
+ }