evolcore 0.0.20 → 0.0.21

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 (123) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +58 -9
  3. package/dist/agents/baseagent.js +10 -6
  4. package/dist/agents/claude-runner.js +379 -108
  5. package/dist/agents/codex-app-server-client.js +10 -2
  6. package/dist/agents/codex-runner.js +402 -135
  7. package/dist/agents/ecagent-runner.js +171 -61
  8. package/dist/agents/gemini-runner.js +130 -30
  9. package/dist/agents/request-identity.js +25 -0
  10. package/dist/agents/runner-types.js +19 -0
  11. package/dist/aun/aid/agentmd.js +59 -2
  12. package/dist/aun/aid/identity.js +4 -1
  13. package/dist/aun/aid/index.js +1 -1
  14. package/dist/aun/msg/group.js +72 -6
  15. package/dist/aun/msg/history.js +213 -36
  16. package/dist/aun/msg/managed-operation.js +58 -9
  17. package/dist/aun/msg/p2p.js +5 -0
  18. package/dist/aun/outbox.js +182 -80
  19. package/dist/aun/service-proxy.js +43 -25
  20. package/dist/channels/aun.js +409 -88
  21. package/dist/channels/daemon.js +6 -1
  22. package/dist/cli/agent-command.js +4 -3
  23. package/dist/cli/agent.js +66 -56
  24. package/dist/cli/aun-commands.js +177 -42
  25. package/dist/cli/command-log.js +10 -11
  26. package/dist/cli/contact.js +1 -0
  27. package/dist/cli/daemon-commands.js +69 -115
  28. package/dist/cli/init.js +27 -15
  29. package/dist/cli/task-context.js +46 -0
  30. package/dist/cli/trigger-command.js +1 -1
  31. package/dist/cli/watch-logs.js +10 -3
  32. package/dist/config/builtin-roles.js +1 -0
  33. package/dist/config/config-field-policy.js +16 -5
  34. package/dist/config/config-manager.js +135 -17
  35. package/dist/config/contact-operation-service.js +32 -1
  36. package/dist/config/contact-request-service.js +44 -0
  37. package/dist/config/daemon-services.js +186 -0
  38. package/dist/config/gateway-config.js +20 -9
  39. package/dist/config/role-service.js +54 -3
  40. package/dist/config/schema-migration.js +550 -0
  41. package/dist/config-store.js +151 -9
  42. package/dist/core/agent-application-service.js +279 -0
  43. package/dist/core/audit/log-integrity.js +102 -0
  44. package/dist/core/auth/agent-delegation.js +31 -1
  45. package/dist/core/auth/auth-gateway.js +33 -4
  46. package/dist/core/auth/authorization-audit.js +150 -2
  47. package/dist/core/auth/operation-authorizer.js +41 -1
  48. package/dist/core/auth/operation-catalog.js +9 -1
  49. package/dist/core/bootstrap-service.js +6 -2
  50. package/dist/core/causation/aun-association.js +7 -4
  51. package/dist/core/command/agent-control.js +56 -16
  52. package/dist/core/command/command-handler.js +290 -44
  53. package/dist/core/command/connect-menu.js +3 -4
  54. package/dist/core/command/group-menu.js +5 -7
  55. package/dist/core/command/menu-handler.js +279 -80
  56. package/dist/core/command/role-menu.js +21 -11
  57. package/dist/core/command/slash-gate.js +85 -18
  58. package/dist/core/command/slash-handler.js +350 -32
  59. package/dist/core/event-catalog.js +5 -0
  60. package/dist/core/evolagent.js +4 -0
  61. package/dist/core/handoff/dispatcher.js +4 -0
  62. package/dist/core/handoff/runtime.js +10 -0
  63. package/dist/core/handoff/store.js +32 -9
  64. package/dist/core/inference/text-inference.js +7 -15
  65. package/dist/core/message/im-renderer.js +83 -84
  66. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  67. package/dist/core/message/message-bridge.js +124 -10
  68. package/dist/core/message/message-log.js +14 -7
  69. package/dist/core/message/message-queue.js +206 -16
  70. package/dist/core/message/message-utils.js +12 -5
  71. package/dist/core/message/response-engine.js +486 -68
  72. package/dist/core/message/send-receipt.js +1 -0
  73. package/dist/core/message/stream-debouncer.js +9 -2
  74. package/dist/core/model/model-catalog.js +23 -15
  75. package/dist/core/model/model-diagnostics.js +28 -10
  76. package/dist/core/permission/approval-gateway.js +180 -6
  77. package/dist/core/permission/ec-command-parser.js +410 -54
  78. package/dist/core/permission/mode.js +18 -3
  79. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  80. package/dist/core/permission/readonly-shell-query.js +263 -9
  81. package/dist/core/permission/sandbox-runtime.js +159 -1
  82. package/dist/core/permission/tool-policy.js +575 -21
  83. package/dist/core/session/session-fs-store.js +154 -5
  84. package/dist/core/session/session-manager.js +299 -30
  85. package/dist/core/session/session-renew.js +19 -12
  86. package/dist/core/session/session-turn-coordinator.js +11 -4
  87. package/dist/eck/kit-renderer.js +1 -1
  88. package/dist/index.js +253 -46
  89. package/dist/ipc.js +374 -24
  90. package/dist/paths.js +64 -7
  91. package/dist/response-system/context-builder.js +1 -7
  92. package/dist/trigger/anomaly-store.js +1 -0
  93. package/dist/trigger/feedback.js +56 -5
  94. package/dist/trigger/history.js +79 -4
  95. package/dist/trigger/legacy-session-history.js +2 -2
  96. package/dist/trigger/parser.js +3 -2
  97. package/dist/trigger/validation.js +6 -1
  98. package/dist/utils/atomic-write.js +27 -0
  99. package/dist/utils/ecweb-utils.js +16 -2
  100. package/dist/utils/error-utils.js +4 -1
  101. package/dist/utils/logger.js +21 -2
  102. package/dist/utils/process-tree-stats.js +24 -4
  103. package/dist/utils/process-tree-worker.js +31 -0
  104. package/dist/utils/project-path.js +1 -2
  105. package/kits/docs/INDEX.md +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -1
  107. package/kits/docs/evolcore/contact.md +7 -1
  108. package/kits/docs/evolcore/msg.md +16 -0
  109. package/kits/schemas/_meta.json +4 -2
  110. package/kits/schemas/agent-config.schema.11.json +13 -0
  111. package/kits/schemas/daemon.schema.5.json +0 -1
  112. package/kits/schemas/daemon.schema.6.json +131 -0
  113. package/kits/schemas/defaults.schema.5.json +15 -3
  114. package/kits/schemas/migrations/README.md +3 -1
  115. package/kits/schemas/relation-config.schema.8.json +13 -0
  116. package/kits/schemas/role-config.schema.1.json +1 -2
  117. package/kits/schemas/single-session.schema.3.json +32 -0
  118. package/kits/templates/roles/admin.json +1 -0
  119. package/kits/templates/roles/member.json +1 -0
  120. package/kits/templates/roles/visitor.json +1 -0
  121. package/package.json +6 -3
  122. package/skills/eclink/SKILL.md +2 -0
  123. package/dist/config/aun-gateway-config.js +0 -2
@@ -11,6 +11,7 @@ export function sentReceipt(envelope, messageIds, threadId) {
11
11
  return {
12
12
  status: 'sent',
13
13
  operationId: sendOperationId(envelope),
14
+ messageId: messages[0].messageId,
14
15
  messages: messages,
15
16
  };
16
17
  }
@@ -27,6 +27,13 @@ export class StreamDebouncer {
27
27
  * 否则新建窗口。timer 到期后自动 flush。
28
28
  */
29
29
  submit(key, message, enqueue) {
30
+ // A per-call permission override is an authorization boundary. Flush any
31
+ // earlier ordinary input first, then enqueue the authorized call on its
32
+ // own so neither side can inherit or erase the override during merging.
33
+ if (message.executionPermissionOverride) {
34
+ const preceding = this.flush(key, enqueue);
35
+ return preceding ? preceding.then(() => enqueue(message)) : enqueue(message);
36
+ }
30
37
  const { content, images, mentions, messageId, sourceMessageIds, replyContext, ...rest } = message;
31
38
  return new Promise((resolve, reject) => {
32
39
  const entry = { messageId, sourceMessageIds, content, images, mentions, replyContext, rest, resolve, reject };
@@ -79,7 +86,7 @@ export class StreamDebouncer {
79
86
  flush(key, enqueue) {
80
87
  const win = this.pending.get(key);
81
88
  if (!win)
82
- return;
89
+ return undefined;
83
90
  this.pending.delete(key);
84
91
  clearTimeout(win.timer);
85
92
  clearTimeout(win.maxWaitTimer);
@@ -130,7 +137,7 @@ export class StreamDebouncer {
130
137
  };
131
138
  const resolves = entries.map(e => e.resolve);
132
139
  const rejects = entries.map(e => e.reject);
133
- enqueue(merged).then(() => resolves.forEach(r => r()), (e) => rejects.forEach(r => r(e)));
140
+ return enqueue(merged).then(() => resolves.forEach(r => r()), (e) => rejects.forEach(r => r(e)));
134
141
  }
135
142
  /** 当前挂起的 key 数量(用于测试/调试) */
136
143
  get pendingCount() {
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../../agents/baseagent.js';
14
14
  import { buildModelRequestHeaders } from '../../agents/request-identity.js';
15
+ import { appendRequestPath, appendV1RequestPath } from '../../agents/request-identity.js';
15
16
  import { activeBaseagent } from './config-scope.js';
16
17
  import { resolveEffective } from '../../config/config-manager.js';
17
18
  import { loadDefaults } from '../../config-store.js';
@@ -33,6 +34,7 @@ function resolveCreds(self, ba) {
33
34
  apiKey: r.apiKey,
34
35
  agentAid: self,
35
36
  headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
37
+ queryParams: r.queryParams,
36
38
  };
37
39
  }
38
40
  if (baseagent === 'ecagent') {
@@ -43,6 +45,7 @@ function resolveCreds(self, ba) {
43
45
  apiKey: r.apiKey,
44
46
  agentAid: self,
45
47
  headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
48
+ queryParams: r.queryParams,
46
49
  };
47
50
  }
48
51
  if (baseagent === 'gemini')
@@ -56,6 +59,7 @@ function resolveCreds(self, ba) {
56
59
  apiKey: r.apiKey,
57
60
  agentAid: self,
58
61
  headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
62
+ queryParams: r.queryParams,
59
63
  };
60
64
  }
61
65
  catch {
@@ -275,22 +279,26 @@ async function loadCatalog(self, baseagent) {
275
279
  if (cliModels)
276
280
  return { models: cliModels, source: 'codex-cli' };
277
281
  }
278
- const { baseUrl, apiKey, headers } = resolveCreds(self, baseagent);
282
+ const { baseUrl, apiKey, headers, queryParams } = resolveCreds(self, baseagent);
279
283
  if (baseUrl) {
280
- const base = baseUrl.replace(/\/+$/, '');
281
- const urls = [...new Set([
282
- base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`,
283
- `${base}/models`,
284
- ])];
285
- // Probe compatible routes concurrently so a dead route does not add
286
- // another full timeout before the other route is attempted.
287
- const results = await Promise.all(urls.map(url => fetchModelList(url, apiKey, headers)));
288
- const hitIndex = results.findIndex((models) => !!models);
289
- if (hitIndex >= 0) {
290
- return {
291
- models: withAliases(results[hitIndex], baseagent),
292
- source: hitIndex === 0 ? 'v1/models' : 'models',
293
- };
284
+ try {
285
+ const urls = [...new Set([
286
+ appendV1RequestPath(baseUrl, 'models', queryParams),
287
+ appendRequestPath(baseUrl, 'models', queryParams),
288
+ ])];
289
+ // Probe compatible routes concurrently so a dead route does not add
290
+ // another full timeout before the other route is attempted.
291
+ const results = await Promise.all(urls.map(url => fetchModelList(url, apiKey, headers)));
292
+ const hitIndex = results.findIndex((models) => !!models);
293
+ if (hitIndex >= 0) {
294
+ return {
295
+ models: withAliases(results[hitIndex], baseagent),
296
+ source: hitIndex === 0 ? 'v1/models' : 'models',
297
+ };
298
+ }
299
+ }
300
+ catch {
301
+ // Invalid configured URLs behave like other unavailable gateways.
294
302
  }
295
303
  }
296
304
  // This remote directory is Claude-only. Never use it for Codex or Gemini.
@@ -1,5 +1,6 @@
1
1
  import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../../agents/baseagent.js';
2
2
  import { buildModelRequestHeaders } from '../../agents/request-identity.js';
3
+ import { appendV1RequestPath } from '../../agents/request-identity.js';
3
4
  import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
4
5
  import { constrainResolvedModelForRole } from './model-permission.js';
5
6
  import { getCatalog } from './model-catalog.js';
@@ -27,6 +28,7 @@ function resolveGatewayCreds(self, baseagent) {
27
28
  baseUrl: resolved.baseUrl,
28
29
  apiKey: resolved.apiKey,
29
30
  headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }),
31
+ queryParams: resolved.queryParams,
30
32
  };
31
33
  }
32
34
  if (baseagent === 'claude') {
@@ -36,6 +38,7 @@ function resolveGatewayCreds(self, baseagent) {
36
38
  baseUrl: resolved.baseUrl,
37
39
  apiKey: resolved.apiKey,
38
40
  headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }),
41
+ queryParams: resolved.queryParams,
39
42
  };
40
43
  }
41
44
  if (baseagent === 'ecagent') {
@@ -45,6 +48,7 @@ function resolveGatewayCreds(self, baseagent) {
45
48
  baseUrl: resolved.baseUrl,
46
49
  apiKey: resolved.apiKey,
47
50
  headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }),
51
+ queryParams: resolved.queryParams,
48
52
  };
49
53
  }
50
54
  }
@@ -64,15 +68,23 @@ async function responseError(resp) {
64
68
  }
65
69
  return String(message).slice(0, 80);
66
70
  }
67
- export async function probeModel(baseUrl, apiKey, modelId, timeoutMs, requestHeaders) {
68
- const base = baseUrl.replace(/\/+$/, '');
71
+ export async function probeModel(baseUrl, apiKey, modelId, timeoutMs, requestHeaders, queryParams) {
72
+ let anthropicUrl;
73
+ let openaiUrl;
74
+ try {
75
+ anthropicUrl = appendV1RequestPath(baseUrl, 'messages', queryParams);
76
+ openaiUrl = appendV1RequestPath(baseUrl, 'chat/completions', queryParams);
77
+ }
78
+ catch {
79
+ return { ok: false, ms: 0, error: '网关地址无效', style: 'unknown' };
80
+ }
69
81
  const authHeaders = apiKey
70
82
  ? { 'x-api-key': apiKey, Authorization: `Bearer ${apiKey}` }
71
83
  : {};
72
84
  const deadline = Date.now() + timeoutMs;
73
85
  const remainingMs = () => Math.max(0, deadline - Date.now());
74
86
  const anthropicStartedAt = Date.now();
75
- const anthropic = await fetchWithTimeout(`${base}/v1/messages`, {
87
+ const anthropic = await fetchWithTimeout(anthropicUrl, {
76
88
  method: 'POST',
77
89
  headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', ...authHeaders, ...requestHeaders },
78
90
  body: JSON.stringify({ model: modelId, max_tokens: 5, messages: [{ role: 'user', content: 'ok' }] }),
@@ -90,7 +102,7 @@ export async function probeModel(baseUrl, apiKey, modelId, timeoutMs, requestHea
90
102
  return { ok: false, ms: Date.now() - anthropicStartedAt, error: '探测超时', style: 'unknown' };
91
103
  }
92
104
  const openaiStartedAt = Date.now();
93
- const openai = await fetchWithTimeout(`${base}/v1/chat/completions`, {
105
+ const openai = await fetchWithTimeout(openaiUrl, {
94
106
  method: 'POST',
95
107
  headers: { 'Content-Type': 'application/json', ...authHeaders, ...requestHeaders },
96
108
  body: JSON.stringify({ model: modelId, max_tokens: 5, messages: [{ role: 'user', content: 'ok' }] }),
@@ -117,16 +129,22 @@ export async function runModelCheck(input) {
117
129
  steps.push(step);
118
130
  input.onStep?.(step);
119
131
  };
120
- const { baseUrl, apiKey, headers } = resolveGatewayCreds(input.self, input.baseagent);
132
+ const { baseUrl, apiKey, headers, queryParams } = resolveGatewayCreds(input.self, input.baseagent);
121
133
  if (!baseUrl) {
122
134
  log({ label: '网关地址', ok: false, detail: '未配置受支持的自定义网关,跳过 API 探测' });
123
135
  }
124
136
  else {
125
137
  const startedAt = Date.now();
126
- const ping = await fetchWithTimeout(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
127
- method: 'GET',
128
- headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
129
- }, 4000);
138
+ let ping = null;
139
+ try {
140
+ ping = await fetchWithTimeout(appendV1RequestPath(baseUrl, 'models', queryParams), {
141
+ method: 'GET',
142
+ headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
143
+ }, 4000);
144
+ }
145
+ catch {
146
+ // Report malformed URLs through the same failed-connectivity step.
147
+ }
130
148
  const elapsed = Date.now() - startedAt;
131
149
  log(ping
132
150
  ? { label: '网关连通', ok: true, detail: baseUrl, ms: elapsed }
@@ -169,7 +187,7 @@ export async function runModelCheck(input) {
169
187
  ];
170
188
  for (let index = 0; index < targets.length; index += 2) {
171
189
  const batch = await Promise.all(targets.slice(index, index + 2).map(async (target) => {
172
- const result = await probeModel(baseUrl, apiKey, target.probe, 10_000, headers);
190
+ const result = await probeModel(baseUrl, apiKey, target.probe, 10_000, headers, queryParams);
173
191
  return { id: target.display, ...result };
174
192
  }));
175
193
  for (const result of batch) {
@@ -1,13 +1,14 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
2
  import { hasTrustedPrincipal } from '../auth/authenticated-actor.js';
3
3
  import { renderActionAsText } from '../interaction-router.js';
4
- import { buildEnvelope, sendInteractionPayload } from '../message/message-utils.js';
4
+ import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../message/message-utils.js';
5
5
  import { logger } from '../../utils/logger.js';
6
6
  import { summarizeToolInput } from '../../utils/tool-summary.js';
7
7
  import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
8
8
  import { recordCausationSpan } from '../causation/audit.js';
9
9
  import { checkDangerousCommand } from './tool-policy.js';
10
- import { resolveProtectedCandidate } from '../protected-paths.js';
10
+ import { resolveProtectedCandidate } from './protected-paths.js';
11
+ import { auditPermissionDecision } from '../auth/authorization-audit.js';
11
12
  export async function requestDangerousCommandPermission(gateway, sessionId, toolName, input, sendPrompt, context, grantScope = 'default', mode = 'request') {
12
13
  const dangerCheck = checkDangerousCommand(toolName, input);
13
14
  if (!dangerCheck.isDangerous) {
@@ -18,6 +19,19 @@ export async function requestDangerousCommandPermission(gateway, sessionId, tool
18
19
  }
19
20
  if (!gateway || !sendPrompt) {
20
21
  logger.warn(`[PermissionGateway] Dangerous operation denied because approval is unavailable: session=${sessionId} tool=${toolName}`);
22
+ auditPermissionDecision({
23
+ requestId: `perm-infra-${randomUUID()}`,
24
+ sessionId,
25
+ toolName,
26
+ decisionSource: 'infrastructure',
27
+ policyCode: 'approval_channel_unavailable',
28
+ reason: 'dangerous operation approval channel is unavailable',
29
+ permissionMode: context?.permissionMode,
30
+ agentAid: context?.selfAid,
31
+ agentName: context?.agentName,
32
+ role: context?.role,
33
+ taskId: context?.taskId,
34
+ });
21
35
  return { matched: true, decision: 'deny' };
22
36
  }
23
37
  const decision = await gateway.requestPermission(sessionId, dangerCheck.cacheKey, input, sendPrompt, context, `⚠️ ${dangerCheck.command}`, dangerCheck.reason, grantScope, dangerCheck.kind === 'git-destructive' ? 'agent_manager' : undefined);
@@ -432,6 +446,21 @@ export class PermissionGateway {
432
446
  if (sessionGrant)
433
447
  this.addTemporaryGrant(pending, decisionCausation);
434
448
  pending.resolve(decision);
449
+ if (!approved) {
450
+ auditPermissionDecision({
451
+ requestId,
452
+ sessionId,
453
+ toolName: pending.toolName,
454
+ decisionSource: 'approval',
455
+ policyCode: 'user_approval_denied',
456
+ reason: 'owner approval denied',
457
+ permissionMode: pending.permissionMode,
458
+ agentAid: pending.selfAid,
459
+ agentName: pending.agentName,
460
+ role: pending.role,
461
+ taskId: pending.taskId,
462
+ });
463
+ }
435
464
  void this.notifyPermissionState(pending, approved
436
465
  ? { state: 'approved', requestId, toolName: pending.toolName }
437
466
  : {
@@ -446,7 +475,17 @@ export class PermissionGateway {
446
475
  refs: { permissionRequestId: requestId, sessionId },
447
476
  reason: approved ? undefined : 'denied',
448
477
  });
449
- this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, approved, causation: decisionCausation });
478
+ this.eventBus?.publish({
479
+ type: 'permission:resolved',
480
+ sessionId,
481
+ requestId,
482
+ toolName: pending.toolName,
483
+ approved,
484
+ ...(approved
485
+ ? { decisionSource: 'approval' }
486
+ : { decisionSource: 'approval', policyCode: 'user_approval_denied' }),
487
+ causation: decisionCausation,
488
+ });
450
489
  return true;
451
490
  }
452
491
  expireCrossSessionPermission(sessionId, requestId, reason, notifyState = true, cancelled = reason === 'cancelled') {
@@ -458,6 +497,21 @@ export class PermissionGateway {
458
497
  this.crossSessionPending.delete(requestId);
459
498
  this.syncManagementRevalidationTimer();
460
499
  pending.resolve('deny');
500
+ const decisionSource = 'infrastructure';
501
+ const policyCode = cancelled ? 'approval_cancelled' : `approval_${reason}`;
502
+ auditPermissionDecision({
503
+ requestId,
504
+ sessionId,
505
+ toolName: pending.toolName,
506
+ decisionSource,
507
+ policyCode,
508
+ reason,
509
+ permissionMode: pending.permissionMode,
510
+ agentAid: pending.selfAid,
511
+ agentName: pending.agentName,
512
+ role: pending.role,
513
+ taskId: pending.taskId,
514
+ });
461
515
  if (notifyState) {
462
516
  const failure = permissionFailure(reason);
463
517
  void this.notifyPermissionState(pending, {
@@ -481,6 +535,8 @@ export class PermissionGateway {
481
535
  requestId,
482
536
  toolName: pending.toolName,
483
537
  reason,
538
+ decisionSource: 'infrastructure',
539
+ policyCode: `approval_${reason}`,
484
540
  causation: decisionCausation,
485
541
  });
486
542
  }
@@ -492,6 +548,8 @@ export class PermissionGateway {
492
548
  toolName: pending.toolName,
493
549
  reason,
494
550
  approved: false,
551
+ decisionSource: 'infrastructure',
552
+ policyCode: `approval_${reason}`,
495
553
  causation: decisionCausation,
496
554
  });
497
555
  }
@@ -502,6 +560,18 @@ export class PermissionGateway {
502
560
  const interactionRouter = context.interactionRouter;
503
561
  if (!approval || !interactionRouter) {
504
562
  await sendPrompt('当前会话权限不足,且没有可用的 owner 审批通道。');
563
+ auditPermissionDecision({
564
+ sessionId,
565
+ toolName: challenge.toolName,
566
+ decisionSource: 'infrastructure',
567
+ policyCode: 'approval_route_unavailable',
568
+ reason: 'owner approval routing context is unavailable',
569
+ permissionMode: context.permissionMode,
570
+ agentAid: context.selfAid,
571
+ agentName: context.agentName,
572
+ role: context.role,
573
+ taskId: context.taskId,
574
+ });
505
575
  return 'deny';
506
576
  }
507
577
  const { id: requestId, toolName, toolInput, summary: displaySummary, reason } = challenge;
@@ -593,6 +663,10 @@ export class PermissionGateway {
593
663
  approverChannelKey: route.approverChannelKey,
594
664
  approverPrincipalId: route.approverPrincipalId,
595
665
  selfAid: approval.selfAid,
666
+ agentName: context.agentName,
667
+ permissionMode: context.permissionMode,
668
+ role: context.role,
669
+ taskId: context.taskId,
596
670
  originPrincipalId: approval.originPrincipalId,
597
671
  originAuthRevision: approval.originAuthRevision,
598
672
  reauthorizeApproval: approval.reauthorizeApproval,
@@ -661,7 +735,8 @@ export class PermissionGateway {
661
735
  });
662
736
  let sent = false;
663
737
  try {
664
- sent = !!await sendInteractionPayload(route.adapter, envelope, interaction, renderActionAsText(interaction), ownerReplyContext);
738
+ const receipt = await sendInteractionPayload(route.adapter, envelope, interaction, renderActionAsText(interaction), ownerReplyContext);
739
+ sent = isInteractionSendAccepted(receipt);
665
740
  }
666
741
  catch (error) {
667
742
  logger.warn(`[PermissionGateway] Owner approval delivery failed: session=${sessionId} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
@@ -700,6 +775,20 @@ export class PermissionGateway {
700
775
  this.pending.delete(requestId);
701
776
  this.syncManagementRevalidationTimer();
702
777
  pending.resolve('deny');
778
+ const decisionSource = reason === 'denied' ? 'approval' : 'infrastructure';
779
+ auditPermissionDecision({
780
+ requestId,
781
+ sessionId,
782
+ toolName: pending.toolName,
783
+ decisionSource,
784
+ policyCode: `approval_${reason}`,
785
+ reason,
786
+ permissionMode: pending.permissionMode,
787
+ agentAid: pending.selfAid,
788
+ agentName: pending.agentName,
789
+ role: pending.role,
790
+ taskId: pending.taskId,
791
+ });
703
792
  const decisionCausation = deriveCausation(pending.causation);
704
793
  recordCausationSpan(decisionCausation, 'permission.decision', {
705
794
  status: 'failed',
@@ -713,6 +802,8 @@ export class PermissionGateway {
713
802
  toolName: pending.toolName,
714
803
  reason,
715
804
  approved: false,
805
+ decisionSource,
806
+ policyCode: `approval_${reason}`,
716
807
  causation: decisionCausation,
717
808
  });
718
809
  return true;
@@ -725,6 +816,7 @@ export class PermissionGateway {
725
816
  ?? context?.approvalRouting?.approverPolicy
726
817
  ?? 'requester';
727
818
  if (context?.approvalInteractionPolicy === 'deny') {
819
+ const denialRequestId = `perm-policy-${randomUUID()}`;
728
820
  const anomaly = {
729
821
  code: 'runtime_permission_denied',
730
822
  severity: 'warning',
@@ -732,6 +824,12 @@ export class PermissionGateway {
732
824
  occurredAt: Date.now(),
733
825
  toolName,
734
826
  policy: effectiveApproverPolicy,
827
+ policyCode: 'permission_mode_denied',
828
+ decisionSource: 'policy',
829
+ agentAid: context.selfAid,
830
+ sessionId,
831
+ requestId: denialRequestId,
832
+ permissionMode: context.permissionMode,
735
833
  summary: structuredApprovalAuditSummary(toolInput),
736
834
  effect: 'operation_skipped',
737
835
  };
@@ -742,10 +840,35 @@ export class PermissionGateway {
742
840
  logger.warn(`[PermissionGateway] Failed to record unattended denial: session=${sessionId} tool=${toolName} error=${error instanceof Error ? error.message : String(error)}`);
743
841
  }
744
842
  logger.info(`[PermissionGateway] Unattended runtime permission denied: session=${sessionId} tool=${toolName} policy=${effectiveApproverPolicy}`);
843
+ auditPermissionDecision({
844
+ requestId: denialRequestId,
845
+ sessionId,
846
+ toolName,
847
+ decisionSource: 'policy',
848
+ policyCode: 'permission_mode_denied',
849
+ reason: 'permission mode denied runtime approval',
850
+ permissionMode: context.permissionMode,
851
+ agentAid: context.selfAid,
852
+ agentName: context.agentName,
853
+ role: context.role,
854
+ taskId: context.taskId,
855
+ });
745
856
  return 'deny';
746
857
  }
747
858
  if (!context?.userId) {
748
859
  await sendPrompt('当前操作需要授权,但无法验证申请人身份。');
860
+ auditPermissionDecision({
861
+ sessionId,
862
+ toolName,
863
+ decisionSource: 'infrastructure',
864
+ policyCode: 'approval_identity_missing',
865
+ reason: 'approval requester identity is unavailable',
866
+ permissionMode: context?.permissionMode,
867
+ agentAid: context?.selfAid,
868
+ agentName: context?.agentName,
869
+ role: context?.role,
870
+ taskId: context?.taskId,
871
+ });
749
872
  return 'deny';
750
873
  }
751
874
  const grantMatch = permissionGrantMatch(toolName, toolInput, matchOptions);
@@ -803,8 +926,23 @@ export class PermissionGateway {
803
926
  toolName,
804
927
  reason: route.reason,
805
928
  approved: false,
929
+ decisionSource: 'infrastructure',
930
+ policyCode: 'approval_route_unavailable',
806
931
  causation: decisionCausation,
807
932
  });
933
+ auditPermissionDecision({
934
+ requestId,
935
+ sessionId,
936
+ toolName,
937
+ decisionSource: 'infrastructure',
938
+ policyCode: 'approval_route_unavailable',
939
+ reason: route.reason,
940
+ permissionMode: context.permissionMode,
941
+ agentAid: context.selfAid,
942
+ agentName: context.agentName,
943
+ role: context.role,
944
+ taskId: context.taskId,
945
+ });
808
946
  return 'deny';
809
947
  }
810
948
  if (route.kind === 'handoff') {
@@ -857,6 +995,7 @@ export class PermissionGateway {
857
995
  approverChannelKey: route.approverChannelKey,
858
996
  approverPrincipalId: route.approverPrincipalId,
859
997
  selfAid: context?.approvalRouting?.selfAid,
998
+ agentName: context?.agentName,
860
999
  originPrincipalId: context?.approvalRouting?.originPrincipalId,
861
1000
  originAuthRevision: context?.approvalRouting?.originAuthRevision,
862
1001
  reauthorizeApproval: context?.approvalRouting?.reauthorizeApproval,
@@ -864,6 +1003,9 @@ export class PermissionGateway {
864
1003
  interactionRouter: context?.interactionRouter,
865
1004
  causation: requestCausation,
866
1005
  permissionStateChanged: context?.permissionStateChanged,
1006
+ permissionMode: context?.permissionMode,
1007
+ role: context?.role,
1008
+ taskId: context?.taskId,
867
1009
  };
868
1010
  pending.timeout = setTimeout(() => {
869
1011
  this.failPendingPermission(sessionId, requestId, 'expired');
@@ -925,7 +1067,7 @@ export class PermissionGateway {
925
1067
  : '同操作授权 30 分钟';
926
1068
  const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm ${requestId} allow 允许本次 / /perm ${requestId} always ${temporaryGrantLabel} / /perm ${requestId} deny 拒绝`;
927
1069
  const result = await sendInteractionPayload(context.adapter, envelope, interaction, fallbackText, context.replyContext);
928
- interactionSent = !!result;
1070
+ interactionSent = isInteractionSendAccepted(result);
929
1071
  }
930
1072
  catch (err) {
931
1073
  // sendInteractionPayload 已内部捕获,但保险起见再 try/catch
@@ -979,7 +1121,24 @@ export class PermissionGateway {
979
1121
  refs: { permissionRequestId: requestId, sessionId },
980
1122
  reason: normalizedDecision,
981
1123
  });
982
- this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, approved: normalizedDecision !== 'deny', causation: decisionCausation });
1124
+ const decisionSource = 'approval';
1125
+ const policyCode = normalizedDecision === 'deny' ? 'user_approval_denied' : undefined;
1126
+ if (normalizedDecision === 'deny') {
1127
+ auditPermissionDecision({
1128
+ requestId,
1129
+ sessionId,
1130
+ toolName: pending.toolName,
1131
+ decisionSource,
1132
+ policyCode: 'user_approval_denied',
1133
+ reason: 'user approval denied',
1134
+ permissionMode: pending.permissionMode,
1135
+ agentAid: pending.selfAid,
1136
+ agentName: pending.agentName,
1137
+ role: pending.role,
1138
+ taskId: pending.taskId,
1139
+ });
1140
+ }
1141
+ this.eventBus?.publish({ type: 'permission:resolved', sessionId, requestId, toolName: pending.toolName, approved: normalizedDecision !== 'deny', ...(policyCode ? { decisionSource, policyCode } : { decisionSource }), causation: decisionCausation });
983
1142
  return true;
984
1143
  }
985
1144
  /**
@@ -1042,6 +1201,19 @@ export class PermissionGateway {
1042
1201
  this.clearPendingResources(pending);
1043
1202
  pending.interactionRouter?.cancel(requestId, reason);
1044
1203
  pending.resolve('deny');
1204
+ auditPermissionDecision({
1205
+ requestId,
1206
+ sessionId,
1207
+ toolName: pending.toolName,
1208
+ decisionSource: 'infrastructure',
1209
+ policyCode: `approval_${reason}`,
1210
+ reason,
1211
+ permissionMode: pending.permissionMode,
1212
+ agentAid: pending.selfAid,
1213
+ agentName: pending.agentName,
1214
+ role: pending.role,
1215
+ taskId: pending.taskId,
1216
+ });
1045
1217
  this.pending.delete(requestId);
1046
1218
  const decisionCausation = deriveCausation(pending.causation);
1047
1219
  recordCausationSpan(decisionCausation, 'permission.decision', {
@@ -1055,6 +1227,8 @@ export class PermissionGateway {
1055
1227
  requestId,
1056
1228
  toolName: pending.toolName,
1057
1229
  reason,
1230
+ decisionSource: 'infrastructure',
1231
+ policyCode: `approval_${reason}`,
1058
1232
  causation: decisionCausation,
1059
1233
  });
1060
1234
  }