evolcore 0.0.20 → 0.0.22

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 (147) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +58 -9
  3. package/bin/codex-managed-hook.mjs +3 -0
  4. package/bin/install-codex-managed-hooks.mjs +3 -1
  5. package/dist/agents/baseagent.js +10 -6
  6. package/dist/agents/claude-runner.js +393 -108
  7. package/dist/agents/codex-app-server-client.js +41 -7
  8. package/dist/agents/codex-runner.js +1292 -220
  9. package/dist/agents/ecagent-runner.js +171 -61
  10. package/dist/agents/gemini-runner.js +130 -30
  11. package/dist/agents/request-identity.js +25 -0
  12. package/dist/agents/runner-types.js +19 -0
  13. package/dist/aun/aid/agentmd.js +59 -2
  14. package/dist/aun/aid/identity.js +4 -1
  15. package/dist/aun/aid/index.js +1 -1
  16. package/dist/aun/msg/group.js +72 -6
  17. package/dist/aun/msg/history.js +213 -36
  18. package/dist/aun/msg/managed-operation.js +58 -9
  19. package/dist/aun/msg/p2p.js +5 -0
  20. package/dist/aun/outbox.js +189 -80
  21. package/dist/aun/service-proxy.js +43 -25
  22. package/dist/channels/aun.js +618 -123
  23. package/dist/channels/daemon.js +6 -1
  24. package/dist/cli/agent-command.js +4 -3
  25. package/dist/cli/agent.js +66 -56
  26. package/dist/cli/aun-commands.js +177 -42
  27. package/dist/cli/command-log.js +10 -11
  28. package/dist/cli/contact.js +1 -0
  29. package/dist/cli/daemon-commands.js +98 -123
  30. package/dist/cli/init.js +27 -15
  31. package/dist/cli/task-context.js +50 -0
  32. package/dist/cli/trigger-command.js +14 -5
  33. package/dist/cli/watch-logs.js +10 -3
  34. package/dist/config/builtin-roles.js +1 -0
  35. package/dist/config/config-field-policy.js +19 -5
  36. package/dist/config/config-manager.js +167 -22
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/contact-operation-service.js +32 -1
  39. package/dist/config/contact-request-service.js +44 -0
  40. package/dist/config/daemon-services.js +186 -0
  41. package/dist/config/gateway-config.js +20 -9
  42. package/dist/config/role-service.js +54 -3
  43. package/dist/config/schema-migration.js +550 -0
  44. package/dist/config-store.js +151 -9
  45. package/dist/core/agent-application-service.js +279 -0
  46. package/dist/core/audit/log-integrity.js +102 -0
  47. package/dist/core/auth/agent-delegation.js +43 -1
  48. package/dist/core/auth/auth-gateway.js +41 -4
  49. package/dist/core/auth/authorization-audit.js +216 -8
  50. package/dist/core/auth/operation-authorizer.js +41 -1
  51. package/dist/core/auth/operation-catalog.js +9 -1
  52. package/dist/core/bootstrap-messages.js +8 -0
  53. package/dist/core/bootstrap-service.js +99 -27
  54. package/dist/core/causation/aun-association.js +7 -4
  55. package/dist/core/command/agent-control.js +56 -16
  56. package/dist/core/command/command-handler.js +311 -44
  57. package/dist/core/command/connect-menu.js +3 -4
  58. package/dist/core/command/group-menu.js +5 -7
  59. package/dist/core/command/menu-handler.js +288 -80
  60. package/dist/core/command/menu-protocol.js +1 -1
  61. package/dist/core/command/role-menu.js +21 -11
  62. package/dist/core/command/slash-gate.js +85 -18
  63. package/dist/core/command/slash-handler.js +377 -36
  64. package/dist/core/data-migration.js +11 -1
  65. package/dist/core/event-catalog.js +37 -0
  66. package/dist/core/evolagent.js +4 -0
  67. package/dist/core/handoff/dispatcher.js +4 -0
  68. package/dist/core/handoff/runtime.js +33 -3
  69. package/dist/core/handoff/store.js +32 -9
  70. package/dist/core/inference/text-inference.js +7 -15
  71. package/dist/core/message/im-renderer.js +90 -87
  72. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  73. package/dist/core/message/message-bridge.js +184 -12
  74. package/dist/core/message/message-log.js +47 -7
  75. package/dist/core/message/message-queue.js +227 -16
  76. package/dist/core/message/message-utils.js +12 -5
  77. package/dist/core/message/response-engine.js +658 -109
  78. package/dist/core/message/send-receipt.js +1 -0
  79. package/dist/core/message/stream-debouncer.js +9 -2
  80. package/dist/core/model/model-catalog.js +23 -15
  81. package/dist/core/model/model-diagnostics.js +28 -10
  82. package/dist/core/permission/approval-gateway.js +180 -6
  83. package/dist/core/permission/ec-command-parser.js +627 -69
  84. package/dist/core/permission/mode.js +18 -3
  85. package/dist/core/{protected-paths.js → permission/protected-paths.js} +27 -14
  86. package/dist/core/permission/readonly-shell-query.js +263 -9
  87. package/dist/core/permission/sandbox-runtime.js +159 -1
  88. package/dist/core/permission/tool-error-code.js +12 -0
  89. package/dist/core/permission/tool-policy.js +618 -23
  90. package/dist/core/session/session-fs-store.js +154 -5
  91. package/dist/core/session/session-manager.js +329 -30
  92. package/dist/core/session/session-renew.js +37 -13
  93. package/dist/core/session/session-turn-coordinator.js +16 -5
  94. package/dist/eck/kit-renderer.js +1 -1
  95. package/dist/index.js +316 -50
  96. package/dist/ipc.js +459 -29
  97. package/dist/paths.js +82 -7
  98. package/dist/response-system/context-builder.js +1 -7
  99. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  100. package/dist/stats/price-resolver.js +4 -0
  101. package/dist/trigger/anomaly-store.js +1 -0
  102. package/dist/trigger/feedback.js +70 -7
  103. package/dist/trigger/history.js +79 -4
  104. package/dist/trigger/legacy-session-history.js +2 -2
  105. package/dist/trigger/parser.js +13 -3
  106. package/dist/trigger/scheduler.js +20 -3
  107. package/dist/trigger/validation.js +6 -1
  108. package/dist/utils/atomic-write.js +27 -0
  109. package/dist/utils/ecweb-utils.js +16 -2
  110. package/dist/utils/error-utils.js +4 -1
  111. package/dist/utils/logger.js +30 -6
  112. package/dist/utils/process-tree-stats.js +24 -4
  113. package/dist/utils/process-tree-worker.js +31 -0
  114. package/dist/utils/project-path.js +1 -2
  115. package/dist/utils/tool-summary.js +59 -0
  116. package/dist/utils/windows-shell-trust.js +201 -0
  117. package/kits/docs/INDEX.md +1 -1
  118. package/kits/docs/evolcore/INDEX.md +3 -3
  119. package/kits/docs/evolcore/agent-create.md +146 -0
  120. package/kits/docs/evolcore/agent.md +6 -0
  121. package/kits/docs/evolcore/contact.md +7 -1
  122. package/kits/docs/evolcore/group-collaboration.md +251 -0
  123. package/kits/docs/evolcore/group-rules.md +1 -19
  124. package/kits/docs/evolcore/group.md +3 -1
  125. package/kits/docs/evolcore/msg.md +16 -0
  126. package/kits/docs/evolcore/trigger.md +6 -3
  127. package/kits/docs/prompt-loading-architecture.md +6 -0
  128. package/kits/eck_message_manifest.json +6 -6
  129. package/kits/schemas/_meta.json +7 -4
  130. package/kits/schemas/agent-config.schema.11.json +13 -0
  131. package/kits/schemas/agent-config.schema.12.json +427 -0
  132. package/kits/schemas/daemon.schema.5.json +0 -1
  133. package/kits/schemas/daemon.schema.6.json +131 -0
  134. package/kits/schemas/defaults.schema.5.json +15 -3
  135. package/kits/schemas/migrations/README.md +3 -1
  136. package/kits/schemas/relation-config.schema.8.json +13 -0
  137. package/kits/schemas/role-config.schema.1.json +1 -2
  138. package/kits/schemas/single-session.schema.3.json +32 -0
  139. package/kits/templates/message-fragments/item.md +1 -1
  140. package/kits/templates/roles/admin.json +1 -0
  141. package/kits/templates/roles/member.json +1 -0
  142. package/kits/templates/roles/visitor.json +1 -0
  143. package/kits/templates/system-fragments/bootstrap.md +2 -1
  144. package/kits/templates/system-fragments/commands.md +2 -2
  145. package/package.json +6 -3
  146. package/skills/eclink/SKILL.md +2 -0
  147. package/dist/config/aun-gateway-config.js +0 -2
@@ -14,13 +14,15 @@ import fs from 'fs';
14
14
  import path from 'path';
15
15
  import os from 'os';
16
16
  import crypto from 'crypto';
17
+ import { hasTrustedFullAccessContext } from './runner-types.js';
17
18
  import { resolveGoogleConfig } from './baseagent.js';
18
19
  import { commandExists } from '../utils/cross-platform.js';
19
20
  import { GeminiSessionFileAdapter } from '../core/session/adapters/gemini-session-file-adapter.js';
20
21
  import { logger } from '../utils/logger.js';
21
- import { normalizePermissionMode } from '../core/permission/mode.js';
22
- import { workspaceContainsHClassPaths } from '../core/protected-paths.js';
22
+ import { normalizeExecutionPermissionMode, normalizePermissionMode } from '../core/permission/mode.js';
23
+ import { workspaceContainsHClassPaths } from '../core/permission/protected-paths.js';
23
24
  import { buildBubblewrapCommand } from '../core/permission/sandbox-runtime.js';
25
+ import { getManagedTaskTempDir } from '../cli/task-context.js';
24
26
  // Strip ANSI escape codes from Gemini CLI text output.
25
27
  // Gemini embeds raw terminal colors from tool stdout (e.g. vitest, npm)
26
28
  // into its assistant text, unlike Claude SDK which strips them internally.
@@ -142,6 +144,8 @@ const GEMINI_EC_COMMAND_PATTERNS = [
142
144
  String.raw `\x00"command":"[ ]*ec[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))*"\x00`,
143
145
  ];
144
146
  export function resolveGeminiPermissionProfile(value, bootstrap = false) {
147
+ if (value === 'fullaccess')
148
+ return { mode: 'fullaccess', approvalMode: 'yolo' };
145
149
  const normalized = normalizePermissionMode(value);
146
150
  const profile = normalized.mode === 'auto'
147
151
  ? { mode: 'auto', approvalMode: 'auto_edit' }
@@ -238,6 +242,12 @@ export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath) {
238
242
  return lines.join('\n');
239
243
  }
240
244
  export function buildGeminiPermissionArgs(profile, policyPath, externallySandboxed = false) {
245
+ if (profile.mode === 'fullaccess') {
246
+ // Absence of --sandbox is not enough: ~/.gemini/settings.json may enable
247
+ // it again. An explicit CLI false keeps this per-call host profile
248
+ // authoritative over user/project settings.
249
+ return ['--sandbox=false', `--approval-mode=${profile.approvalMode}`];
250
+ }
241
251
  return [
242
252
  '--admin-policy', policyPath,
243
253
  ...(!externallySandboxed ? ['--sandbox'] : []),
@@ -269,7 +279,7 @@ export function hasStandardGeminiAdminPolicy() {
269
279
  // ── Gemini Runner ──
270
280
  export class GeminiRunner {
271
281
  name = 'gemini';
272
- capabilities = { clear: true, compact: false, fork: false, forkAtTurn: false, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported' };
282
+ capabilities = { fullaccess: true, clear: true, compact: false, fork: false, forkAtTurn: false, askUserQuestion: false, planApproval: false, fileRewind: 'unsupported' };
273
283
  resolved;
274
284
  model;
275
285
  activeProcesses = new Map();
@@ -329,22 +339,39 @@ export class GeminiRunner {
329
339
  }
330
340
  // ── Core: runQuery ──
331
341
  async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
332
- let geminiSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
342
+ const topicBinding = modelOverride?.backend?.kind === 'topic';
343
+ if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
344
+ const error = new Error('topic backend run requires an active TurnLease and binding callback');
345
+ error.code = 'TOPIC_TURN_LEASE_REQUIRED';
346
+ throw error;
347
+ }
348
+ const topicAgentSessionId = topicBinding ? modelOverride?.backend?.agentSessionId ?? null : undefined;
349
+ const cachedSessionId = topicBinding
350
+ ? (topicAgentSessionId && this.activeSessions.get(sessionId) === topicAgentSessionId ? topicAgentSessionId : undefined)
351
+ : this.activeSessions.get(sessionId);
352
+ let geminiSessionId = topicBinding
353
+ ? (topicAgentSessionId || undefined)
354
+ : initialAgentSessionId || cachedSessionId;
333
355
  // per-call 权限模式/模型:优先 override,缺省回落实例级(多会话并发互不污染)
334
356
  const requestedPermissionMode = modelOverride?.permissionMode || this.currentMode;
357
+ const executionPermissionMode = normalizeExecutionPermissionMode(requestedPermissionMode);
358
+ if (executionPermissionMode === 'fullaccess' && !hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
359
+ throw new Error('Gemini fullaccess requires a trusted per-call execution authorization');
360
+ }
335
361
  const bootstrapAgentMdPath = modelOverride?.bootstrapAgentMdPath
336
362
  ? normalizeGeminiBootstrapAgentMdPath(modelOverride.bootstrapAgentMdPath).filePath
337
363
  : undefined;
338
364
  const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode, !!bootstrapAgentMdPath);
339
365
  const callModel = modelOverride?.model || this.model;
340
- const runtimeDir = runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR;
366
+ const managedTaskTmpDir = getManagedTaskTempDir(runtimeEnv);
367
+ const runtimeDir = managedTaskTmpDir ?? runtimeEnv?.EVOLCORE_SESSION_RUNTIME_DIR;
341
368
  const managedTmpDir = runtimeDir ?? process.env.TMPDIR;
342
369
  if (!managedTmpDir || !path.isAbsolute(managedTmpDir))
343
370
  throw new Error('managed TMPDIR is unset or not absolute');
344
371
  const runtimeLockDir = runtimeEnv?.EVOLCORE_RUNTIME_LOCK_DIR;
345
372
  const geminiHome = path.join(os.homedir(), '.gemini');
346
373
  fs.mkdirSync(geminiHome, { recursive: true, mode: 0o700 });
347
- const sandboxProbe = buildBubblewrapCommand(this.resolved.cliPath, [], {
374
+ const sandboxProbe = executionPermissionMode === 'fullaccess' ? undefined : buildBubblewrapCommand(this.resolved.cliPath, [], {
348
375
  projectPath,
349
376
  writablePaths: [
350
377
  geminiHome,
@@ -353,7 +380,7 @@ export class GeminiRunner {
353
380
  ...(bootstrapAgentMdPath ? [bootstrapAgentMdPath] : []),
354
381
  ],
355
382
  });
356
- if (!sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
383
+ if (executionPermissionMode !== 'fullaccess' && !sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
357
384
  throw new Error('Gemini 缺少可用的路径级隔离运行时,且项目覆盖 EvolCore 受保护根;已拒绝启动本轮任务');
358
385
  }
359
386
  // Build CLI args
@@ -369,12 +396,20 @@ export class GeminiRunner {
369
396
  const tempFiles = [];
370
397
  {
371
398
  if (hasStandardGeminiAdminPolicy()) {
399
+ if (executionPermissionMode === 'fullaccess') {
400
+ throw new Error('Gemini 系统级 admin policy 无法被本次调用可靠关闭;已拒绝以不完整的 fullaccess profile 启动');
401
+ }
372
402
  throw new Error('Gemini 系统级 admin policy 会忽略 EvolCore 的临时安全策略,已拒绝启动本轮任务');
373
403
  }
374
- const policyPath = path.join(managedTmpDir, `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
375
- fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile, bootstrapAgentMdPath), { mode: 0o600, flag: 'wx' });
376
- tempFiles.push(policyPath);
377
- args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
404
+ if (executionPermissionMode === 'fullaccess') {
405
+ args.push(...buildGeminiPermissionArgs(permissionProfile, '', false));
406
+ }
407
+ else {
408
+ const policyPath = path.join(managedTmpDir, `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
409
+ fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile, bootstrapAgentMdPath), { mode: 0o600, flag: 'wx' });
410
+ tempFiles.push(policyPath);
411
+ args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
412
+ }
378
413
  }
379
414
  if (images?.length) {
380
415
  const tmpDir = managedTmpDir;
@@ -406,7 +441,7 @@ export class GeminiRunner {
406
441
  }
407
442
  // Spawn subprocess
408
443
  const env = this.buildAgentEnv(sessionId, runtimeEnv);
409
- const sandboxedCommand = buildBubblewrapCommand(this.resolved.cliPath, args, {
444
+ const sandboxedCommand = executionPermissionMode === 'fullaccess' ? undefined : buildBubblewrapCommand(this.resolved.cliPath, args, {
410
445
  projectPath,
411
446
  writablePaths: [
412
447
  geminiHome,
@@ -429,10 +464,10 @@ export class GeminiRunner {
429
464
  if (msg)
430
465
  logger.debug(`[GeminiRunner:stderr] ${msg}`);
431
466
  });
432
- return this.transformStream(child, sessionId, tempFiles);
467
+ return this.transformStream(child, sessionId, tempFiles, modelOverride?.turn);
433
468
  }
434
469
  // ── Event stream transformation ──
435
- async *transformStream(child, sessionId, tempFiles) {
470
+ async *transformStream(child, sessionId, tempFiles, turn) {
436
471
  const pendingToolNames = new Map(); // toolId → toolName
437
472
  const recordedDeniedToolIds = new Set();
438
473
  const startTime = Date.now();
@@ -506,8 +541,19 @@ export class GeminiRunner {
506
541
  // Extract session_id from init event
507
542
  const geminiId = event.session_id;
508
543
  if (geminiId) {
544
+ const result = this.onSessionIdUpdate
545
+ ? (turn === undefined
546
+ ? await this.onSessionIdUpdate(sessionId, geminiId)
547
+ : await this.onSessionIdUpdate(sessionId, geminiId, { turn }))
548
+ : 'legacy_updated';
549
+ const accepted = result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated';
550
+ if (!accepted) {
551
+ child.kill('SIGTERM');
552
+ const error = new Error(`backend discovery rejected: ${result}`);
553
+ error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
554
+ throw error;
555
+ }
509
556
  this.activeSessions.set(sessionId, geminiId);
510
- this.onSessionIdUpdate?.(sessionId, geminiId);
511
557
  yield { type: 'session_id', sessionId: geminiId };
512
558
  }
513
559
  break;
@@ -619,7 +665,9 @@ export class GeminiRunner {
619
665
  }
620
666
  finally {
621
667
  rl.close();
622
- this.activeProcesses.delete(sessionId);
668
+ if (this.activeProcesses.get(sessionId) === child) {
669
+ this.activeProcesses.delete(sessionId);
670
+ }
623
671
  // Kill process if still running
624
672
  if (!child.killed && !processExited) {
625
673
  child.kill('SIGTERM');
@@ -652,6 +700,12 @@ export class GeminiRunner {
652
700
  toolName,
653
701
  ...(requestId ? { requestId } : {}),
654
702
  policy: context.approvalRouting?.approverPolicy ?? 'requester',
703
+ policyCode: 'permission_mode_denied',
704
+ decisionSource: 'policy',
705
+ agentAid: context.selfAid,
706
+ agentName: context.agentName,
707
+ sessionId,
708
+ permissionMode: context.permissionMode,
655
709
  summary: `${toolName} runtime authorization denied by Gemini headless policy`,
656
710
  effect: 'operation_skipped',
657
711
  });
@@ -679,19 +733,65 @@ export class GeminiRunner {
679
733
  }
680
734
  // ── Session commands ──
681
735
  updateSessionId(sessionId, agentSessionId) {
682
- if (agentSessionId) {
683
- this.activeSessions.set(sessionId, agentSessionId);
684
- }
685
- else {
686
- this.activeSessions.delete(sessionId);
736
+ const previousId = this.activeSessions.get(sessionId);
737
+ const applyLocal = () => {
738
+ const currentId = this.activeSessions.get(sessionId);
739
+ if (currentId !== previousId && !(previousId === undefined && currentId === undefined))
740
+ return;
741
+ if (agentSessionId) {
742
+ this.activeSessions.set(sessionId, agentSessionId);
743
+ }
744
+ else if (this.activeSessions.get(sessionId) === previousId) {
745
+ this.activeSessions.delete(sessionId);
746
+ }
747
+ };
748
+ if (!this.onSessionIdUpdate) {
749
+ applyLocal();
750
+ return;
687
751
  }
688
- this.onSessionIdUpdate?.(sessionId, agentSessionId);
752
+ void Promise.resolve(this.onSessionIdUpdate(sessionId, agentSessionId)).then(result => {
753
+ if (result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated' || !agentSessionId)
754
+ applyLocal();
755
+ }).catch(error => {
756
+ logger.warn(`[GeminiRunner] session binding callback failed: ${error instanceof Error ? error.message : String(error)}`);
757
+ if (!agentSessionId)
758
+ applyLocal();
759
+ });
689
760
  }
690
761
  async closeSession(sessionId) {
691
- this.activeSessions.delete(sessionId);
692
- this.activeStreams.delete(sessionId);
693
- this.activeProcesses.delete(sessionId);
694
- this.permissionContexts.delete(sessionId);
762
+ const capturedId = this.activeSessions.get(sessionId);
763
+ const capturedStream = this.activeStreams.get(sessionId);
764
+ const capturedPermissionContext = this.permissionContexts.get(sessionId);
765
+ const child = this.activeProcesses.get(sessionId);
766
+ if (child && child.exitCode === null && child.signalCode === null) {
767
+ await new Promise(resolve => {
768
+ let settled = false;
769
+ const finish = () => {
770
+ if (settled)
771
+ return;
772
+ settled = true;
773
+ clearTimeout(killTimer);
774
+ child.off('exit', finish);
775
+ child.off('error', finish);
776
+ resolve();
777
+ };
778
+ child.once('exit', finish);
779
+ child.once('error', finish);
780
+ child.kill('SIGTERM');
781
+ const killTimer = setTimeout(() => {
782
+ if (child.exitCode === null && child.signalCode === null)
783
+ child.kill('SIGKILL');
784
+ }, 3_000);
785
+ });
786
+ }
787
+ if (this.activeSessions.get(sessionId) === capturedId)
788
+ this.activeSessions.delete(sessionId);
789
+ if (this.activeStreams.get(sessionId) === capturedStream)
790
+ this.activeStreams.delete(sessionId);
791
+ if (!child || this.activeProcesses.get(sessionId) === child)
792
+ this.activeProcesses.delete(sessionId);
793
+ if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
794
+ this.permissionContexts.delete(sessionId);
695
795
  }
696
796
  resolveSessionFile(agentSessionId, projectPath) {
697
797
  const adapter = new GeminiSessionFileAdapter();
@@ -702,12 +802,12 @@ export class GeminiRunner {
702
802
  this.activeSessions.delete(sessionId);
703
803
  return true;
704
804
  }
705
- async compactSession(_sessionId, _agentSessionId, _projectPath) {
805
+ async compactSession(_sessionId, _agentSessionId, _projectPath, _modelOverride) {
706
806
  logger.info('[GeminiRunner] Compact not supported, Gemini CLI handles context internally');
707
- return false;
807
+ return { ok: false, code: 'unsupported', message: 'Gemini CLI handles context internally', durationMs: 0 };
708
808
  }
709
- async compact(_sessionId, _agentSessionId, _projectPath) {
710
- return this.compactSession(_sessionId, _agentSessionId, _projectPath);
809
+ async compact(_sessionId, _agentSessionId, _projectPath, modelOverride) {
810
+ return this.compactSession(_sessionId, _agentSessionId, _projectPath, modelOverride);
711
811
  }
712
812
  setCompactStartCallback(_callback) { }
713
813
  // ── Cleanup ──
@@ -53,3 +53,28 @@ export function buildModelRequestHeaders(input) {
53
53
  export function _resetRequestIdentityWarningsForTests() {
54
54
  warnedReservedHeaders.clear();
55
55
  }
56
+ /** Preserve existing query parameters and replace configured names deterministically. */
57
+ export function appendRequestQueryParams(url, queryParams) {
58
+ if (!queryParams || Object.keys(queryParams).length === 0)
59
+ return url;
60
+ const parsed = new URL(url);
61
+ for (const [name, value] of Object.entries(queryParams))
62
+ parsed.searchParams.set(name, value);
63
+ return parsed.toString();
64
+ }
65
+ /** Append an API resource to the URL pathname without corrupting an existing query string. */
66
+ export function appendRequestPath(baseUrl, resourcePath, queryParams) {
67
+ const parsed = new URL(baseUrl);
68
+ const basePath = parsed.pathname.replace(/\/+$/u, '');
69
+ const resource = resourcePath.replace(/^\/+|\/+$/gu, '');
70
+ parsed.pathname = `${basePath}/${resource}`;
71
+ for (const [name, value] of Object.entries(queryParams ?? {}))
72
+ parsed.searchParams.set(name, value);
73
+ return parsed.toString();
74
+ }
75
+ /** Append a resource below /v1 unless the configured pathname already ends there. */
76
+ export function appendV1RequestPath(baseUrl, resourcePath, queryParams) {
77
+ const pathname = new URL(baseUrl).pathname.replace(/\/+$/u, '');
78
+ const resource = resourcePath.replace(/^\/+|\/+$/gu, '').replace(/^v1\/+/, '');
79
+ return appendRequestPath(baseUrl, pathname.endsWith('/v1') ? resource : `v1/${resource}`, queryParams);
80
+ }
@@ -12,6 +12,25 @@ export class BaseagentRunnerUnavailableError extends Error {
12
12
  this.name = 'BaseagentRunnerUnavailableError';
13
13
  }
14
14
  }
15
+ export function hasTrustedFullAccessContext(context) {
16
+ const authorization = context?.executionPermission;
17
+ if (!authorization
18
+ || authorization.permissionMode !== 'fullaccess'
19
+ || authorization.processRole !== 'fullaccess-run'
20
+ || authorization.dataScope !== 'daemon')
21
+ return false;
22
+ if (authorization.source === 'fullaccess-command') {
23
+ return typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0;
24
+ }
25
+ return authorization.source === 'trigger'
26
+ && typeof authorization.authorizedBy === 'string' && authorization.authorizedBy.length > 0
27
+ && typeof authorization.triggerId === 'string' && authorization.triggerId.length > 0
28
+ && typeof authorization.runId === 'string' && authorization.runId.length > 0
29
+ // Trigger fullaccess is authorized for one scheduler attempt. Requiring
30
+ // its stable attempt identity prevents a partial trigger envelope from
31
+ // being mistaken for a runnable privileged invocation.
32
+ && typeof authorization.attemptId === 'string' && authorization.attemptId.length > 0;
33
+ }
15
34
  // ── 类型守卫 ──
16
35
  export function hasModelSwitcher(agent) {
17
36
  return typeof agent.setModel === 'function' && typeof agent.listModels === 'function';
@@ -1,7 +1,9 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { parseDocument } from 'yaml';
3
4
  import { getAidStore, loadAid, SLOT } from './store.js';
4
5
  import { agentMdPath, aidLocalDir, resolveRoot } from '../../paths.js';
6
+ export const DEFAULT_AGENT_DESCRIPTION = 'EvolCore AI agent.';
5
7
  const displayNameCache = new Map();
6
8
  const displayNamePending = new Map();
7
9
  const displayNameRevision = new Map();
@@ -82,9 +84,10 @@ export function invalidateAgentDisplayName(aid) {
82
84
  displayNameRevision.set(normalizedAid, (displayNameRevision.get(normalizedAid) ?? 0) + 1);
83
85
  }
84
86
  export function buildInitialAgentMd(opts) {
85
- const agentName = opts.aid.split('.')[0];
87
+ const agentName = opts.name?.trim() || opts.aid.split('.')[0];
86
88
  const agentType = opts.type || 'ai';
87
- return `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolcore\n---\n`;
89
+ const description = opts.description?.trim() || DEFAULT_AGENT_DESCRIPTION;
90
+ return `---\naid: ${yamlDoubleQuote(opts.aid)}\nname: ${yamlDoubleQuote(agentName)}\ntype: ${yamlDoubleQuote(agentType)}\nversion: "1.0.0"\ndescription: ${yamlDoubleQuote(description)}\ntags:\n - evolcore\n---\n`;
88
91
  }
89
92
  function ensureTrailingNewline(content) {
90
93
  return content.endsWith('\n') ? content : `${content}\n`;
@@ -98,6 +101,56 @@ function lineEnding(line, fallback) {
98
101
  function yamlDoubleQuote(value) {
99
102
  return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
100
103
  }
104
+ /** Validate the gateway-facing agent.md frontmatter before signing or upload. */
105
+ export function validateAgentMdFrontmatter(content, expectedAid) {
106
+ const errors = [];
107
+ const payload = stripAgentMdSignature(content);
108
+ if (Buffer.byteLength(payload, 'utf8') > 4 * 1024) {
109
+ errors.push('agent.md must not exceed 4KB before the signature block');
110
+ }
111
+ const frontmatter = payload.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
112
+ if (!frontmatter)
113
+ return { valid: false, errors: [...errors, 'agent.md has no YAML frontmatter'] };
114
+ const document = parseDocument(frontmatter);
115
+ if (document.errors.length > 0) {
116
+ return {
117
+ valid: false,
118
+ errors: [...errors, ...document.errors.map(error => `invalid YAML frontmatter: ${error.message}`)],
119
+ };
120
+ }
121
+ const fields = document.toJS();
122
+ if (!fields || typeof fields !== 'object' || Array.isArray(fields)) {
123
+ return { valid: false, errors: [...errors, 'agent.md frontmatter must be a mapping'] };
124
+ }
125
+ const values = fields;
126
+ const requiredString = (key) => {
127
+ const value = values[key];
128
+ if (typeof value !== 'string' || !value.trim()) {
129
+ errors.push(`agent.md frontmatter ${key} must be a non-empty string`);
130
+ return undefined;
131
+ }
132
+ return value.trim();
133
+ };
134
+ const aid = requiredString('aid');
135
+ requiredString('name');
136
+ requiredString('type');
137
+ const version = requiredString('version');
138
+ const description = requiredString('description');
139
+ if (expectedAid && aid && aid !== expectedAid) {
140
+ errors.push(`agent.md frontmatter aid must match upload target ${expectedAid}`);
141
+ }
142
+ if (version && !/^\d+\.\d+\.\d+$/.test(version)) {
143
+ errors.push('agent.md frontmatter version must use major.minor.patch format');
144
+ }
145
+ if (description && description.length > 100) {
146
+ errors.push('agent.md frontmatter description must be at most 100 characters');
147
+ }
148
+ if (values.tags !== undefined && (!Array.isArray(values.tags)
149
+ || values.tags.some(tag => typeof tag !== 'string' || !tag.trim()))) {
150
+ errors.push('agent.md frontmatter tags must be an array of non-empty strings');
151
+ }
152
+ return { valid: errors.length === 0, errors };
153
+ }
101
154
  /**
102
155
  * Remove the SDK-generated trailing signature block before modifying agent.md.
103
156
  * Only a block at EOF is stripped, so body text that mentions AUN-SIGNATURE is preserved.
@@ -217,6 +270,10 @@ export async function agentmdGet(aid, opts) {
217
270
  * its own LocalTokenStore + AuthFlow internally, so no AUNClient/authenticate needed.
218
271
  */
219
272
  export async function agentmdPut(content, opts) {
273
+ const validation = validateAgentMdFrontmatter(content, opts.aid);
274
+ if (!validation.valid) {
275
+ throw new Error(`invalid agent.md frontmatter: ${validation.errors.join('; ')}`);
276
+ }
220
277
  const aunPath = opts.aunPath ?? resolveRoot();
221
278
  const store = opts.store ?? await getAidStore({ slotId: SLOT.cli, aunPath });
222
279
  const ownStore = !opts.store;
@@ -111,7 +111,10 @@ export async function verifySignAbility(aid, opts) {
111
111
  const code = e instanceof AidLoadError ? e.code : 'LOAD_FAILED';
112
112
  return { ok: false, reason: `load failed: ${code} ${String(e?.message || e).slice(0, 100)}` };
113
113
  }
114
- const probe = `# probe\naid: "${aid}"\n`;
114
+ // signAgentMd validates the same frontmatter contract as a published agent.md.
115
+ // Keep the probe minimal, but make it a valid document so self-checks do not
116
+ // reject otherwise healthy identities before exercising sign + verify.
117
+ const probe = `---\naid: "${aid}"\nname: "probe"\ntype: "ai"\nversion: "1.0.0"\ndescription: "Local signing self-check"\ntags:\n - evolcore\n---\n`;
115
118
  const signRes = aidObj.signAgentMd(probe);
116
119
  if (!signRes.ok) {
117
120
  return { ok: false, reason: `sign failed: ${String(signRes.error?.message || signRes.error?.code).slice(0, 120)}` };
@@ -1,4 +1,4 @@
1
1
  export { isValidAid, aidList, aidListVerified, aidCreate, aidShow, aidDelete, aidLookup, verifySignAbility, probePkiRecoverability, appendAidLifecycle, readAidLifecycle } from './identity.js';
2
- export { buildInitialAgentMd, agentmdGet, agentmdPut, agentmdSync, invalidateAgentDisplayName, parseAgentDisplayName, refreshAgentDisplayName, resolveAgentDisplayName, stripAgentMdSignature, updateAgentMdFrontmatterAvatar, updateAgentMdFrontmatterName, } from './agentmd.js';
2
+ export { DEFAULT_AGENT_DESCRIPTION, buildInitialAgentMd, validateAgentMdFrontmatter, agentmdGet, agentmdPut, agentmdSync, invalidateAgentDisplayName, parseAgentDisplayName, refreshAgentDisplayName, resolveAgentDisplayName, stripAgentMdSignature, updateAgentMdFrontmatterAvatar, updateAgentMdFrontmatterName, } from './agentmd.js';
3
3
  export { MIN_AUN_CORE_SDK, AUN_CORE_SDK_PKG, isAunSdkVersionOk, resolveAunCoreSdkPkg, ensureAunSdk, isAunSdkReady, downloadCaRoot, suppressSdkLogs, } from './client.js';
4
4
  export { getAidStore, loadClient, loadAid, AidLoadError, SLOT } from './store.js';
@@ -15,6 +15,21 @@ import { resolvePaths } from '../../paths.js';
15
15
  import { AGENT_DELEGATION_TOKEN_ENV } from '../../core/auth/agent-delegation.js';
16
16
  import { normalizeAunMentionEntries } from './mention-schema.js';
17
17
  import { downloadGroupFsBytes } from '../group-fs-download.js';
18
+ /** Map a group directory probe failure to a stable Menu protocol code. */
19
+ export function groupDirectoryErrorCode(rawCode, error) {
20
+ if (rawCode === -33001 || rawCode === -33005 || rawCode === -33006 || rawCode === '-33001' || rawCode === '-33005' || rawCode === '-33006')
21
+ return 'NOT_FOUND';
22
+ if (rawCode === -32004 || rawCode === 403 || rawCode === 4030 || rawCode === '-32004' || rawCode === '403' || rawCode === '4030')
23
+ return 'PERMISSION_DENIED';
24
+ if (rawCode === -32001 || rawCode === -32003 || rawCode === 4001 || rawCode === 4010 || rawCode === '-32001' || rawCode === '-32003' || rawCode === '4001' || rawCode === '4010')
25
+ return 'UNAUTHORIZED';
26
+ if (rawCode === 'TEMPORARILY_UNAVAILABLE')
27
+ return 'TEMPORARILY_UNAVAILABLE';
28
+ const message = String(error ?? '').toLowerCase();
29
+ if (message.includes('websocket connect') || message.includes('websocket disconnected') || message.includes('temporarily unavailable') || message.includes('gateway unavailable'))
30
+ return 'TEMPORARILY_UNAVAILABLE';
31
+ return 'TEMPORARILY_UNAVAILABLE';
32
+ }
18
33
  function buildGroupPayload(body) {
19
34
  return body.mode === 'text' ? { type: 'text', text: body.text } : { ...body.payload };
20
35
  }
@@ -312,9 +327,35 @@ export async function groupCreate(args) {
312
327
  catch { }
313
328
  }
314
329
  }
330
+ // Menu pages commonly ask for several fields from the same group at once. Keep
331
+ // concurrent probes on one short connection so a burst does not create several
332
+ // independent WebSocket handshakes for the same AID/group/slot.
333
+ const groupInfoInflight = new Map();
315
334
  export async function groupInfo(args) {
316
- const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
335
+ const key = JSON.stringify([
336
+ args.from,
337
+ args.groupId,
338
+ args.aunPath ?? null,
339
+ args.slotId ?? SLOT.cli,
340
+ [...new Set(args.required ?? [])].sort(),
341
+ ]);
342
+ const existing = groupInfoInflight.get(key);
343
+ if (existing)
344
+ return existing;
345
+ const request = groupInfoUncached(args);
346
+ groupInfoInflight.set(key, request);
317
347
  try {
348
+ return await request;
349
+ }
350
+ finally {
351
+ if (groupInfoInflight.get(key) === request)
352
+ groupInfoInflight.delete(key);
353
+ }
354
+ }
355
+ async function groupInfoUncached(args) {
356
+ let conn;
357
+ try {
358
+ conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
318
359
  const params = { group_id: args.groupId };
319
360
  if (args.required?.length)
320
361
  params.required = args.required;
@@ -324,10 +365,17 @@ export async function groupInfo(args) {
324
365
  return { ok: true, found, ...(found ? { group: normalizeGroupInfo(raw) } : {}) };
325
366
  }
326
367
  catch (e) {
327
- return formatRpcError(e);
368
+ return formatRpcError(e, { transientConnection: true });
328
369
  }
329
370
  finally {
330
- await conn.close();
371
+ if (conn) {
372
+ try {
373
+ await conn.close();
374
+ }
375
+ catch {
376
+ // Cleanup failures must not replace the structured query result.
377
+ }
378
+ }
331
379
  }
332
380
  }
333
381
  export async function groupList(args) {
@@ -1258,11 +1306,29 @@ function normalizeGroupInfo(value) {
1258
1306
  name: String(value?.name ?? groupId),
1259
1307
  };
1260
1308
  }
1261
- function formatRpcError(e) {
1309
+ function formatRpcError(e, options = {}) {
1262
1310
  if (e?.code !== undefined && e?.message !== undefined) {
1263
- return { ok: false, error: String(e.message), code: e.code };
1311
+ return {
1312
+ ok: false,
1313
+ error: String(e.message),
1314
+ code: options.transientConnection && isTransientRpcError(e) ? 'TEMPORARILY_UNAVAILABLE' : e.code,
1315
+ };
1264
1316
  }
1265
- return { ok: false, error: String(e?.message ?? e) };
1317
+ return {
1318
+ ok: false,
1319
+ error: String(e?.message ?? e),
1320
+ ...(options.transientConnection && isTransientRpcError(e) ? { code: 'TEMPORARILY_UNAVAILABLE' } : {}),
1321
+ };
1322
+ }
1323
+ function isTransientRpcError(error) {
1324
+ const value = error;
1325
+ const name = String(value?.name ?? '');
1326
+ const message = String(value?.message ?? error ?? '').toLowerCase();
1327
+ return name === 'ConnectionError'
1328
+ || name === 'TimeoutError'
1329
+ || message.includes('websocket connect timeout')
1330
+ || message.includes('websocket connect failed')
1331
+ || message.includes('websocket disconnected');
1266
1332
  }
1267
1333
  function normalizeRecord(value) {
1268
1334
  return value && typeof value === 'object' ? value : { value };