evolcore 0.0.12 → 0.0.14

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 (116) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/ec-safe-output.js +26 -7
  4. package/bin/install-codex-managed-hooks.mjs +201 -0
  5. package/dist/agents/claude-runner.js +105 -11
  6. package/dist/agents/codex-app-server-client.js +176 -7
  7. package/dist/agents/codex-runner.js +248 -84
  8. package/dist/agents/ecagent-runner.js +21 -2
  9. package/dist/agents/gemini-runner.js +9 -4
  10. package/dist/aun/aid/control-aid.js +36 -13
  11. package/dist/aun/aid/store.js +13 -7
  12. package/dist/aun/group-fs-download.js +56 -0
  13. package/dist/aun/group-identity.js +1 -2
  14. package/dist/aun/msg/group.js +11 -33
  15. package/dist/aun/msg/index.js +1 -2
  16. package/dist/aun/msg/managed-operation.js +176 -55
  17. package/dist/aun/msg/upload.js +16 -1
  18. package/dist/aun/outbox.js +31 -2
  19. package/dist/aun/rpc/client.js +66 -0
  20. package/dist/aun/rpc/index.js +1 -2
  21. package/dist/aun/storage/{upload.js → client.js} +11 -2
  22. package/dist/aun/storage/index.js +1 -2
  23. package/dist/channels/aun.js +514 -150
  24. package/dist/channels/daemon.js +2 -0
  25. package/dist/cli/agent.js +9 -96
  26. package/dist/cli/aun-commands.js +1 -1
  27. package/dist/cli/cli-argv.js +10 -0
  28. package/dist/cli/config.js +9 -17
  29. package/dist/cli/ctl-command.js +1 -1
  30. package/dist/cli/daemon-commands.js +39 -24
  31. package/dist/cli/fs-command.js +65 -19
  32. package/dist/cli/init.js +77 -8
  33. package/dist/cli/queue-command.js +42 -0
  34. package/dist/cli/restart-monitor.js +3 -14
  35. package/dist/cli/task-context.js +172 -0
  36. package/dist/cli/trigger-command.js +7 -0
  37. package/dist/config/access-policy.js +1 -2
  38. package/dist/config/builtin-roles.js +7 -0
  39. package/dist/config/config-manager.js +27 -3
  40. package/dist/config/contact-book-store.js +7 -2
  41. package/dist/config/peer-role-resolver.js +6 -2
  42. package/dist/config/role-migration-startup.js +53 -0
  43. package/dist/config/role-store.js +1 -1
  44. package/dist/core/auth/agent-delegation.js +1 -1
  45. package/dist/core/auth/auth-gateway.js +30 -1
  46. package/dist/core/auth/authorization-audit.js +47 -0
  47. package/dist/core/auth/operation-authorizer.js +1 -0
  48. package/dist/core/auth/operation-catalog.js +10 -1
  49. package/dist/core/bootstrap-messages.js +9 -2
  50. package/dist/core/bootstrap-service.js +8 -1
  51. package/dist/core/command/command-handler.js +201 -51
  52. package/dist/core/command/menu-catalog.js +4 -0
  53. package/dist/core/command/menu-handler.js +85 -18
  54. package/dist/core/command/menu-protocol.js +23 -2
  55. package/dist/core/command/slash-handler.js +58 -10
  56. package/dist/core/event-catalog.js +8 -0
  57. package/dist/core/handoff/runtime.js +12 -1
  58. package/dist/core/handoff/store.js +5 -1
  59. package/dist/core/interaction-router.js +10 -0
  60. package/dist/core/message/im-renderer.js +34 -1
  61. package/dist/core/message/message-bridge.js +68 -13
  62. package/dist/core/message/message-log.js +2 -0
  63. package/dist/core/message/message-queue.js +267 -39
  64. package/dist/core/message/message-utils.js +81 -4
  65. package/dist/core/message/response-engine.js +242 -48
  66. package/dist/core/message/stream-debouncer.js +10 -2
  67. package/dist/core/permission/approval-gateway.js +1 -0
  68. package/dist/core/permission/ec-command-parser.js +649 -18
  69. package/dist/core/permission/index.js +1 -1
  70. package/dist/core/permission/mode.js +15 -0
  71. package/dist/core/permission/readonly-shell-query.js +148 -8
  72. package/dist/core/permission/sandbox-runtime.js +21 -0
  73. package/dist/core/permission/tool-policy.js +266 -56
  74. package/dist/core/protected-paths.js +107 -2
  75. package/dist/core/runtime-lock.js +101 -0
  76. package/dist/core/session/session-manager.js +21 -2
  77. package/dist/core/session/session-mapper.js +20 -1
  78. package/dist/eck/detect.js +18 -1
  79. package/dist/eck/group-rules-sync.js +15 -44
  80. package/dist/index.js +191 -89
  81. package/dist/ipc.js +16 -3
  82. package/dist/paths.js +3 -0
  83. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  84. package/dist/response-system/modes/single-session/index.js +3 -0
  85. package/dist/trigger/feedback.js +64 -11
  86. package/dist/trigger/history.js +42 -7
  87. package/dist/trigger/manager.js +21 -1
  88. package/dist/trigger/parser.js +14 -2
  89. package/dist/trigger/scheduler.js +1 -0
  90. package/dist/trigger/validation.js +67 -6
  91. package/dist/utils/ecweb-supervisor.js +332 -0
  92. package/dist/utils/error-utils.js +17 -0
  93. package/dist/utils/logger.js +37 -4
  94. package/kits/docs/evolcore/trigger.md +17 -1
  95. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  96. package/kits/schemas/role-config.schema.1.json +1 -0
  97. package/kits/templates/roles/admin.json +7 -0
  98. package/kits/templates/roles/member.json +8 -1
  99. package/kits/templates/roles/visitor.json +2 -0
  100. package/package.json +1 -1
  101. package/dist/aun/msg/payload-type.js +0 -27
  102. package/dist/aun/rpc/caller.js +0 -42
  103. package/dist/aun/rpc/connection.js +0 -25
  104. package/dist/aun/storage/manage.js +0 -10
  105. package/dist/config/access-policy-domain.js +0 -18
  106. package/dist/config/config-batch-get.js +0 -11
  107. package/dist/config/owner-policy.js +0 -4
  108. package/dist/config/role-config-v4-startup.js +0 -32
  109. package/dist/config/role-config-v5-startup.js +0 -27
  110. package/dist/core/auth/trigger-authorization.js +0 -15
  111. package/dist/core/interaction-registration.js +0 -10
  112. package/dist/core/permission/execution-sandbox.js +0 -16
  113. package/dist/core/relation/peer-key.js +0 -1
  114. package/dist/core/session/session-key.js +0 -24
  115. package/dist/eck/baseagent-caps.js +0 -18
  116. package/dist/eck/rules-loader.js +0 -28
@@ -0,0 +1,101 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ export const RUNTIME_LOCK_DIR_ENV = 'EVOLCORE_RUNTIME_LOCK_DIR';
5
+ /**
6
+ * Resolve a task-provided lock location without accepting relative or escaped
7
+ * paths. When no managed task context is present, callers retain their legacy
8
+ * on-disk lock path for daemon-side compatibility and migration tooling.
9
+ */
10
+ export function resolveManagedLockPath(namespace, identity, legacyPath) {
11
+ const configured = process.env[RUNTIME_LOCK_DIR_ENV]?.trim();
12
+ if (!configured || !path.isAbsolute(configured) || !namespace || !identity || !isTrustedRuntimeLockDir(configured)) {
13
+ return legacyPath;
14
+ }
15
+ const digest = crypto.createHash('sha256').update(identity).digest('hex').slice(0, 24);
16
+ return path.join(path.resolve(configured), namespace, `${digest}.lock`);
17
+ }
18
+ function isTrustedRuntimeLockDir(candidate) {
19
+ const resolved = path.resolve(candidate);
20
+ if (path.basename(resolved) !== 'evolcore-locks')
21
+ return false;
22
+ const tmpRoot = process.env.TMPDIR?.trim();
23
+ if (!tmpRoot || !path.isAbsolute(tmpRoot))
24
+ return false;
25
+ const resolvedTmpRoot = path.resolve(tmpRoot);
26
+ const relative = path.relative(resolvedTmpRoot, resolved);
27
+ const isWithinTmp = relative !== '..'
28
+ && !relative.startsWith(`..${path.sep}`)
29
+ && !path.isAbsolute(relative);
30
+ // The daemon supplies each task either a generic evolcore-runtime-* child
31
+ // or Codex's private digest directory. Both share a sibling evolcore-locks
32
+ // directory so mutations from separate sessions still serialize. Do not
33
+ // accept arbitrary TMPDIR siblings supplied by a child process.
34
+ const tmpBase = path.basename(resolvedTmpRoot);
35
+ const tmpParent = path.dirname(resolvedTmpRoot);
36
+ const isGenericSessionRuntime = /^evolcore-runtime-[a-f0-9]{24}$/.test(tmpBase);
37
+ const isCodexSessionRuntime = /^[a-f0-9]{24}$/.test(tmpBase)
38
+ && /^evolcore-codex-(?:\d+|user)$/.test(path.basename(tmpParent));
39
+ const isManagedSessionSibling = (isGenericSessionRuntime || isCodexSessionRuntime)
40
+ && path.dirname(resolved) === tmpParent;
41
+ if (!isWithinTmp && !isManagedSessionSibling)
42
+ return false;
43
+ try {
44
+ const tmpStat = fs.lstatSync(resolvedTmpRoot);
45
+ if (!tmpStat.isDirectory() || tmpStat.isSymbolicLink())
46
+ return false;
47
+ if (typeof process.getuid === 'function' && tmpStat.uid !== process.getuid())
48
+ return false;
49
+ if ((tmpStat.mode & 0o077) !== 0)
50
+ return false;
51
+ if (isManagedSessionSibling) {
52
+ const parentStat = fs.lstatSync(tmpParent);
53
+ if (!parentStat.isDirectory() || parentStat.isSymbolicLink())
54
+ return false;
55
+ if (typeof process.getuid === 'function' && parentStat.uid !== process.getuid())
56
+ return false;
57
+ if ((parentStat.mode & 0o077) !== 0)
58
+ return false;
59
+ }
60
+ const stat = fs.lstatSync(resolved);
61
+ if (!stat.isDirectory() || stat.isSymbolicLink())
62
+ return false;
63
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid())
64
+ return false;
65
+ return (stat.mode & 0o077) === 0;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ export function ensureManagedLockParent(lockPath) {
72
+ const configured = process.env[RUNTIME_LOCK_DIR_ENV]?.trim();
73
+ if (!configured || !isTrustedRuntimeLockDir(configured)) {
74
+ throw new Error('managed runtime lock directory is not trusted');
75
+ }
76
+ const root = path.resolve(configured);
77
+ const parent = path.resolve(path.dirname(lockPath));
78
+ const relative = path.relative(root, parent);
79
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
80
+ throw new Error('managed runtime lock parent escapes its runtime directory');
81
+ }
82
+ // Create each component individually. mkdir({recursive:true}) would follow a
83
+ // pre-existing symlink in a namespace component before it can be checked.
84
+ let current = root;
85
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
86
+ current = path.join(current, segment);
87
+ try {
88
+ fs.mkdirSync(current, { mode: 0o700 });
89
+ }
90
+ catch (error) {
91
+ if (error?.code !== 'EEXIST')
92
+ throw error;
93
+ }
94
+ const stat = fs.lstatSync(current);
95
+ if (!stat.isDirectory() || stat.isSymbolicLink()
96
+ || (typeof process.getuid === 'function' && stat.uid !== process.getuid())
97
+ || (stat.mode & 0o077) !== 0) {
98
+ throw new Error(`managed runtime lock path is not a private real directory: ${current}`);
99
+ }
100
+ }
101
+ }
@@ -2,8 +2,7 @@ import { ensureDir } from '../../utils/atomic-write.js';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  import { encodePath } from '../../utils/cross-platform.js';
4
4
  import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, readJsonFile, readLastJsonlLine, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
5
- import { sessionToFile, fileToSession } from './session-mapper.js';
6
- import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
5
+ import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID } from './session-mapper.js';
7
6
  import { tryParseChannelKey } from '../channel-loader.js';
8
7
  import { isSystemControlChannel } from '../system-channels.js';
9
8
  import path from 'path';
@@ -597,6 +596,14 @@ export class SessionManager {
597
596
  mutated = true;
598
597
  }
599
598
  }
599
+ if (chatType === 'group' && metadata?.groupId) {
600
+ if (!session.metadata)
601
+ session.metadata = {};
602
+ if (session.metadata.groupId !== metadata.groupId) {
603
+ session.metadata.groupId = metadata.groupId;
604
+ mutated = true;
605
+ }
606
+ }
600
607
  if (metadata?.peerType && session.metadata?.peerType !== metadata.peerType) {
601
608
  if (!session.metadata)
602
609
  session.metadata = {};
@@ -636,6 +643,9 @@ export class SessionManager {
636
643
  if (metadata?.peerType) {
637
644
  session.metadata.peerType = metadata.peerType;
638
645
  }
646
+ if (chatType === 'group' && metadata?.groupId) {
647
+ session.metadata.groupId = metadata.groupId;
648
+ }
639
649
  this.persistSession(session, 'sync');
640
650
  return session;
641
651
  }
@@ -702,11 +712,20 @@ export class SessionManager {
702
712
  const existing = this.readMetaLatest(metaPath);
703
713
  if (existing) {
704
714
  const validSessionId = this.validateSessionFile(existing);
715
+ let mutated = false;
705
716
  if (metadata) {
706
717
  const creatorPeerId = existing.metadata?.peerId;
707
718
  existing.metadata = { ...(existing.metadata || {}), ...metadata };
708
719
  if (creatorPeerId && existing.metadata)
709
720
  existing.metadata.peerId = creatorPeerId;
721
+ mutated = true;
722
+ }
723
+ if (chatType && existing.chatType !== chatType) {
724
+ logger.info(`[SessionManager] Updating thread chatType for session ${existing.id}: ${existing.chatType} -> ${chatType}`);
725
+ existing.chatType = chatType;
726
+ mutated = true;
727
+ }
728
+ if (mutated) {
710
729
  this.persistSession(existing, 'none');
711
730
  }
712
731
  return { ...existing, agentSessionId: validSessionId };
@@ -1,5 +1,24 @@
1
1
  import { formatTimestamp } from './session-fs-store.js';
2
- import { formatSessionKey, DEFAULT_THREAD_ID } from './session-key.js';
2
+ /** Default topic component used by the stable session routing key. */
3
+ export const DEFAULT_THREAD_ID = 'main';
4
+ export function formatSessionKey(channelType, channelId, threadId) {
5
+ const tid = threadId || DEFAULT_THREAD_ID;
6
+ return `${channelType}#${encodeURIComponent(channelId)}#${encodeURIComponent(tid)}`;
7
+ }
8
+ export function parseSessionKey(key) {
9
+ const first = key.indexOf('#');
10
+ if (first <= 0)
11
+ throw new Error(`Invalid session key: ${key}`);
12
+ const rest = key.slice(first + 1);
13
+ const second = rest.indexOf('#');
14
+ if (second <= 0)
15
+ throw new Error(`Invalid session key (missing threadId): ${key}`);
16
+ return {
17
+ channelType: key.slice(0, first),
18
+ channelId: decodeURIComponent(rest.slice(0, second)),
19
+ threadId: decodeURIComponent(rest.slice(second + 1)),
20
+ };
21
+ }
3
22
  export function sessionToFile(session) {
4
23
  const metadata = {};
5
24
  if (session.metadata) {
@@ -1,6 +1,23 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { BASEAGENT_CAPS } from './baseagent-caps.js';
3
+ export const BASEAGENT_CAPS = {
4
+ 'claude-code': {
5
+ autoLoadsRules: true,
6
+ supportsSystemPrompt: true,
7
+ },
8
+ 'claude': {
9
+ autoLoadsRules: true,
10
+ supportsSystemPrompt: true,
11
+ },
12
+ 'codex': {
13
+ autoLoadsRules: false,
14
+ supportsSystemPrompt: true,
15
+ },
16
+ 'gemini': {
17
+ autoLoadsRules: false,
18
+ supportsSystemPrompt: true,
19
+ },
20
+ };
4
21
  const MAX_DEPTH = 5;
5
22
  export function resolveEckInjection(agentConfig, projectPath, kitsRulesPath) {
6
23
  const caps = BASEAGENT_CAPS[agentConfig.baseAgent]
@@ -1,8 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { createHash } from 'crypto';
4
3
  import { TextDecoder } from 'util';
5
4
  import { getAidStore, isValidAid, loadClient, SLOT } from '../aun/aid/index.js';
5
+ import { downloadGroupFsBytes, GroupFsDownloadError } from '../aun/group-fs-download.js';
6
6
  import { checkGroupIndex as checkGroupIndexRpc, getGroupIndex as getGroupIndexRpc } from '../aun/msg/group-index.js';
7
7
  import { agentRelationsDir } from '../paths.js';
8
8
  import { formatPeerKey } from '../core/relation/peer-identity.js';
@@ -220,7 +220,9 @@ function lazyGroupClient(selfAid) {
220
220
  client: async () => {
221
221
  if (client)
222
222
  return client;
223
- store = await getAidStore({ slotId: SLOT.daemon });
223
+ // This short-lived RPC client must never share the daemon's gateway
224
+ // cursor. A short connection can perform a background pull on connect.
225
+ store = await getAidStore({ slotId: SLOT.groupRules });
224
226
  client = await loadClient(store, selfAid);
225
227
  await client.connect({ connection_kind: 'short', short_ttl_ms: 30000, auto_reconnect: false });
226
228
  return client;
@@ -242,39 +244,15 @@ function lazyGroupClient(selfAid) {
242
244
  };
243
245
  }
244
246
  async function downloadGroupFileBytes(client, remoteRef) {
245
- const ticket = await client.call('group.fs.create_download_ticket', { path: remoteRef });
246
- if (!isRecord(ticket)) {
247
- throw statusError('unreadable', 'group.fs.create_download_ticket returned invalid response');
248
- }
249
- const downloadUrl = stringField(ticket, ['download_url', 'url']);
250
- if (!downloadUrl) {
251
- throw statusError('unreadable', 'group.fs.create_download_ticket did not return download_url');
252
- }
253
- const bytes = await client.group.fs.lowlevel.httpGet(downloadUrl, bearerHeaders(client));
254
- const expectedSha = stringField(ticket, ['sha256']);
255
- if (expectedSha && sha256Hex(bytes).toLowerCase() !== expectedSha.toLowerCase()) {
256
- throw statusError('unreadable', 'download hash verification failed');
247
+ try {
248
+ return (await downloadGroupFsBytes(client, remoteRef)).bytes;
257
249
  }
258
- return bytes;
259
- }
260
- function sha256Hex(bytes) {
261
- return createHash('sha256').update(bytes).digest('hex');
262
- }
263
- function bearerHeaders(client) {
264
- const token = accessTokenFromClient(client);
265
- return token ? { Authorization: `Bearer ${token}` } : undefined;
266
- }
267
- function accessTokenFromClient(client) {
268
- const anyClient = client;
269
- if (typeof anyClient.getAccessToken === 'function') {
270
- const token = stringValue(anyClient.getAccessToken());
271
- if (token)
272
- return token;
250
+ catch (error) {
251
+ if (error instanceof GroupFsDownloadError) {
252
+ throw statusError('unreadable', error.message);
253
+ }
254
+ throw error;
273
255
  }
274
- return stringValue(anyClient.accessToken)
275
- || stringValue(anyClient.access_token)
276
- || stringValue(anyClient._identity?.access_token)
277
- || stringValue(anyClient._sessionParams?.access_token);
278
256
  }
279
257
  function assertUtf8(bytes) {
280
258
  new TextDecoder('utf-8', { fatal: true }).decode(bytes);
@@ -302,6 +280,10 @@ function classifyError(e) {
302
280
  if (e instanceof StatusError)
303
281
  return e.status;
304
282
  const msg = errorMessage(e).toLowerCase();
283
+ // A peer cache miss is a signer/identity lookup failure, not a missing
284
+ // rules file.
285
+ if (msg.includes('peer not found in cache'))
286
+ return 'unreadable';
305
287
  if (msg.includes('forbidden') || msg.includes('unauthorized') || msg.includes('permission'))
306
288
  return 'forbidden';
307
289
  if (msg.includes('not found') || msg.includes('does not exist'))
@@ -314,17 +296,6 @@ function errorMessage(e) {
314
296
  function isRecord(x) {
315
297
  return typeof x === 'object' && x !== null && !Array.isArray(x);
316
298
  }
317
- function stringValue(x) {
318
- return typeof x === 'string' ? x : '';
319
- }
320
- function stringField(obj, keys) {
321
- for (const k of keys) {
322
- const v = obj[k];
323
- if (typeof v === 'string')
324
- return v;
325
- }
326
- return undefined;
327
- }
328
299
  function numberField(obj, keys) {
329
300
  for (const k of keys) {
330
301
  const v = obj[k];