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
@@ -1,9 +1,10 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import crypto from 'crypto';
3
4
  import { agentTriggersDir } from '../paths.js';
4
5
  import { assertExplicitAunDeliveryRoutes, definitionRevision, normalizeTriggerDefinition, resolveScriptPath, safeRelativePath, } from './validation.js';
5
- import { atomicWriteJson } from '../core/session/session-fs-store.js';
6
6
  import { TriggerHistoryStore } from './history.js';
7
+ import { decodeTriggerScriptFile } from './local-files.js';
7
8
  export class TriggerDefinitionManager {
8
9
  agentAid;
9
10
  rootDir;
@@ -12,6 +13,7 @@ export class TriggerDefinitionManager {
12
13
  this.agentAid = agentAid;
13
14
  this.rootDir = rootDir;
14
15
  fs.mkdirSync(this.rootDir, { recursive: true });
16
+ assertRealDirectory(this.rootDir, 'Trigger root');
15
17
  this.history = new TriggerHistoryStore(rootDir, agentAid);
16
18
  for (const definition of this.list({ all: true })) {
17
19
  this.history.ensureDefinitionSnapshot(definition);
@@ -63,9 +65,16 @@ export class TriggerDefinitionManager {
63
65
  if (fs.existsSync(dir))
64
66
  throw new Error(`trigger directory already exists: ${definition.id}`);
65
67
  fs.mkdirSync(dir, { recursive: true });
66
- this.writeFiles(dir, files);
67
- this.validateScriptFile(definition, dir);
68
- this.writeDefinition(definition);
68
+ try {
69
+ this.writeFiles(dir, files, definition, () => {
70
+ this.validateScriptFile(definition, dir);
71
+ this.writeDefinition(definition);
72
+ });
73
+ }
74
+ catch (error) {
75
+ fs.rmSync(dir, { recursive: true, force: true });
76
+ throw error;
77
+ }
69
78
  this.clearActive(definition.id);
70
79
  this.history.recordDefinition('trigger.created', definition);
71
80
  return definition;
@@ -91,9 +100,10 @@ export class TriggerDefinitionManager {
91
100
  this.assertUnique(updated, triggerId);
92
101
  const dir = this.triggerDir(triggerId);
93
102
  fs.mkdirSync(dir, { recursive: true });
94
- this.writeFiles(dir, files);
95
- this.validateScriptFile(updated, dir);
96
- this.writeDefinition(updated);
103
+ this.writeFiles(dir, files, updated, () => {
104
+ this.validateScriptFile(updated, dir);
105
+ this.writeDefinition(updated);
106
+ });
97
107
  this.history.recordDefinition('trigger.updated', updated, {
98
108
  previousRevision: definitionRevision(existing),
99
109
  });
@@ -119,7 +129,7 @@ export class TriggerDefinitionManager {
119
129
  return definition;
120
130
  }
121
131
  triggerDir(triggerId) {
122
- return path.join(this.rootDir, safeRelativePath(triggerId));
132
+ return path.join(this.rootDir, safeTriggerId(triggerId));
123
133
  }
124
134
  definitionPath(triggerId) {
125
135
  return path.join(this.triggerDir(triggerId), 'trigger.json');
@@ -191,7 +201,25 @@ export class TriggerDefinitionManager {
191
201
  writeDefinition(definition) {
192
202
  const dir = this.triggerDir(definition.id);
193
203
  fs.mkdirSync(dir, { recursive: true });
194
- atomicWriteJson(this.definitionPath(definition.id), definition);
204
+ assertSafeDirectoryPath(this.rootDir, dir);
205
+ const target = this.definitionPath(definition.id);
206
+ const existing = lstatOptional(target);
207
+ if (existing && (!existing.isFile() || existing.isSymbolicLink() || existing.nlink !== 1)) {
208
+ throw new Error('Trigger definition target must be a regular, unlinked file');
209
+ }
210
+ const temporary = path.join(dir, `.ec-trigger-definition-${crypto.randomBytes(12).toString('hex')}.tmp`);
211
+ try {
212
+ writeNewFile(temporary, JSON.stringify(definition, null, 2) + '\n');
213
+ assertSafeDirectoryPath(this.rootDir, dir);
214
+ assertUnchangedTarget(target, existing);
215
+ fs.renameSync(temporary, target);
216
+ }
217
+ finally {
218
+ try {
219
+ fs.unlinkSync(temporary);
220
+ }
221
+ catch { }
222
+ }
195
223
  }
196
224
  listTriggerIds() {
197
225
  let entries;
@@ -230,28 +258,98 @@ export class TriggerDefinitionManager {
230
258
  const record = triggers[triggerId];
231
259
  return isRecord(record) ? record : undefined;
232
260
  }
233
- writeFiles(dir, files) {
234
- for (const file of files) {
235
- const rel = safeRelativePath(file.relativePath);
236
- const target = path.resolve(dir, rel);
237
- const relFromRoot = path.relative(path.resolve(dir), target);
238
- if (relFromRoot.startsWith('..') || path.isAbsolute(relFromRoot)) {
239
- throw new Error(`file escapes trigger directory: ${file.relativePath}`);
261
+ writeFiles(dir, files, definition, afterWrite) {
262
+ if (!Array.isArray(files) || files.length > 1)
263
+ throw new Error('a Trigger can upload only its configured script file');
264
+ if (files.length === 0) {
265
+ afterWrite();
266
+ return;
267
+ }
268
+ if (definition.execution.type !== 'script' || !definition.execution.script) {
269
+ throw new Error('uploaded Trigger files require execution.type=script');
270
+ }
271
+ assertRealDirectory(dir, 'Trigger directory');
272
+ const expectedRelative = safeScriptRelativePath(definition.execution.script.path);
273
+ const file = files[0];
274
+ if (!file || typeof file !== 'object')
275
+ throw new Error('invalid Trigger script upload');
276
+ const relative = safeScriptRelativePath(file.relativePath);
277
+ const normalize = (value) => process.platform === 'win32' ? value.toLowerCase() : value;
278
+ if (normalize(relative) !== normalize(expectedRelative)) {
279
+ throw new Error('uploaded file must match execution.script.path');
280
+ }
281
+ const contents = decodeTriggerScriptFile(file.contentBase64);
282
+ const target = path.resolve(dir, relative);
283
+ assertPathWithinDirectory(dir, target);
284
+ const parent = ensureSafeDirectoryPath(dir, path.dirname(target));
285
+ const existing = lstatOptional(target);
286
+ if (existing && (!existing.isFile() || existing.isSymbolicLink() || existing.nlink !== 1)) {
287
+ throw new Error('Trigger script target must be a regular, unlinked file');
288
+ }
289
+ const temporary = path.join(parent, `.ec-trigger-upload-${crypto.randomBytes(12).toString('hex')}.tmp`);
290
+ const backup = path.join(parent, `.ec-trigger-rollback-${crypto.randomBytes(12).toString('hex')}.tmp`);
291
+ let backedUp = false;
292
+ let installed = false;
293
+ let installedFile;
294
+ let committed = false;
295
+ try {
296
+ writeNewFile(temporary, contents);
297
+ assertSafeDirectoryPath(dir, parent);
298
+ assertUnchangedTarget(target, existing);
299
+ if (existing) {
300
+ fs.copyFileSync(target, backup, fs.constants.COPYFILE_EXCL);
301
+ backedUp = true;
302
+ assertUnchangedTarget(target, existing);
303
+ }
304
+ fs.renameSync(temporary, target);
305
+ installed = true;
306
+ installedFile = fs.lstatSync(target);
307
+ afterWrite();
308
+ committed = true;
309
+ }
310
+ catch (error) {
311
+ if (installed) {
312
+ try {
313
+ assertSafeDirectoryPath(dir, parent);
314
+ assertUnchangedTarget(target, installedFile);
315
+ fs.unlinkSync(target);
316
+ if (backedUp) {
317
+ fs.renameSync(backup, target);
318
+ backedUp = false;
319
+ }
320
+ }
321
+ catch (rollbackError) {
322
+ throw new AggregateError([error, rollbackError], 'Trigger script update failed and the previous file could not be restored');
323
+ }
324
+ }
325
+ throw error;
326
+ }
327
+ finally {
328
+ try {
329
+ fs.unlinkSync(temporary);
330
+ }
331
+ catch { }
332
+ if (backedUp && (committed || !installed)) {
333
+ try {
334
+ fs.unlinkSync(backup);
335
+ }
336
+ catch { }
240
337
  }
241
- fs.mkdirSync(path.dirname(target), { recursive: true });
242
- fs.writeFileSync(target, Buffer.from(file.contentBase64, 'base64'));
243
338
  }
244
339
  }
245
340
  validateScriptFile(definition, dir) {
246
341
  if (definition.execution.type !== 'script')
247
342
  return;
248
- const scriptAbs = resolveScriptPath(dir, definition.execution.script.path);
343
+ const relative = safeScriptRelativePath(definition.execution.script.path);
344
+ const scriptAbs = resolveScriptPath(dir, relative);
249
345
  if (!fs.existsSync(scriptAbs)) {
250
346
  throw new Error(`script file not found: ${definition.execution.script.path}`);
251
347
  }
252
- const stat = fs.statSync(scriptAbs);
253
- if (!stat.isFile())
254
- throw new Error(`script.path is not a file: ${definition.execution.script.path}`);
348
+ const stat = fs.lstatSync(scriptAbs);
349
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
350
+ throw new Error(`script.path must be a regular unlinked file: ${definition.execution.script.path}`);
351
+ }
352
+ assertSafeDirectoryPath(dir, path.dirname(scriptAbs));
255
353
  }
256
354
  assertAgent(definition) {
257
355
  if (definition.agentAid !== this.agentAid) {
@@ -269,6 +367,121 @@ export class TriggerDefinitionManager {
269
367
  }
270
368
  }
271
369
  }
370
+ const RESERVED_TRIGGER_FILES = new Set([
371
+ 'trigger.json',
372
+ 'active.json',
373
+ 'history.jsonl',
374
+ 'schedule.json',
375
+ 'limits.json',
376
+ ]);
377
+ function safeTriggerId(value) {
378
+ if (typeof value !== 'string' || !value || value.includes('/') || value.includes('\\')
379
+ || value.includes(':') || value.includes('\0') || value === '.' || value === '..') {
380
+ throw new Error(`invalid Trigger ID: ${value}`);
381
+ }
382
+ const normalized = safeRelativePath(value);
383
+ if (normalized.includes('/') || normalized.includes('\\') || normalized.includes(':')
384
+ || normalized.endsWith('.') || normalized.endsWith(' ')) {
385
+ throw new Error(`invalid Trigger ID: ${value}`);
386
+ }
387
+ if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(normalized)) {
388
+ throw new Error(`invalid Trigger ID: ${value}`);
389
+ }
390
+ return normalized;
391
+ }
392
+ function assertPathWithinDirectory(root, candidate) {
393
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
394
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
395
+ throw new Error(`file escapes Trigger directory: ${candidate}`);
396
+ }
397
+ }
398
+ function lstatOptional(value) {
399
+ try {
400
+ return fs.lstatSync(value);
401
+ }
402
+ catch (error) {
403
+ if (error?.code === 'ENOENT')
404
+ return undefined;
405
+ throw error;
406
+ }
407
+ }
408
+ function writeNewFile(target, contents) {
409
+ const descriptor = fs.openSync(target, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
410
+ try {
411
+ fs.writeFileSync(descriptor, contents);
412
+ fs.fsyncSync(descriptor);
413
+ }
414
+ finally {
415
+ fs.closeSync(descriptor);
416
+ }
417
+ }
418
+ function assertUnchangedTarget(target, expected) {
419
+ const current = lstatOptional(target);
420
+ if (!expected && current)
421
+ throw new Error('Trigger file target appeared during update');
422
+ if (expected && (!current || !current.isFile() || current.isSymbolicLink() || current.nlink !== 1
423
+ || current.dev !== expected.dev || current.ino !== expected.ino || current.size !== expected.size
424
+ || current.mtimeMs !== expected.mtimeMs || current.ctimeMs !== expected.ctimeMs)) {
425
+ throw new Error('Trigger file target changed during update');
426
+ }
427
+ }
428
+ function assertRealDirectory(value, label) {
429
+ const stat = fs.lstatSync(value);
430
+ const canonical = fs.realpathSync.native(value);
431
+ const lexical = path.resolve(value);
432
+ const equal = process.platform === 'win32'
433
+ ? canonical.toLowerCase() === lexical.toLowerCase()
434
+ : canonical === lexical;
435
+ if (!stat.isDirectory() || stat.isSymbolicLink() || !equal) {
436
+ throw new Error(`${label} must not be a symbolic link or junction`);
437
+ }
438
+ }
439
+ function ensureSafeDirectoryPath(root, target) {
440
+ const absoluteRoot = path.resolve(root);
441
+ const absoluteTarget = path.resolve(target);
442
+ if (absoluteTarget !== absoluteRoot)
443
+ assertPathWithinDirectory(absoluteRoot, absoluteTarget);
444
+ const relative = path.relative(absoluteRoot, absoluteTarget);
445
+ let current = absoluteRoot;
446
+ assertRealDirectory(current, 'Trigger directory');
447
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
448
+ current = path.join(current, segment);
449
+ try {
450
+ fs.mkdirSync(current);
451
+ }
452
+ catch (error) {
453
+ if (error?.code !== 'EEXIST')
454
+ throw error;
455
+ }
456
+ assertRealDirectory(current, 'Trigger script parent');
457
+ }
458
+ return current;
459
+ }
460
+ function assertSafeDirectoryPath(root, target) {
461
+ const absoluteRoot = path.resolve(root);
462
+ const absoluteTarget = path.resolve(target);
463
+ if (absoluteTarget !== absoluteRoot)
464
+ assertPathWithinDirectory(absoluteRoot, absoluteTarget);
465
+ let current = absoluteRoot;
466
+ assertRealDirectory(current, 'Trigger directory');
467
+ for (const segment of path.relative(absoluteRoot, absoluteTarget).split(path.sep).filter(Boolean)) {
468
+ current = path.join(current, segment);
469
+ assertRealDirectory(current, 'Trigger script parent');
470
+ }
471
+ }
472
+ function safeScriptRelativePath(value) {
473
+ if (typeof value !== 'string' || /[\0\r\n:<>"|?*]/.test(value)
474
+ || value.replace(/\\/g, '/').split('/').some(segment => segment === '..'
475
+ || (/[. ]$/.test(segment) && segment !== '.')
476
+ || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(segment))) {
477
+ throw new Error('invalid Trigger script relative path');
478
+ }
479
+ const relative = safeRelativePath(value);
480
+ if (relative.split('/').some(segment => RESERVED_TRIGGER_FILES.has(segment.toLowerCase()))) {
481
+ throw new Error('script path conflicts with reserved Trigger state');
482
+ }
483
+ return relative;
484
+ }
272
485
  function optionalString(value) {
273
486
  return typeof value === 'string' && value.length > 0 ? value : undefined;
274
487
  }
@@ -758,6 +758,27 @@ export class TriggerRuntimeScheduler {
758
758
  }
759
759
  async runRecordedExecutionAttempt(definition, runId, firedAt, sourcePayload, signal, dryRun, causation, attempt, previousAttempt) {
760
760
  const attemptId = `${runId}:attempt:${attempt}`;
761
+ const executionAuthorization = this.runtime.authorizeExecution?.(definition);
762
+ if (executionAuthorization && !executionAuthorization.allowed) {
763
+ const reply = errorReply(runId, 0, 'authorization_denied', executionAuthorization.reason || 'Trigger execution authorization denied');
764
+ const execution = {
765
+ script: null,
766
+ processing: { mode: definition.execution.type },
767
+ reply,
768
+ };
769
+ if (!dryRun) {
770
+ this.state.startExecutionAttempt?.(definition.id, runId, {
771
+ attempt,
772
+ attemptId,
773
+ startedAt: Date.now(),
774
+ });
775
+ this.state.completeExecutionAttempt?.(definition.id, runId, attemptId, {
776
+ status: 'failed',
777
+ error: { code: 'authorization_denied', message: reply.error?.text || 'Trigger execution authorization denied' },
778
+ });
779
+ }
780
+ return execution;
781
+ }
761
782
  if (!dryRun) {
762
783
  this.state.startExecutionAttempt?.(definition.id, runId, {
763
784
  attempt,
@@ -766,7 +787,7 @@ export class TriggerRuntimeScheduler {
766
787
  });
767
788
  }
768
789
  try {
769
- const execution = await this.runExecutionAttempt(definition, runId, firedAt, sourcePayload, signal, dryRun, causation, attempt, previousAttempt);
790
+ const execution = await this.runExecutionAttempt(definition, runId, firedAt, sourcePayload, signal, dryRun, causation, attempt, previousAttempt, executionAuthorization);
770
791
  if (!dryRun) {
771
792
  const summary = attemptSummaryFromExecution(attempt, attemptId, execution);
772
793
  this.state.completeExecutionAttempt?.(definition.id, runId, attemptId, summary);
@@ -783,7 +804,7 @@ export class TriggerRuntimeScheduler {
783
804
  throw error;
784
805
  }
785
806
  }
786
- async runExecutionAttempt(definition, runId, firedAt, sourcePayload, signal, dryRun, causation, attempt = 1, previousAttempt) {
807
+ async runExecutionAttempt(definition, runId, firedAt, sourcePayload, signal, dryRun, causation, attempt = 1, previousAttempt, executionAuthorization) {
787
808
  if (signal.aborted) {
788
809
  return {
789
810
  script: null,
@@ -792,7 +813,7 @@ export class TriggerRuntimeScheduler {
792
813
  };
793
814
  }
794
815
  if (definition.execution.type === 'script') {
795
- const script = await this.runScriptOnce(definition, runId, firedAt, sourcePayload, signal);
816
+ const script = await this.runScriptOnce(definition, runId, firedAt, sourcePayload, signal, executionAuthorization);
796
817
  const processing = {
797
818
  mode: 'script',
798
819
  scriptPath: definition.execution.script?.path,
@@ -961,13 +982,15 @@ export class TriggerRuntimeScheduler {
961
982
  };
962
983
  }
963
984
  }
964
- async runScriptOnce(definition, runId, firedAt, sourcePayload, signal) {
985
+ async runScriptOnce(definition, runId, firedAt, sourcePayload, signal, executionAuthorization) {
965
986
  return await this.scriptExecutor.execute({
966
987
  trigger: definition,
967
988
  triggerDir: this.manager.triggerDir(definition.id),
968
989
  runId,
969
990
  firedAt,
970
991
  sourcePayload,
992
+ projectPath: this.runtime.projectPath,
993
+ executionAuthorization,
971
994
  signal,
972
995
  });
973
996
  }
@@ -1906,7 +1929,9 @@ function branchFromReply(reply) {
1906
1929
  return 'onFailure';
1907
1930
  }
1908
1931
  function shouldRetryExecution(reply) {
1909
- return reply.outcome === 'error' || reply.outcome === 'timeout' || reply.outcome === 'interrupted';
1932
+ return reply.outcome === 'error'
1933
+ ? reply.error?.reason !== 'authorization_denied'
1934
+ : reply.outcome === 'timeout' || reply.outcome === 'interrupted';
1910
1935
  }
1911
1936
  function triggerCancellationReason(signal) {
1912
1937
  const reason = signal.reason instanceof Error