evolcore 0.0.35 → 0.0.37

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 (224) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +6 -0
  3. package/bin/install-codex-managed-hooks.mjs +20 -3
  4. package/dist/agents/baseagent-availability.js +1 -1
  5. package/dist/agents/baseagent.js +88 -0
  6. package/dist/agents/claude-runner.js +59 -12
  7. package/dist/agents/codex/app-server-client.js +18 -1
  8. package/dist/agents/codex-runner.js +262 -21
  9. package/dist/agents/ecagent-runner.js +47 -11
  10. package/dist/agents/gemini-runner.js +36 -12
  11. package/dist/aun/aid/client.js +4 -3
  12. package/dist/aun/aid/control-aid.js +270 -8
  13. package/dist/aun/aid/identity.js +6 -3
  14. package/dist/aun/aid/ownership-operation.js +58 -0
  15. package/dist/aun/aid/store.js +1 -0
  16. package/dist/aun/group/fs-download.js +67 -2
  17. package/dist/aun/group/history.js +20 -5
  18. package/dist/aun/group/operations.js +8 -2
  19. package/dist/aun/menu/client.js +39 -2
  20. package/dist/aun/menu/transport.js +1 -1
  21. package/dist/aun/msg/history.js +43 -4
  22. package/dist/aun/msg/managed-operation.js +30 -53
  23. package/dist/aun/msg/p2p.js +51 -15
  24. package/dist/aun/outbox.js +4 -0
  25. package/dist/channels/aun.js +260 -73
  26. package/dist/channels/daemon.js +6 -4
  27. package/dist/channels/feishu.js +3 -0
  28. package/dist/cli/agent-command.js +161 -223
  29. package/dist/cli/agent-create.js +190 -223
  30. package/dist/cli/agent.js +3 -1
  31. package/dist/cli/aid-bind-command.js +53 -0
  32. package/dist/cli/aun-commands.js +384 -91
  33. package/dist/cli/bench.js +1 -1
  34. package/dist/cli/cli-argv.js +74 -0
  35. package/dist/cli/daemon-commands.js +105 -74
  36. package/dist/cli/diag.js +1 -1
  37. package/dist/cli/fs-command.js +18 -19
  38. package/dist/cli/getmeta-command.js +159 -6
  39. package/dist/cli/index.js +6 -12
  40. package/dist/cli/init-channel.js +8 -55
  41. package/dist/cli/init.js +125 -218
  42. package/dist/cli/queue-command.js +11 -8
  43. package/dist/cli/restart-monitor.js +177 -85
  44. package/dist/cli/session-command.js +1 -1
  45. package/dist/cli/stats.js +7 -5
  46. package/dist/cli/thread-command.js +103 -30
  47. package/dist/cli/trigger-command.js +1 -1
  48. package/dist/cli/watch-msg-diag.js +1 -0
  49. package/dist/config/access-policy.js +2 -2
  50. package/dist/config/boot-log.js +14 -11
  51. package/dist/config/config-field-policy.js +9 -1
  52. package/dist/config/config-manager.js +176 -90
  53. package/dist/config/config-operation-service.js +7 -14
  54. package/dist/config/contact-book-store.js +142 -26
  55. package/dist/config/contact-book.js +4 -2
  56. package/dist/config/lifecycle.js +1 -1
  57. package/dist/config/owner-config.js +96 -0
  58. package/dist/config/peer-role-resolver.js +12 -18
  59. package/dist/config/providers-config.js +382 -0
  60. package/dist/config/providers-import.js +74 -0
  61. package/dist/config/providers-management.js +328 -0
  62. package/dist/config/providers.js +22 -0
  63. package/dist/config/resolved-config-op.js +17 -14
  64. package/dist/config/snapshot.js +28 -16
  65. package/dist/config-store.js +18 -14
  66. package/dist/core/agent-ipc-registration.js +21 -63
  67. package/dist/core/agent-runtime.js +16 -5
  68. package/dist/core/auth/agent-delegation.js +199 -25
  69. package/dist/core/auth/auth-gateway.js +44 -2
  70. package/dist/core/auth/authorization-denial.js +1 -1
  71. package/dist/core/auth/debug-context.js +2 -1
  72. package/dist/core/auth/delegation-authorization.js +2 -2
  73. package/dist/core/auth/delegation-diagnostics.js +4 -1
  74. package/dist/core/auth/operation-authorizer.js +10 -1
  75. package/dist/core/auth/operation-catalog.js +19 -1
  76. package/dist/core/bootstrap-service.js +43 -20
  77. package/dist/core/channel-loader.js +0 -1
  78. package/dist/core/channel-runtime.js +30 -4
  79. package/dist/core/command/agent-control.js +196 -37
  80. package/dist/core/command/cli-intent-parser.js +60 -4
  81. package/dist/core/command/command-handler.js +212 -60
  82. package/dist/core/command/connect-menu.js +1 -0
  83. package/dist/core/command/diag-service.js +5 -4
  84. package/dist/core/command/file-search.js +368 -0
  85. package/dist/core/command/menu-catalog.js +2 -1
  86. package/dist/core/command/menu-handler.js +292 -107
  87. package/dist/core/command/menu-protocol.js +23 -0
  88. package/dist/core/command/role-menu.js +7 -19
  89. package/dist/core/command/skill-slash.js +130 -25
  90. package/dist/core/command/slash-handler.js +101 -53
  91. package/dist/core/command/topic-slash.js +16 -5
  92. package/dist/core/daemon-bootstrap.js +7 -1
  93. package/dist/core/daemon-control.js +19 -1
  94. package/dist/core/daemon-lifecycle.js +2 -1
  95. package/dist/core/event-catalog.js +2 -0
  96. package/dist/core/evolagent-registry.js +13 -2
  97. package/dist/core/evolagent.js +3 -6
  98. package/dist/core/handoff/ipc-registration.js +2 -2
  99. package/dist/core/handoff/runtime.js +40 -6
  100. package/dist/core/handoff/store.js +1 -0
  101. package/dist/core/inference/text-inference.js +95 -6
  102. package/dist/core/ipc-registration.js +43 -24
  103. package/dist/core/memory/consolidation-context.js +4 -1
  104. package/dist/core/memory/consolidation.js +25 -14
  105. package/dist/core/message/aun-ipc-sender.js +296 -88
  106. package/dist/core/message/aun-request-registry.js +226 -0
  107. package/dist/core/message/create-status.js +33 -4
  108. package/dist/core/message/message-bridge.js +62 -13
  109. package/dist/core/message/message-log.js +0 -1
  110. package/dist/core/message/message-queue.js +4 -1
  111. package/dist/core/message/message-utils.js +8 -1
  112. package/dist/core/message/peer-mode.js +17 -9
  113. package/dist/core/message/response-engine.js +266 -124
  114. package/dist/core/message/retry-scheduler.js +11 -8
  115. package/dist/core/model/catalog-service.js +60 -18
  116. package/dist/core/model/model-catalog.js +112 -9
  117. package/dist/core/model/model-fallback.js +94 -26
  118. package/dist/core/ownership/owner-resolver.js +687 -0
  119. package/dist/core/permission/approval-gateway.js +196 -62
  120. package/dist/core/permission/ec-command-parser.js +138 -25
  121. package/dist/core/permission/filesystem-approval-scope.js +141 -0
  122. package/dist/core/permission/protected-paths.js +14 -1
  123. package/dist/core/permission/sandbox-runtime.js +171 -18
  124. package/dist/core/permission/tool-policy.js +13 -2
  125. package/dist/core/role/runtime-policy.js +3 -4
  126. package/dist/core/session/session-idle-monitor.js +10 -8
  127. package/dist/core/session/session-manager.js +72 -45
  128. package/dist/core/session/session-mapper.js +18 -7
  129. package/dist/core/session/session-renew.js +5 -4
  130. package/dist/core/session/session-title.js +2 -1
  131. package/dist/core/session/session-turn-coordinator.js +4 -2
  132. package/dist/core/skill/index-generator.js +2 -16
  133. package/dist/core/task/task-recovery-service.js +15 -2
  134. package/dist/core/thread/command-parser.js +28 -13
  135. package/dist/core/thread/index.js +1 -0
  136. package/dist/core/thread/ipc-operation.js +11 -8
  137. package/dist/core/thread/thread-operation.js +79 -44
  138. package/dist/core/thread/thread-route.js +54 -6
  139. package/dist/core/thread/thread-service.js +236 -45
  140. package/dist/core/thread/topic-id.js +248 -0
  141. package/dist/eck/kit-renderer.js +1 -1
  142. package/dist/eck/message-renderer.js +12 -2
  143. package/dist/index.js +50 -27
  144. package/dist/ipc.js +59 -6
  145. package/dist/paths.js +1 -0
  146. package/dist/product.js +1 -0
  147. package/dist/response-system/engines/v1/proactive-flow.js +10 -3
  148. package/dist/stats/db.js +8 -0
  149. package/dist/stats/writer.js +26 -4
  150. package/dist/trigger/authorization.js +48 -3
  151. package/dist/trigger/feedback.js +28 -10
  152. package/dist/trigger/ipc-service.js +86 -10
  153. package/dist/trigger/local-files.js +55 -23
  154. package/dist/trigger/scheduler.js +57 -11
  155. package/dist/trigger/system-definitions.js +1 -0
  156. package/dist/trigger/validation.js +44 -15
  157. package/dist/utils/aid-bind.js +17 -297
  158. package/dist/utils/avatar-upload.js +16 -4
  159. package/dist/utils/cross-platform.js +115 -36
  160. package/dist/utils/ecweb-supervisor.js +1 -1
  161. package/dist/utils/ecweb-utils.js +8 -1
  162. package/dist/utils/error-utils.js +44 -6
  163. package/dist/utils/instance-registry.js +12 -2
  164. package/dist/utils/npm-ops.js +102 -27
  165. package/dist/utils/package-version-info.js +72 -27
  166. package/dist/utils/restart-safety.js +50 -0
  167. package/dist/utils/upgrade-jobs.js +304 -0
  168. package/dist/utils/windows-autostart.js +17 -10
  169. package/kits/docs/INDEX.md +5 -0
  170. package/kits/docs/aun/ABILITY_CALLING.md +239 -0
  171. package/kits/docs/evolcore/INDEX.md +14 -3
  172. package/kits/docs/evolcore/agent-create.md +55 -141
  173. package/kits/docs/evolcore/agent.md +38 -16
  174. package/kits/docs/evolcore/aid.md +34 -0
  175. package/kits/docs/evolcore/config.md +20 -1
  176. package/kits/docs/evolcore/event.md +2 -2
  177. package/kits/docs/evolcore/fs.md +18 -2
  178. package/kits/docs/evolcore/group-collaboration.md +13 -246
  179. package/kits/docs/evolcore/group.md +2 -1
  180. package/kits/docs/evolcore/memory-consolidation-batches.md +1 -1
  181. package/kits/docs/evolcore/menu-protocol.md +65 -1
  182. package/kits/docs/evolcore/msg.md +64 -2
  183. package/kits/docs/evolcore/skill.md +86 -0
  184. package/kits/docs/evolcore/stats.md +6 -0
  185. package/kits/docs/evolcore/task.md +2 -0
  186. package/kits/docs/evolcore/thread-chain.md +145 -51
  187. package/kits/docs/evolcore/trigger.md +48 -3
  188. package/kits/eck_manifest.json +21 -0
  189. package/kits/eck_message_manifest.json +4 -4
  190. package/kits/migrations/migrate-role-config-v5.mjs +20 -7
  191. package/kits/rules/01-overview.md +2 -1
  192. package/kits/rules/03-identity.md +3 -1
  193. package/kits/rules/04-relation.md +1 -1
  194. package/kits/rules/07-memory-and-thread.md +1 -1
  195. package/kits/schemas/_meta.json +16 -1
  196. package/kits/schemas/agent-config.schema.13.json +44 -2
  197. package/kits/schemas/contact-book.schema.3.json +5 -0
  198. package/kits/schemas/daemon.schema.6.json +3 -1
  199. package/kits/schemas/daemon.schema.8.json +329 -0
  200. package/kits/schemas/defaults.schema.5.json +39 -3
  201. package/kits/schemas/providers.schema.1.json +76 -0
  202. package/kits/schemas/relation-config.schema.9.json +5 -1
  203. package/kits/skills/.switches.json +28 -0
  204. package/kits/skills/aid-ability/SKILL.md +214 -0
  205. package/kits/skills/ec-agent-create/SKILL.md +162 -0
  206. package/kits/skills/ec-group/SKILL.md +2 -1
  207. package/kits/skills/ec-group-collaboration/SKILL.md +228 -0
  208. package/kits/skills/ec-identity/SKILL.md +143 -0
  209. package/kits/skills/ec-skill/scripts/cli.mjs +36 -3
  210. package/kits/skills/ec-skill/scripts/skill.mjs +202 -15
  211. package/kits/skills/ec-trigger/SKILL.md +167 -0
  212. package/kits/skills/ec-trigger/references/examples.md +153 -0
  213. package/kits/skills/thread-ops/SKILL.md +108 -420
  214. package/kits/templates/message-fragments/thread-context.md +1 -2
  215. package/kits/templates/message-fragments/trigger-thread-check.md +1 -1
  216. package/kits/templates/roles/member.json +1 -1
  217. package/kits/templates/roles/visitor.json +1 -0
  218. package/kits/templates/system-fragments/bootstrap.md +1 -0
  219. package/kits/templates/system-fragments/channel.md +2 -1
  220. package/kits/templates/system-fragments/commands.md +5 -4
  221. package/kits/templates/system-fragments/identity.md +2 -0
  222. package/kits/templates/system-fragments/session.md +1 -1
  223. package/kits/templates/system-fragments/venue.md +1 -1
  224. package/package.json +4 -3
@@ -10,6 +10,23 @@ import { checkDangerousCommand } from './tool-policy.js';
10
10
  import { resolveProtectedCandidate } from './protected-paths.js';
11
11
  import { auditPermissionDecision, auditPermissionLifecycle } from '../auth/authorization-audit.js';
12
12
  import { ConfigTarget, onConfigWrite } from '../../config/config-manager.js';
13
+ import { fileChangeApprovalScope, permissionApprovalScope, filesystemScopeContains, } from './filesystem-approval-scope.js';
14
+ export function parsePermissionApprovalDecision(value) {
15
+ if (value === 'approve_once')
16
+ return 'allow';
17
+ if (value === 'approve_session_30m')
18
+ return 'always';
19
+ if (value === 'approve_file_60m')
20
+ return 'file_60m';
21
+ if (value === 'approve_directory_60m')
22
+ return 'directory_60m';
23
+ if (value === 'approve_all_directories_60m')
24
+ return 'all_directories_60m';
25
+ if (['allow', 'always', 'deny', 'file_60m', 'directory_60m', 'all_directories_60m'].includes(value)) {
26
+ return value;
27
+ }
28
+ return undefined;
29
+ }
13
30
  export async function requestDangerousCommandPermission(gateway, sessionId, toolName, input, sendPrompt, context, grantScope = 'default', mode = 'request') {
14
31
  const dangerCheck = checkDangerousCommand(toolName, input);
15
32
  if (!dangerCheck.isDangerous) {
@@ -40,6 +57,7 @@ export async function requestDangerousCommandPermission(gateway, sessionId, tool
40
57
  return { matched: true, decision };
41
58
  }
42
59
  const SESSION_GRANT_TTL_MS = 30 * 60 * 1000;
60
+ const FILESYSTEM_GRANT_TTL_MS = 60 * 60 * 1000;
43
61
  const LOCAL_APPROVAL_TTL_MS = 20 * 60 * 1000;
44
62
  const MANAGEMENT_APPROVAL_REVALIDATION_INTERVAL_MS = 5_000;
45
63
  const APPROVAL_DETAIL_LIMIT = 800;
@@ -98,11 +116,19 @@ function collectFileChangeGrantPaths(value, output) {
98
116
  }
99
117
  function permissionGrantMatch(toolName, input, options) {
100
118
  const fileChangeCwd = options?.fileChangeCwd?.trim();
119
+ const fileOnly = !['permissions', 'additionalPermissions', 'networkApprovalContext', 'proposedNetworkPolicyAmendments']
120
+ .some(key => input[key] !== undefined);
121
+ const rawPaths = [];
122
+ if (toolName === 'FileChange')
123
+ collectFileChangeGrantPaths(input.fileChanges, rawPaths);
124
+ const filesystem = toolName === 'FileChange' && fileChangeCwd && fileOnly
125
+ ? fileChangeApprovalScope(rawPaths, fileChangeCwd, input.grantRoot)
126
+ : toolName === 'PermissionGrant' && options?.filesystemCwd && options.workspacePath
127
+ ? permissionApprovalScope(input, options.filesystemCwd, options.workspacePath)
128
+ : undefined;
101
129
  // grantRoot is already a broad filesystem capability. Keep it exact rather
102
130
  // than silently widening it through the concrete-file convenience scope.
103
- if (toolName === 'FileChange' && fileChangeCwd && !input.grantRoot) {
104
- const rawPaths = [];
105
- collectFileChangeGrantPaths(input.fileChanges, rawPaths);
131
+ if (toolName === 'FileChange' && fileChangeCwd && fileOnly && !input.grantRoot) {
106
132
  try {
107
133
  const canonicalPaths = [...new Set(rawPaths.map(candidate => {
108
134
  const canonical = resolveProtectedCandidate(candidate, fileChangeCwd);
@@ -115,6 +141,7 @@ function permissionGrantMatch(toolName, input, options) {
115
141
  paths: canonicalPaths,
116
142
  }),
117
143
  scope: 'file-change-paths',
144
+ filesystem,
118
145
  };
119
146
  }
120
147
  }
@@ -126,8 +153,73 @@ function permissionGrantMatch(toolName, input, options) {
126
153
  return {
127
154
  inputFingerprint: permissionInputFingerprint(input),
128
155
  scope: 'exact-operation',
156
+ filesystem,
129
157
  };
130
158
  }
159
+ function permissionGrantContextKey(context, approverPolicy) {
160
+ return permissionInputFingerprint({
161
+ selfAid: context.selfAid ?? context.approvalRouting?.selfAid,
162
+ userId: context.userId,
163
+ principalId: context.actor?.principalId,
164
+ channelKey: context.actor?.operatorChannelKey ?? context.channel,
165
+ role: context.role,
166
+ permissionMode: context.permissionMode,
167
+ originPrincipalId: context.approvalRouting?.originPrincipalId,
168
+ originAuthRevision: context.approvalRouting?.originAuthRevision,
169
+ approverPolicy,
170
+ });
171
+ }
172
+ function approvalButtons(match, route, context) {
173
+ const handoff = route.kind === 'handoff';
174
+ const buttons = [
175
+ { key: handoff ? 'approve_once' : 'allow', label: handoff ? '批准本次' : '✓ 允许本次', style: 'primary' },
176
+ ];
177
+ const offeredDecisions = ['allow', 'deny'];
178
+ if (match.scope === 'file-change-paths' && match.filesystem) {
179
+ buttons.push({ key: 'approve_file_60m', label: '同文件 60 分钟', style: 'default' });
180
+ offeredDecisions.push('file_60m');
181
+ }
182
+ else {
183
+ buttons.push({ key: handoff ? 'approve_session_30m' : 'always', label: handoff ? '本会话 30 分钟' : '同操作 30 分钟', style: 'default' });
184
+ offeredDecisions.push('always');
185
+ }
186
+ const scope = match.filesystem;
187
+ if (scope?.directory) {
188
+ buttons.push({ key: 'approve_directory_60m', label: '当前目录 60 分钟', style: 'default' });
189
+ offeredDecisions.push('directory_60m');
190
+ }
191
+ // A broad directory grant must be issued by an actual owner, including an
192
+ // owner answering a manager handoff. Never offer it to an admin/requester.
193
+ const agentAndHostOwner = !!route.approverPrincipalId
194
+ && context.approvalRouting?.owners?.includes(route.approverPrincipalId) === true
195
+ && context.approvalRouting.hostOwners?.includes(route.approverPrincipalId) === true;
196
+ const owner = !!route.approverPrincipalId && !!route.approverChannelKey
197
+ && !!context.approvalRouting?.reauthorizeApproval
198
+ && agentAndHostOwner;
199
+ if (scope && owner) {
200
+ buttons.push({ key: 'approve_all_directories_60m', label: '所有普通目录 60 分钟', style: 'default' });
201
+ offeredDecisions.push('all_directories_60m');
202
+ }
203
+ // Without matching Agent and host owners, keep the filesystem choices to
204
+ // the one-shot, per-file, and deny actions. Recursive directory grants are
205
+ // deliberately withheld from an owner that is not also the host Owner.
206
+ if (scope && !owner) {
207
+ const filtered = buttons.filter(button => button.key !== 'approve_directory_60m');
208
+ buttons.length = 0;
209
+ buttons.push(...filtered);
210
+ const filteredDecisions = offeredDecisions.filter(decision => decision !== 'directory_60m');
211
+ offeredDecisions.length = 0;
212
+ offeredDecisions.push(...filteredDecisions);
213
+ }
214
+ buttons.push({ key: 'deny', label: '✗ 拒绝', style: 'danger' });
215
+ const detail = scope ? [
216
+ ...(scope.directory ? [`目录授权范围(含子目录):${scope.directory}`] : []),
217
+ ...(owner ? ['“所有普通目录 60 分钟”覆盖当前会话内所有非受保护目录。'] : []),
218
+ `临时文件权限:${scope.access === 'write' ? '读写' : '只读'};仅当前会话和权限上下文有效。`,
219
+ '受保护路径和其他会话临时目录仍按现有策略校验;不授权 Shell 命令、网络或系统管理权限。',
220
+ ].join('\n') : '';
221
+ return { buttons, offeredDecisions, detail };
222
+ }
131
223
  function truncateApprovalDetail(value) {
132
224
  const trimmed = value.trim();
133
225
  return trimmed.length > APPROVAL_DETAIL_LIMIT
@@ -372,20 +464,60 @@ export class PermissionGateway {
372
464
  }
373
465
  hasTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default', matchOptions) {
374
466
  const match = permissionGrantMatch(toolName, toolInput, matchOptions);
375
- return !!this.getTemporaryGrant(sessionId, toolName, match.inputFingerprint, grantScope);
467
+ return !!this.getTemporaryGrant(sessionId, toolName, match, grantScope);
376
468
  }
377
- getTemporaryGrant(sessionId, toolName, inputFingerprint, grantScope = 'default') {
469
+ getTemporaryGrant(sessionId, toolName, match, grantScope = 'default', grantContextKey) {
378
470
  const now = Date.now();
379
471
  this.pruneTemporaryGrants(now);
380
- const key = this.temporaryGrantKey(sessionId, toolName, inputFingerprint, grantScope);
381
- return this.temporaryGrants.get(key);
472
+ for (const [key, grant] of this.temporaryGrants) {
473
+ if (grant.sessionId !== sessionId || grant.toolName !== toolName || grant.grantScope !== grantScope
474
+ || (grantContextKey !== undefined && grant.grantContextKey !== grantContextKey))
475
+ continue;
476
+ const target = grant.target;
477
+ const matches = target.kind === 'operation'
478
+ ? target.inputFingerprint === match.inputFingerprint
479
+ : !!match.filesystem && filesystemScopeContains(target, match.filesystem);
480
+ if (!matches)
481
+ continue;
482
+ const authorization = this.authorizePendingResponse(grant.authorization, grant.authorization.approverOperatorId, grant.authorization.approverChannelKey);
483
+ if (authorization.status !== 'authorized') {
484
+ this.temporaryGrants.delete(key);
485
+ continue;
486
+ }
487
+ return grant;
488
+ }
489
+ return undefined;
382
490
  }
383
- addTemporaryGrant(pending, causation) {
491
+ addTemporaryGrant(pending, decision, causation) {
384
492
  const now = Date.now();
385
493
  this.pruneTemporaryGrants(now);
386
- const expiresAt = now + SESSION_GRANT_TTL_MS;
387
- const key = this.temporaryGrantKey(pending.sessionId, pending.toolName, pending.inputFingerprint, pending.grantScope);
388
- this.temporaryGrants.set(key, { expiresAt, causation: normalizeCausation(causation) });
494
+ const filesystem = pending.grantMatch.filesystem;
495
+ const target = decision === 'directory_60m' && filesystem?.directory
496
+ ? { kind: 'directory', directory: filesystem.directory, access: filesystem.access }
497
+ : decision === 'all_directories_60m' && filesystem
498
+ ? { kind: 'all-directories', access: filesystem.access }
499
+ : { kind: 'operation', inputFingerprint: pending.inputFingerprint };
500
+ const expiresAt = now + (decision === 'always' ? SESSION_GRANT_TTL_MS : FILESYSTEM_GRANT_TTL_MS);
501
+ const fingerprint = target.kind === 'operation'
502
+ ? target.inputFingerprint : permissionInputFingerprint(target);
503
+ const key = this.temporaryGrantKey(pending.sessionId, pending.toolName, fingerprint, pending.grantScope);
504
+ const authorization = {
505
+ approverPolicy: decision === 'all_directories_60m' ? 'agent_owner' : pending.approverPolicy,
506
+ requiredApproverRole: decision === 'all_directories_60m' ? 'owner' : pending.requiredApproverRole,
507
+ approverOperatorId: pending.approverOperatorId,
508
+ approverChannelKey: pending.approverChannelKey,
509
+ approverPrincipalId: pending.approverPrincipalId,
510
+ selfAid: pending.selfAid,
511
+ originPrincipalId: pending.originPrincipalId,
512
+ originAuthRevision: pending.originAuthRevision,
513
+ reauthorizeApproval: pending.reauthorizeApproval,
514
+ requireHostOwner: decision === 'all_directories_60m',
515
+ };
516
+ this.temporaryGrants.set(key, {
517
+ sessionId: pending.sessionId, toolName: pending.toolName, grantScope: pending.grantScope,
518
+ grantContextKey: pending.grantContextKey, authorization, target,
519
+ expiresAt, causation: normalizeCausation(causation),
520
+ });
389
521
  return expiresAt;
390
522
  }
391
523
  authorizePendingResponse(pending, operatorId, channelKey) {
@@ -455,6 +587,9 @@ export class PermissionGateway {
455
587
  reason: stillAssigned ? 'approver_role_changed' : 'approver_revoked',
456
588
  };
457
589
  }
590
+ if (pending.requireHostOwner && !result.hostOwners?.includes(actor.principalId)) {
591
+ return { status: 'revoked', reason: 'approver_not_host_owner' };
592
+ }
458
593
  if (pending.originPrincipalId !== undefined
459
594
  && result.originPrincipalId !== pending.originPrincipalId) {
460
595
  return { status: 'revoked', reason: 'origin_authorization_changed' };
@@ -465,6 +600,20 @@ export class PermissionGateway {
465
600
  }
466
601
  return { status: 'authorized' };
467
602
  }
603
+ pendingDecision(pending, action) {
604
+ const decision = parsePermissionApprovalDecision(action);
605
+ if (!decision)
606
+ return undefined;
607
+ // Old /perm always links keep their historical 30-minute exact-file grant.
608
+ if (decision === 'always' && pending.offeredDecisions.includes('file_60m'))
609
+ return decision;
610
+ return pending.offeredDecisions.includes(decision) ? decision : undefined;
611
+ }
612
+ decisionAuthorization(pending, decision) {
613
+ return decision === 'all_directories_60m'
614
+ ? { ...pending, requiredApproverRole: 'owner', approverPolicy: 'agent_owner', requireHostOwner: true }
615
+ : pending;
616
+ }
468
617
  /** Legacy diagnostics alias. Grants are no longer tool-wide or permanent. */
469
618
  getAlwaysAllowList() {
470
619
  this.pruneTemporaryGrants(Date.now());
@@ -474,7 +623,10 @@ export class PermissionGateway {
474
623
  const pending = this.crossSessionPending.get(requestId);
475
624
  if (!pending || pending.sessionId !== sessionId)
476
625
  return false;
477
- const authorization = this.authorizePendingResponse(pending, operatorId, channelKey);
626
+ const selectedDecision = this.pendingDecision(pending, action);
627
+ if (!selectedDecision)
628
+ return false;
629
+ const authorization = this.authorizePendingResponse(this.decisionAuthorization(pending, selectedDecision), operatorId, channelKey);
478
630
  if (authorization.status === 'identity_mismatch')
479
631
  return false;
480
632
  if (authorization.status === 'revoked') {
@@ -486,12 +638,12 @@ export class PermissionGateway {
486
638
  pending.interactionRouter?.cancel(requestId);
487
639
  this.crossSessionPending.delete(requestId);
488
640
  this.syncManagementRevalidationTimer();
489
- const sessionGrant = action === 'approve_session_30m';
490
- const approved = action === 'approve_once' || sessionGrant || action === 'allow';
641
+ const sessionGrant = selectedDecision !== 'allow' && selectedDecision !== 'deny';
642
+ const approved = selectedDecision !== 'deny';
491
643
  const decision = approved ? 'allow' : 'deny';
492
644
  const decisionCausation = deriveCausation(pending.causation);
493
645
  if (sessionGrant)
494
- this.addTemporaryGrant(pending, decisionCausation);
646
+ this.addTemporaryGrant(pending, selectedDecision, decisionCausation);
495
647
  pending.resolve(decision);
496
648
  auditPermissionLifecycle({
497
649
  phase: 'response',
@@ -695,6 +847,7 @@ export class PermissionGateway {
695
847
  : undefined;
696
848
  const approvalDeadline = new Date(expiresAt).toLocaleString('zh-CN', { hour12: false });
697
849
  const target = formatApprovalTarget(toolName, toolInput);
850
+ const choices = approvalButtons(grantMatch, route, context);
698
851
  const body = [
699
852
  '**申请信息**',
700
853
  '',
@@ -715,6 +868,7 @@ export class PermissionGateway {
715
868
  '',
716
869
  '- **风险:中** · 跨会话临时授权,请核对目标和参数',
717
870
  `- **审批截止**:${escapeApprovalMarkdown(approvalDeadline)}(约 ${Math.round(ttlMs / 60000)} 分钟)`,
871
+ ...(choices.detail ? ['', approvalCodeBlock(choices.detail)] : []),
718
872
  ].join('\n');
719
873
  const interaction = {
720
874
  type: 'interaction',
@@ -724,15 +878,7 @@ export class PermissionGateway {
724
878
  title: '临时授权申请',
725
879
  body,
726
880
  bodyFormat: 'markdown',
727
- buttons: [
728
- { key: 'approve_once', label: '批准本次', style: 'primary' },
729
- {
730
- key: 'approve_session_30m',
731
- label: grantMatch.scope === 'file-change-paths' ? '同文件 30 分钟' : '本会话 30 分钟',
732
- style: 'default',
733
- },
734
- { key: 'deny', label: '拒绝', style: 'danger' },
735
- ],
881
+ buttons: choices.buttons,
736
882
  },
737
883
  channelId: route.approverOperatorId,
738
884
  sessionId,
@@ -740,11 +886,9 @@ export class PermissionGateway {
740
886
  expiresAt,
741
887
  fallback: {
742
888
  command: 'perm',
743
- buttonArgMap: {
744
- approve_once: `${requestId} allow`,
745
- approve_session_30m: `${requestId} always`,
746
- deny: `${requestId} deny`,
747
- },
889
+ buttonArgMap: Object.fromEntries(choices.buttons.map(button => [
890
+ button.key, `${requestId} ${parsePermissionApprovalDecision(button.key)}`,
891
+ ])),
748
892
  },
749
893
  };
750
894
  // Register before delivery. Some adapters can surface a user response as
@@ -760,6 +904,9 @@ export class PermissionGateway {
760
904
  resolve,
761
905
  inputFingerprint: grantMatch.inputFingerprint,
762
906
  grantScope,
907
+ grantMatch,
908
+ grantContextKey: permissionGrantContextKey(context, challenge.approverPolicy),
909
+ offeredDecisions: choices.offeredDecisions,
763
910
  approverPolicy: challenge.approverPolicy,
764
911
  approvalRouteKind: route.kind,
765
912
  requiredApproverRole: requiredApproverRole(challenge.approverPolicy),
@@ -999,7 +1146,8 @@ export class PermissionGateway {
999
1146
  return 'deny';
1000
1147
  }
1001
1148
  const grantMatch = permissionGrantMatch(toolName, toolInput, matchOptions);
1002
- const temporaryGrant = this.getTemporaryGrant(sessionId, toolName, grantMatch.inputFingerprint, grantScope);
1149
+ const grantContextKey = permissionGrantContextKey(context, effectiveApproverPolicy);
1150
+ const temporaryGrant = this.getTemporaryGrant(sessionId, toolName, grantMatch, grantScope, grantContextKey);
1003
1151
  if (temporaryGrant) {
1004
1152
  const consumeCausation = deriveCausation(normalizeCausation(temporaryGrant.causation)
1005
1153
  ?? normalizeCausation(context?.causation)
@@ -1007,7 +1155,7 @@ export class PermissionGateway {
1007
1155
  recordCausationSpan(consumeCausation, 'permission.consume', {
1008
1156
  status: 'completed',
1009
1157
  refs: { taskId: context?.taskId, sessionId },
1010
- reason: 'temporary_grant',
1158
+ reason: `temporary_grant:${temporaryGrant.target.kind}:expires=${temporaryGrant.expiresAt}`,
1011
1159
  });
1012
1160
  return 'allow';
1013
1161
  }
@@ -1107,6 +1255,7 @@ export class PermissionGateway {
1107
1255
  if (route.kind === 'handoff') {
1108
1256
  return this.requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation, grantMatch);
1109
1257
  }
1258
+ const choices = approvalButtons(grantMatch, route, context);
1110
1259
  // 构造 ActionInteraction
1111
1260
  const interaction = {
1112
1261
  type: 'interaction',
@@ -1114,16 +1263,8 @@ export class PermissionGateway {
1114
1263
  kind: {
1115
1264
  kind: 'action',
1116
1265
  title: '🔐 权限请求',
1117
- body: `工具:${toolName}\n操作:${displaySummary}${reasonLine}`,
1118
- buttons: [
1119
- { key: 'allow', label: '✅ 允许本次', style: 'primary' },
1120
- {
1121
- key: 'always',
1122
- label: grantMatch.scope === 'file-change-paths' ? '⏱ 同文件 30 分钟' : '⏱ 同操作 30 分钟',
1123
- style: 'default',
1124
- },
1125
- { key: 'deny', label: '❌ 拒绝', style: 'danger' },
1126
- ],
1266
+ body: `工具:${toolName}\n操作:${displaySummary}${reasonLine}${choices.detail ? `\n\n${choices.detail}` : ''}`,
1267
+ buttons: choices.buttons,
1127
1268
  },
1128
1269
  channelId: context?.channelId || '',
1129
1270
  sessionId,
@@ -1131,11 +1272,9 @@ export class PermissionGateway {
1131
1272
  expiresAt: Date.now() + LOCAL_APPROVAL_TTL_MS,
1132
1273
  fallback: {
1133
1274
  command: 'perm',
1134
- buttonArgMap: {
1135
- allow: `${requestId} allow`,
1136
- always: `${requestId} always`,
1137
- deny: `${requestId} deny`,
1138
- },
1275
+ buttonArgMap: Object.fromEntries(choices.buttons.map(button => [
1276
+ button.key, `${requestId} ${parsePermissionApprovalDecision(button.key)}`,
1277
+ ])),
1139
1278
  },
1140
1279
  };
1141
1280
  // Register before delivery so an immediate card/text response cannot beat
@@ -1147,6 +1286,9 @@ export class PermissionGateway {
1147
1286
  toolName,
1148
1287
  inputFingerprint: grantMatch.inputFingerprint,
1149
1288
  grantScope,
1289
+ grantMatch,
1290
+ grantContextKey,
1291
+ offeredDecisions: choices.offeredDecisions,
1150
1292
  approverPolicy: challenge.approverPolicy,
1151
1293
  approvalRouteKind: route.kind,
1152
1294
  requiredApproverRole: requiredApproverRole(challenge.approverPolicy),
@@ -1222,10 +1364,7 @@ export class PermissionGateway {
1222
1364
  replyContext: context.replyContext,
1223
1365
  causation: requestCausation,
1224
1366
  });
1225
- const temporaryGrantLabel = grantMatch.scope === 'file-change-paths'
1226
- ? '同文件授权 30 分钟'
1227
- : '同操作授权 30 分钟';
1228
- const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm ${requestId} allow 允许本次 / /perm ${requestId} always ${temporaryGrantLabel} / /perm ${requestId} deny 拒绝`;
1367
+ const fallbackText = renderActionAsText(interaction);
1229
1368
  const result = await sendInteractionPayload(context.adapter, envelope, interaction, fallbackText, context.replyContext);
1230
1369
  interactionSent = isInteractionSendAccepted(result);
1231
1370
  }
@@ -1254,7 +1393,10 @@ export class PermissionGateway {
1254
1393
  const pending = this.pending.get(requestId);
1255
1394
  if (!pending || pending.sessionId !== sessionId)
1256
1395
  return false;
1257
- const authorization = this.authorizePendingResponse(pending, operatorId, channelKey);
1396
+ const normalizedDecision = this.pendingDecision(pending, decision);
1397
+ if (!normalizedDecision)
1398
+ return false;
1399
+ const authorization = this.authorizePendingResponse(this.decisionAuthorization(pending, normalizedDecision), operatorId, channelKey);
1258
1400
  if (authorization.status === 'identity_mismatch')
1259
1401
  return false;
1260
1402
  if (authorization.status === 'revoked') {
@@ -1262,9 +1404,6 @@ export class PermissionGateway {
1262
1404
  this.failPendingPermission(sessionId, requestId, authorization.reason);
1263
1405
  return false;
1264
1406
  }
1265
- const normalizedDecision = decision === 'allow' || decision === 'always'
1266
- ? decision
1267
- : 'deny';
1268
1407
  // The backend receives a one-shot allow. Future requests must return
1269
1408
  // through this gateway so the selected match scope and TTL are checked.
1270
1409
  this.clearPendingResources(pending);
@@ -1307,8 +1446,8 @@ export class PermissionGateway {
1307
1446
  response: normalizedDecision,
1308
1447
  });
1309
1448
  const decisionCausation = deriveCausation(pending.causation);
1310
- if (normalizedDecision === 'always') {
1311
- this.addTemporaryGrant(pending, decisionCausation);
1449
+ if (normalizedDecision !== 'allow' && normalizedDecision !== 'deny') {
1450
+ this.addTemporaryGrant(pending, normalizedDecision, decisionCausation);
1312
1451
  }
1313
1452
  recordCausationSpan(decisionCausation, 'permission.decision', {
1314
1453
  status: normalizedDecision === 'deny' ? 'failed' : 'completed',
@@ -1333,12 +1472,7 @@ export class PermissionGateway {
1333
1472
  const crossSession = this.crossSessionPending.get(requestId);
1334
1473
  if (!crossSession)
1335
1474
  return false;
1336
- const action = decision === 'always'
1337
- ? 'approve_session_30m'
1338
- : decision === 'allow'
1339
- ? 'approve_once'
1340
- : 'deny';
1341
- return this.resolveCrossSessionPermission(crossSession.sessionId, requestId, action, undefined, operatorId, channelKey);
1475
+ return this.resolveCrossSessionPermission(crossSession.sessionId, requestId, decision, undefined, operatorId, channelKey);
1342
1476
  }
1343
1477
  /**
1344
1478
  * Proactively cancel management approvals after an identity or Agent config