evolcore 0.0.17 → 0.0.19

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 (66) hide show
  1. package/CHANGELOG.md +51 -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 +43 -24
  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 +82 -14
  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/data-migration.js +10 -4
  40. package/dist/core/event-catalog.js +18 -0
  41. package/dist/core/message/message-bridge.js +66 -8
  42. package/dist/core/message/response-engine.js +147 -10
  43. package/dist/core/permission/ec-command-parser.js +148 -22
  44. package/dist/core/permission/sandbox-runtime.js +79 -13
  45. package/dist/core/permission/tool-policy.js +19 -7
  46. package/dist/index.js +357 -48
  47. package/dist/ipc.js +81 -5
  48. package/dist/paths.js +0 -3
  49. package/dist/utils/atomic-write.js +45 -11
  50. package/dist/utils/logger.js +27 -0
  51. package/dist/utils/windows-autostart.js +740 -83
  52. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  53. package/ecagent/dist/harness/agent-harness.js +6 -4
  54. package/kits/docs/evolcore/config.md +1 -1
  55. package/kits/docs/evolcore/group-rules.md +2 -1
  56. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  57. package/kits/docs/path-registry.md +1 -1
  58. package/kits/eck_manifest.json +25 -16
  59. package/kits/rules/01-overview.md +5 -5
  60. package/kits/rules/02-navigation.md +2 -2
  61. package/kits/rules/03-identity.md +1 -1
  62. package/kits/rules/04-relation.md +4 -4
  63. package/kits/rules/05-venue.md +5 -5
  64. package/kits/templates/bootstrap-welcome.md +3 -1
  65. package/kits/templates/system-fragments/bootstrap.md +17 -9
  66. package/package.json +1 -1
@@ -459,11 +459,30 @@ function isWithin(root, candidate) {
459
459
  const relative = path.relative(root, candidate);
460
460
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
461
461
  }
462
- function resolveToolPath(projectPath, candidate, forWrite = false) {
462
+ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath) {
463
463
  const root = fs.realpathSync(projectPath);
464
464
  const resolved = path.resolve(root, candidate || '.');
465
- if (!isWithin(root, resolved))
465
+ const allowedBootstrapFile = bootstrapAgentMdPath
466
+ ? path.resolve(bootstrapAgentMdPath)
467
+ : undefined;
468
+ const isBootstrapAgentMd = allowedBootstrapFile === resolved;
469
+ if (!isWithin(root, resolved) && !isBootstrapAgentMd) {
466
470
  throw new Error(`Path is outside the project: ${candidate}`);
471
+ }
472
+ if (isBootstrapAgentMd) {
473
+ const parent = path.dirname(resolved);
474
+ const canonicalParent = fs.realpathSync(parent);
475
+ if (!isWithin(parent, canonicalParent) || !isWithin(canonicalParent, parent)) {
476
+ throw new Error(`Bootstrap agent.md parent resolves outside its declared path: ${candidate}`);
477
+ }
478
+ if (fs.existsSync(resolved)) {
479
+ const canonicalFile = fs.realpathSync(resolved);
480
+ if (!isWithin(resolved, canonicalFile) || !isWithin(canonicalFile, resolved)) {
481
+ throw new Error(`Bootstrap agent.md resolves outside its declared path: ${candidate}`);
482
+ }
483
+ }
484
+ return resolved;
485
+ }
467
486
  const existing = forWrite ? path.dirname(resolved) : resolved;
468
487
  let cursor = existing;
469
488
  while (!fs.existsSync(cursor) && cursor !== path.dirname(cursor))
@@ -522,7 +541,7 @@ async function walkFiles(root, signal) {
522
541
  function textResult(text, details = {}) {
523
542
  return { content: [{ type: 'text', text: truncate(text) }], details };
524
543
  }
525
- function createTools(env, projectPath, runtimeEnv) {
544
+ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath) {
526
545
  const read = {
527
546
  name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
528
547
  parameters: Type.Object({
@@ -531,7 +550,7 @@ function createTools(env, projectPath, runtimeEnv) {
531
550
  limit: Type.Optional(Type.Number({ minimum: 1 })),
532
551
  }),
533
552
  async execute(_id, params, signal) {
534
- const file = resolveToolPath(projectPath, params.file_path);
553
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
535
554
  const stat = await fs.promises.stat(file);
536
555
  if (stat.size > MAX_READ_BYTES)
537
556
  throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
@@ -548,7 +567,7 @@ function createTools(env, projectPath, runtimeEnv) {
548
567
  name: 'Glob', label: 'Glob', description: 'Find project files matching a glob pattern.',
549
568
  parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }),
550
569
  async execute(_id, params, signal) {
551
- const root = resolveToolPath(projectPath, params.path ?? '.');
570
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
552
571
  const matcher = globRegex(params.pattern.replaceAll('\\', '/'));
553
572
  const files = (await walkFiles(root, signal))
554
573
  .map(file => path.relative(root, file).replaceAll(path.sep, '/'))
@@ -564,7 +583,7 @@ function createTools(env, projectPath, runtimeEnv) {
564
583
  case_insensitive: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()),
565
584
  }),
566
585
  async execute(_id, params, signal) {
567
- const root = resolveToolPath(projectPath, params.path ?? '.');
586
+ const root = resolveEcagentToolPath(projectPath, params.path ?? '.');
568
587
  const files = (await fs.promises.stat(root)).isFile() ? [root] : await walkFiles(root, signal);
569
588
  const fileMatcher = params.glob ? globRegex(params.glob.replaceAll('\\', '/')) : undefined;
570
589
  const flags = params.case_insensitive ? 'i' : '';
@@ -597,7 +616,7 @@ function createTools(env, projectPath, runtimeEnv) {
597
616
  name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
598
617
  parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
599
618
  async execute(_id, params, signal) {
600
- const file = resolveToolPath(projectPath, params.file_path, true);
619
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath);
601
620
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
602
621
  await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
603
622
  return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
@@ -610,7 +629,7 @@ function createTools(env, projectPath, runtimeEnv) {
610
629
  replace_all: Type.Optional(Type.Boolean()),
611
630
  }), executionMode: 'sequential',
612
631
  async execute(_id, params, signal) {
613
- const file = resolveToolPath(projectPath, params.file_path);
632
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath);
614
633
  const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
615
634
  if (!params.old_string)
616
635
  throw new Error('old_string must not be empty');
@@ -809,7 +828,7 @@ export class EcagentRunner {
809
828
  models,
810
829
  model,
811
830
  thinkingLevel: normalizeThinkingLevel(effort),
812
- tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv),
831
+ tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath),
813
832
  systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
814
833
  streamOptions: {
815
834
  timeoutMs: this.config.timeoutMs,
@@ -823,7 +842,15 @@ export class EcagentRunner {
823
842
  runtime.harness = undefined;
824
843
  throw new Error('ecagent run interrupted');
825
844
  }
826
- harness.on('tool_call', event => this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input));
845
+ harness.on('tool_call', async (event, signal) => {
846
+ const pauseController = this.permissionContexts.get(request.sessionId)?.pauseController;
847
+ if (pauseController) {
848
+ const pauseResult = await pauseController.waitAtToolBoundary(signal);
849
+ if (pauseResult === 'cancelled')
850
+ return { block: true, reason: '工具调用已取消' };
851
+ }
852
+ return this.authorizeTool(request.sessionId, request.projectPath, permissionMode, event.toolName, event.input);
853
+ });
827
854
  const unsubscribe = harness.subscribe(event => this.mapHarnessEvent(queue, event, stats, model, effort));
828
855
  queue.push({ type: 'state_changed', state: 'running' });
829
856
  queue.push({ type: 'status', subtype: 'started', message: `ecagent is using ${modelId}` });
@@ -1043,6 +1070,7 @@ export class EcagentRunner {
1043
1070
  return decision.matched && decision.decision === 'deny' ? { block: true, reason: '危险操作未获人工批准' } : {};
1044
1071
  }
1045
1072
  async interrupt(sessionKey) {
1073
+ this.permissionContexts.get(sessionKey)?.pauseController?.cancel();
1046
1074
  this.permissionGateway?.cancelAll(sessionKey, 'interrupted');
1047
1075
  const harness = this.runtimes.get(sessionKey)?.harness;
1048
1076
  if (harness) {
@@ -1058,6 +1086,7 @@ export class EcagentRunner {
1058
1086
  void harness.steer(text).catch(error => logger.warn('[EcagentRunner] Failed to steer active run:', error));
1059
1087
  }
1060
1088
  async closeSession(sessionId) {
1089
+ this.permissionContexts.get(sessionId)?.pauseController?.cancel();
1061
1090
  await this.interrupt(sessionId).catch(() => { });
1062
1091
  this.runtimes.delete(sessionId);
1063
1092
  this.activeStreams.delete(sessionId);
@@ -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) {