evolcore 0.0.17 → 0.0.18

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 (62) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +113 -24
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/p2p.js +20 -8
  11. package/dist/channels/aun.js +159 -21
  12. package/dist/cli/agent-command.js +67 -6
  13. package/dist/cli/agent.js +26 -0
  14. package/dist/cli/command-log.js +23 -4
  15. package/dist/cli/daemon-commands.js +53 -12
  16. package/dist/cli/init.js +21 -5
  17. package/dist/cli/restart-monitor.js +13 -6
  18. package/dist/cli/watch-logs.js +2 -2
  19. package/dist/config/builtin-roles.js +5 -1
  20. package/dist/config/role-ranks.js +4 -0
  21. package/dist/core/audit/event-key.js +29 -0
  22. package/dist/core/audit/log-integrity.js +13 -3
  23. package/dist/core/auth/auth-gateway.js +14 -18
  24. package/dist/core/auth/authorization-audit.js +110 -3
  25. package/dist/core/auth/authorization-denial.js +17 -0
  26. package/dist/core/auth/operation-authorizer.js +143 -18
  27. package/dist/core/auth/operation-catalog.js +21 -5
  28. package/dist/core/bootstrap-messages.js +11 -6
  29. package/dist/core/bootstrap-service.js +26 -4
  30. package/dist/core/causation/aun-association.js +7 -4
  31. package/dist/core/command/agent-control.js +25 -16
  32. package/dist/core/command/command-handler.js +50 -4
  33. package/dist/core/command/group-menu.js +1 -1
  34. package/dist/core/command/menu-catalog.js +32 -7
  35. package/dist/core/command/menu-handler.js +59 -23
  36. package/dist/core/command/menu-protocol.js +196 -0
  37. package/dist/core/command/slash-gate.js +14 -5
  38. package/dist/core/command/slash-handler.js +81 -99
  39. package/dist/core/event-catalog.js +18 -0
  40. package/dist/core/message/message-bridge.js +72 -9
  41. package/dist/core/message/pause-controller.js +53 -0
  42. package/dist/core/message/response-engine.js +97 -11
  43. package/dist/core/permission/sandbox-runtime.js +79 -13
  44. package/dist/core/permission/tool-policy.js +1 -1
  45. package/dist/index.js +357 -48
  46. package/dist/ipc.js +75 -4
  47. package/dist/utils/atomic-write.js +45 -11
  48. package/dist/utils/logger.js +27 -0
  49. package/dist/utils/windows-autostart.js +740 -83
  50. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  51. package/ecagent/dist/harness/agent-harness.js +6 -4
  52. package/kits/docs/evolcore/config.md +1 -1
  53. package/kits/docs/evolcore/group-rules.md +2 -1
  54. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  55. package/kits/eck_manifest.json +25 -16
  56. package/kits/rules/01-overview.md +5 -5
  57. package/kits/rules/03-identity.md +1 -1
  58. package/kits/rules/04-relation.md +4 -4
  59. package/kits/rules/05-venue.md +5 -5
  60. package/kits/templates/bootstrap-welcome.md +3 -1
  61. package/kits/templates/system-fragments/bootstrap.md +17 -9
  62. package/package.json +1 -1
@@ -129,44 +129,99 @@ const GEMINI_L_CLASS_PATTERNS = [
129
129
  const GEMINI_L_CLASS_WRITE_TOOLS = [
130
130
  'write_file', 'replace', 'delete_file', 'move_file', 'rename_file',
131
131
  ];
132
+ const GEMINI_BOOTSTRAP_FILE_TOOLS = ['write_file', 'replace'];
133
+ const GEMINI_BOOTSTRAP_LIFECYCLE_TOOLS = ['update_topic', 'complete_task'];
132
134
  const GEMINI_EC_COMMAND_PATTERNS = [
133
- String.raw `"command":"[ ]*ec[ ]*"`,
135
+ String.raw `\x00"command":"[ ]*ec[ ]*"\x00`,
134
136
  // This is only a shell-process boundary. It deliberately does not inspect
135
137
  // subcommands or arguments. Shell composition remains excluded because the
136
138
  // Gemini policy exception applies to the whole run_shell_command call.
137
- String.raw `"command":"[ ]*ec[ ]+(?:[^"\\;&|$()<>\x60\r\n]|\\")*"`,
139
+ // stableStringify JSON-escapes control characters. Reject those escapes
140
+ // explicitly, while retaining valid `\\`/`\"` escapes used by paths and
141
+ // quoted arguments.
142
+ String.raw `\x00"command":"[ ]*ec[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))*"\x00`,
138
143
  ];
139
- export function resolveGeminiPermissionProfile(value) {
144
+ export function resolveGeminiPermissionProfile(value, bootstrap = false) {
140
145
  const normalized = normalizePermissionMode(value);
141
- if (normalized.mode === 'auto')
142
- return { mode: 'auto', approvalMode: 'auto_edit' };
143
- if (normalized.mode === 'request' || normalized.mode === 'bypass') {
144
- return { mode: 'readonly', approvalMode: 'plan', degradedFrom: normalized.mode };
145
- }
146
- return { mode: 'readonly', approvalMode: 'plan' };
146
+ const profile = normalized.mode === 'auto'
147
+ ? { mode: 'auto', approvalMode: 'auto_edit' }
148
+ : normalized.mode === 'request' || normalized.mode === 'bypass'
149
+ ? { mode: 'readonly', approvalMode: 'plan', degradedFrom: normalized.mode }
150
+ : { mode: 'readonly', approvalMode: 'plan' };
151
+ // Bootstrap has its own deny-by-default Admin policy. auto_edit is required
152
+ // so Gemini exposes write_file/replace, while that policy still limits them
153
+ // to the single declared agent.md path.
154
+ return bootstrap ? { ...profile, approvalMode: 'auto_edit' } : profile;
155
+ }
156
+ function escapeGeminiArgsRegex(value) {
157
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
158
+ }
159
+ function normalizeGeminiBootstrapAgentMdPath(candidate) {
160
+ const filePath = path.resolve(candidate);
161
+ const aidDir = path.dirname(filePath);
162
+ const aid = path.basename(aidDir);
163
+ const aidsDirName = path.basename(path.dirname(aidDir));
164
+ if (path.basename(filePath).toLowerCase() !== 'agent.md'
165
+ || aidsDirName.toLowerCase() !== 'aids'
166
+ || !/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?){2,}$/.test(aid)) {
167
+ throw new Error(`Invalid Gemini bootstrap agent.md path: ${candidate}`);
168
+ }
169
+ return { filePath, aid };
170
+ }
171
+ function geminiJsonFieldEqualsPattern(fields, value) {
172
+ const encodedValue = JSON.stringify(value);
173
+ return `\\x00"(?:${fields.join('|')})":${escapeGeminiArgsRegex(encodedValue)}\\x00`;
174
+ }
175
+ function geminiExactShellCommandPattern(command) {
176
+ // Gemini stableStringify wraps the top-level NUL-delimited field in `{}`.
177
+ // Matching the whole object also rejects extra shell options such as
178
+ // is_background or additional_permissions during bootstrap.
179
+ return `^\\{\\x00"command":"[ ]*${escapeGeminiArgsRegex(command)}[ ]*"\\x00\\}$`;
147
180
  }
148
181
  function appendGeminiPolicyRule(lines, toolName, decision, priority, argsPattern) {
149
182
  lines.push('[[rule]]');
150
183
  lines.push(`toolName = ${Array.isArray(toolName) ? JSON.stringify(toolName) : JSON.stringify(toolName)}`);
151
184
  if (argsPattern)
152
- lines.push(`argsPattern = '${argsPattern}'`);
185
+ lines.push(`argsPattern = ${JSON.stringify(argsPattern)}`);
153
186
  lines.push(`decision = "${decision}"`);
154
187
  lines.push(`priority = ${priority}`, '');
155
188
  }
156
- export function buildGeminiAdminPolicy(profile) {
189
+ export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath) {
157
190
  const lines = [];
191
+ if (bootstrapAgentMdPath) {
192
+ const bootstrap = normalizeGeminiBootstrapAgentMdPath(bootstrapAgentMdPath);
193
+ // Preserve the global protected-path boundary even if a malformed internal
194
+ // caller ever supplies a protected file as the bootstrap target.
195
+ for (const pattern of GEMINI_H_CLASS_PATTERNS) {
196
+ appendGeminiPolicyRule(lines, '*', 'deny', 998, pattern);
197
+ }
198
+ for (const pattern of GEMINI_L_CLASS_PATTERNS) {
199
+ appendGeminiPolicyRule(lines, GEMINI_L_CLASS_WRITE_TOOLS, 'deny', 997, pattern);
200
+ }
201
+ const readPathPattern = geminiJsonFieldEqualsPattern(['file_path'], bootstrap.filePath);
202
+ const writePathPattern = geminiJsonFieldEqualsPattern(['file_path'], bootstrap.filePath);
203
+ appendGeminiPolicyRule(lines, 'read_file', 'allow', 990, readPathPattern);
204
+ appendGeminiPolicyRule(lines, GEMINI_BOOTSTRAP_FILE_TOOLS, 'allow', 990, writePathPattern);
205
+ appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec aid agentmd put ${bootstrap.aid}`));
206
+ appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec agent ready ${bootstrap.aid}`));
207
+ // These are Gemini's non-I/O task bookkeeping tools. They do not expose
208
+ // project files, shell, network, MCP, or user-interaction capabilities.
209
+ appendGeminiPolicyRule(lines, GEMINI_BOOTSTRAP_LIFECYCLE_TOOLS, 'allow', 950);
210
+ appendGeminiPolicyRule(lines, '*', 'deny', 900);
211
+ return lines.join('\n');
212
+ }
158
213
  // EC/daemon owns all command semantics and authorization. The Runner only
159
214
  // admits a single EC process through the otherwise mode-specific shell
160
215
  // policy. Keep this above path denies so `--file` is not reinterpreted here.
161
216
  for (const pattern of GEMINI_EC_COMMAND_PATTERNS) {
162
- appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 1000, pattern);
217
+ appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 999, pattern);
163
218
  }
164
219
  // H-class protection is mode-independent and applies to built-in and MCP tools.
165
220
  for (const pattern of GEMINI_H_CLASS_PATTERNS) {
166
- appendGeminiPolicyRule(lines, '*', 'deny', 999, pattern);
221
+ appendGeminiPolicyRule(lines, '*', 'deny', 998, pattern);
167
222
  }
168
223
  for (const pattern of GEMINI_L_CLASS_PATTERNS) {
169
- appendGeminiPolicyRule(lines, GEMINI_L_CLASS_WRITE_TOOLS, 'deny', 998, pattern);
224
+ appendGeminiPolicyRule(lines, GEMINI_L_CLASS_WRITE_TOOLS, 'deny', 997, pattern);
170
225
  }
171
226
  if (profile.mode === 'readonly') {
172
227
  appendGeminiPolicyRule(lines, GEMINI_READONLY_TOOLS, 'allow', 950);
@@ -277,7 +332,10 @@ export class GeminiRunner {
277
332
  let geminiSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
278
333
  // per-call 权限模式/模型:优先 override,缺省回落实例级(多会话并发互不污染)
279
334
  const requestedPermissionMode = modelOverride?.permissionMode || this.currentMode;
280
- const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode);
335
+ const bootstrapAgentMdPath = modelOverride?.bootstrapAgentMdPath
336
+ ? normalizeGeminiBootstrapAgentMdPath(modelOverride.bootstrapAgentMdPath).filePath
337
+ : undefined;
338
+ const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode, !!bootstrapAgentMdPath);
281
339
  const callModel = modelOverride?.model || this.model;
282
340
  const runtimeDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR;
283
341
  const managedTmpDir = runtimeDir ?? process.env.TMPDIR;
@@ -288,7 +346,12 @@ export class GeminiRunner {
288
346
  fs.mkdirSync(geminiHome, { recursive: true, mode: 0o700 });
289
347
  const sandboxProbe = buildBubblewrapCommand(this.resolved.cliPath, [], {
290
348
  projectPath,
291
- writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
349
+ writablePaths: [
350
+ geminiHome,
351
+ ...(runtimeDir ? [runtimeDir] : []),
352
+ ...(runtimeLockDir ? [runtimeLockDir] : []),
353
+ ...(bootstrapAgentMdPath ? [bootstrapAgentMdPath] : []),
354
+ ],
292
355
  });
293
356
  if (!sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
294
357
  throw new Error('Gemini 缺少可用的路径级隔离运行时,且项目覆盖 EvolCore 受保护根;已拒绝启动本轮任务');
@@ -309,7 +372,7 @@ export class GeminiRunner {
309
372
  throw new Error('Gemini 系统级 admin policy 会忽略 EvolCore 的临时安全策略,已拒绝启动本轮任务');
310
373
  }
311
374
  const policyPath = path.join(managedTmpDir, `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
312
- fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile), { mode: 0o600, flag: 'wx' });
375
+ fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile, bootstrapAgentMdPath), { mode: 0o600, flag: 'wx' });
313
376
  tempFiles.push(policyPath);
314
377
  args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
315
378
  }
@@ -331,9 +394,12 @@ export class GeminiRunner {
331
394
  args.push('--output-format', 'stream-json');
332
395
  args.push('-m', callModel);
333
396
  // Permission mode
334
- if (permissionProfile.degradedFrom) {
397
+ if (permissionProfile.degradedFrom && !bootstrapAgentMdPath) {
335
398
  logger.warn(`[GeminiRunner] permission mode ${permissionProfile.degradedFrom} is unavailable in headless mode; degraded to readonly`);
336
399
  }
400
+ else if (bootstrapAgentMdPath) {
401
+ logger.info('[GeminiRunner] using bootstrap-only tool policy for the declared agent.md');
402
+ }
337
403
  // Resume session
338
404
  if (geminiSessionId) {
339
405
  args.push('-r', geminiSessionId);
@@ -342,7 +408,12 @@ export class GeminiRunner {
342
408
  const env = this.buildAgentEnv(sessionId, runtimeEnv);
343
409
  const sandboxedCommand = buildBubblewrapCommand(this.resolved.cliPath, args, {
344
410
  projectPath,
345
- writablePaths: [geminiHome, ...(runtimeDir ? [runtimeDir] : []), ...(runtimeLockDir ? [runtimeLockDir] : [])],
411
+ writablePaths: [
412
+ geminiHome,
413
+ ...(runtimeDir ? [runtimeDir] : []),
414
+ ...(runtimeLockDir ? [runtimeLockDir] : []),
415
+ ...(bootstrapAgentMdPath ? [bootstrapAgentMdPath] : []),
416
+ ],
346
417
  readonlyPaths: tempFiles,
347
418
  });
348
419
  const child = spawn(sandboxedCommand?.command ?? this.resolved.cliPath, sandboxedCommand?.args ?? args, {
@@ -1,6 +1,12 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { AUN_KEYSTORE_ENCRYPTION_SEED } from './encryption-seed-policy.js';
4
+ // fastaun uses maxPages=1 for the background tail -> forward handoff. That is
5
+ // useful as a latency guard, but it can leave a group backlog when no later
6
+ // push arrives to trigger the next page. Bound the daemon recovery pull to ten
7
+ // pages so a large backlog is drained without monopolizing the sync loop.
8
+ const FULL_GROUP_PULL_MAX_PAGES = 10;
9
+ const GROUP_PULL_PAGINATION_PATCH = Symbol('evolcore.groupPullPaginationPatch');
4
10
  /**
5
11
  * Slot 命名约定(基于 fastaun 0.4.3 隔离键语义)。
6
12
  *
@@ -68,6 +74,36 @@ export async function loadClient(store, aid) {
68
74
  const { AUNClient } = await import('@agentunion/fastaun');
69
75
  return new AUNClient(loadAid(store, aid));
70
76
  }
77
+ /**
78
+ * Enable bounded multi-page group pulls for the long-lived daemon client.
79
+ *
80
+ * The SDK's background realtime recovery path intentionally passes
81
+ * `maxPages=1`; for a daemon this can strand a backlog after a tail sync.
82
+ * This adapter only expands that sentinel value and leaves all other pull
83
+ * options unchanged. It is isolated to the client that explicitly opts in.
84
+ */
85
+ export function enableFullGroupPullPagination(client) {
86
+ const target = client;
87
+ if (target[GROUP_PULL_PAGINATION_PATCH])
88
+ return false;
89
+ const pull = target._pullGroupV2;
90
+ if (typeof pull !== 'function')
91
+ return false;
92
+ const wrappedPull = function (groupId, afterSeq, limit, opts) {
93
+ const expandedOpts = opts?.maxPages === 1
94
+ ? { ...opts, maxPages: FULL_GROUP_PULL_MAX_PAGES }
95
+ : opts;
96
+ return pull.call(this, groupId, afterSeq, limit, expandedOpts);
97
+ };
98
+ Object.defineProperty(target, GROUP_PULL_PAGINATION_PATCH, {
99
+ configurable: false,
100
+ enumerable: false,
101
+ value: true,
102
+ writable: false,
103
+ });
104
+ target._pullGroupV2 = wrappedPull;
105
+ return true;
106
+ }
71
107
  /** 加载本地 AID 值对象(用于离线签名/验签,无需连接)。load 失败抛 AidLoadError。 */
72
108
  export function loadAid(store, aid) {
73
109
  const r = store.load(aid);
@@ -14,22 +14,34 @@ import { normalizeAunMentionEntries } from './mention-schema.js';
14
14
  const DAEMON_SEND_TIMEOUT_GRACE_MS = 10_000;
15
15
  const DEFAULT_IDLE_TIMEOUT_MS = 120_000;
16
16
  const MAX_TIMER_MS = 2_147_483_647;
17
+ /**
18
+ * Read process settings through the config facade. A delegated CLI can run
19
+ * inside a sandbox where daemon.json is intentionally unreadable; sending a
20
+ * message must still use the protocol defaults instead of surfacing EACCES.
21
+ */
22
+ function readDaemonConfigForSend() {
23
+ try {
24
+ return loadDaemonConfig();
25
+ }
26
+ catch {
27
+ return {};
28
+ }
29
+ }
17
30
  export function daemonTaskSendIpcTimeoutMs(file = false) {
18
31
  let idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS;
19
- try {
20
- const config = JSON.parse(fs.readFileSync(resolvePaths().daemonConfig, 'utf8'));
21
- const seconds = config.idleMonitor?.timeout;
22
- if (typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0) {
23
- idleTimeoutMs = seconds * 1000;
24
- }
32
+ // Use the ConfigStore read path so managed tasks never open daemon.json
33
+ // directly. The daemon-owned config reader handles missing, invalid, and
34
+ // protected files without leaking a filesystem access error into IMRenderer.
35
+ const seconds = readDaemonConfigForSend().idleMonitor?.timeout;
36
+ if (typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0) {
37
+ idleTimeoutMs = seconds * 1000;
25
38
  }
26
- catch { }
27
39
  return Math.min(MAX_TIMER_MS, Math.max(file ? 120_000 : 5_000, idleTimeoutMs + DAEMON_SEND_TIMEOUT_GRACE_MS));
28
40
  }
29
41
  export function resolveAunMessageEncrypt(explicit) {
30
42
  if (typeof explicit === 'boolean')
31
43
  return explicit;
32
- return loadDaemonConfig().aun?.defaultEncrypt ?? true;
44
+ return readDaemonConfigForSend().aun?.defaultEncrypt ?? true;
33
45
  }
34
46
  function buildSimplePayload(body) {
35
47
  switch (body.mode) {