evolcore 0.0.13 → 0.0.15

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 (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/bin/codex-managed-hook.mjs +4 -1
  3. package/bin/install-codex-managed-hooks.mjs +201 -0
  4. package/dist/agents/claude-runner.js +53 -5
  5. package/dist/agents/codex-app-server-client.js +123 -2
  6. package/dist/agents/codex-runner.js +152 -30
  7. package/dist/agents/ecagent-runner.js +17 -1
  8. package/dist/agents/gemini-runner.js +9 -4
  9. package/dist/aun/msg/managed-operation.js +63 -3
  10. package/dist/channels/aun.js +144 -15
  11. package/dist/channels/daemon.js +2 -0
  12. package/dist/channels/feishu.js +6 -1
  13. package/dist/cli/aun-commands.js +1 -1
  14. package/dist/cli/fs-command.js +46 -9
  15. package/dist/cli/task-context.js +176 -0
  16. package/dist/config/builtin-roles.js +2 -0
  17. package/dist/config/config-manager.js +6 -2
  18. package/dist/config/contact-book-store.js +7 -2
  19. package/dist/core/auth/auth-gateway.js +1 -0
  20. package/dist/core/auth/authorization-audit.js +32 -0
  21. package/dist/core/auth/operation-catalog.js +3 -3
  22. package/dist/core/bootstrap-service.js +7 -1
  23. package/dist/core/command/command-handler.js +3 -0
  24. package/dist/core/command/slash-handler.js +1 -1
  25. package/dist/core/event-catalog.js +2 -0
  26. package/dist/core/message/im-renderer.js +15 -1
  27. package/dist/core/message/message-bridge.js +5 -2
  28. package/dist/core/message/response-engine.js +138 -10
  29. package/dist/core/permission/approval-gateway.js +99 -16
  30. package/dist/core/permission/ec-command-parser.js +556 -4
  31. package/dist/core/permission/tool-policy.js +17 -29
  32. package/dist/core/runtime-lock.js +101 -0
  33. package/dist/index.js +30 -3
  34. package/dist/response-system/engines/v1/proactive-flow.js +92 -8
  35. package/dist/response-system/modes/single-session/index.js +3 -0
  36. package/dist/trigger/history.js +42 -7
  37. package/dist/utils/error-utils.js +7 -0
  38. package/dist/utils/logger.js +37 -4
  39. package/kits/templates/roles/admin.json +2 -0
  40. package/kits/templates/roles/member.json +1 -0
  41. package/package.json +1 -1
@@ -7,6 +7,7 @@ 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
11
  export async function requestDangerousCommandPermission(gateway, sessionId, toolName, input, sendPrompt, context, grantScope = 'default', mode = 'request') {
11
12
  const dangerCheck = checkDangerousCommand(toolName, input);
12
13
  if (!dangerCheck.isDangerous) {
@@ -41,6 +42,76 @@ function stablePermissionInput(value) {
41
42
  function permissionInputFingerprint(input) {
42
43
  return createHash('sha256').update(stablePermissionInput(input)).digest('hex');
43
44
  }
45
+ function collectExplicitFileChangeGrantPaths(record, output) {
46
+ const pathKeys = ['path', 'filePath', 'file_path', 'movePath', 'move_path', 'destinationPath', 'targetPath'];
47
+ const hasExplicitPath = pathKeys.some(key => typeof record[key] === 'string' && !!record[key]);
48
+ for (const key of pathKeys) {
49
+ const candidate = record[key];
50
+ if (typeof candidate === 'string' && candidate)
51
+ output.push(candidate);
52
+ }
53
+ if (record.kind && typeof record.kind === 'object' && !Array.isArray(record.kind)) {
54
+ collectExplicitFileChangeGrantPaths(record.kind, output);
55
+ }
56
+ return hasExplicitPath;
57
+ }
58
+ function collectFileChangeGrantPaths(value, output) {
59
+ if (Array.isArray(value)) {
60
+ for (const entry of value) {
61
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
62
+ collectExplicitFileChangeGrantPaths(entry, output);
63
+ }
64
+ }
65
+ return;
66
+ }
67
+ if (!value || typeof value !== 'object')
68
+ return;
69
+ const record = value;
70
+ const hasExplicitPath = collectExplicitFileChangeGrantPaths(record, output);
71
+ if (hasExplicitPath)
72
+ return;
73
+ // Some backends encode file changes as a map keyed by path. Do not treat
74
+ // metadata keys as paths, and still collect explicit move destinations.
75
+ for (const [filePath, change] of Object.entries(record)) {
76
+ if (filePath !== 'kind' && filePath !== 'type')
77
+ output.push(filePath);
78
+ if (change && typeof change === 'object' && !Array.isArray(change)) {
79
+ collectExplicitFileChangeGrantPaths(change, output);
80
+ }
81
+ }
82
+ }
83
+ function permissionGrantMatch(toolName, input, options) {
84
+ const fileChangeCwd = options?.fileChangeCwd?.trim();
85
+ // grantRoot is already a broad filesystem capability. Keep it exact rather
86
+ // than silently widening it through the concrete-file convenience scope.
87
+ if (toolName === 'FileChange' && fileChangeCwd && !input.grantRoot) {
88
+ const rawPaths = [];
89
+ collectFileChangeGrantPaths(input.fileChanges, rawPaths);
90
+ try {
91
+ const canonicalPaths = [...new Set(rawPaths.map(candidate => {
92
+ const canonical = resolveProtectedCandidate(candidate, fileChangeCwd);
93
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
94
+ }))].sort();
95
+ if (canonicalPaths.length > 0) {
96
+ return {
97
+ inputFingerprint: permissionInputFingerprint({
98
+ scope: 'file-change-paths-v1',
99
+ paths: canonicalPaths,
100
+ }),
101
+ scope: 'file-change-paths',
102
+ };
103
+ }
104
+ }
105
+ catch {
106
+ // Invalid path input falls back to exact matching; policy checks still
107
+ // decide whether the request itself may be approved.
108
+ }
109
+ }
110
+ return {
111
+ inputFingerprint: permissionInputFingerprint(input),
112
+ scope: 'exact-operation',
113
+ };
114
+ }
44
115
  function truncateApprovalDetail(value) {
45
116
  const trimmed = value.trim();
46
117
  return trimmed.length > APPROVAL_DETAIL_LIMIT
@@ -238,13 +309,14 @@ export class PermissionGateway {
238
309
  this.temporaryGrants.delete(key);
239
310
  }
240
311
  }
241
- hasTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default') {
242
- return !!this.getTemporaryGrant(sessionId, toolName, toolInput, grantScope);
312
+ hasTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default', matchOptions) {
313
+ const match = permissionGrantMatch(toolName, toolInput, matchOptions);
314
+ return !!this.getTemporaryGrant(sessionId, toolName, match.inputFingerprint, grantScope);
243
315
  }
244
- getTemporaryGrant(sessionId, toolName, toolInput, grantScope = 'default') {
316
+ getTemporaryGrant(sessionId, toolName, inputFingerprint, grantScope = 'default') {
245
317
  const now = Date.now();
246
318
  this.pruneTemporaryGrants(now);
247
- const key = this.temporaryGrantKey(sessionId, toolName, permissionInputFingerprint(toolInput), grantScope);
319
+ const key = this.temporaryGrantKey(sessionId, toolName, inputFingerprint, grantScope);
248
320
  return this.temporaryGrants.get(key);
249
321
  }
250
322
  addTemporaryGrant(pending, causation) {
@@ -425,7 +497,7 @@ export class PermissionGateway {
425
497
  }
426
498
  return true;
427
499
  }
428
- async requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation) {
500
+ async requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation, grantMatch) {
429
501
  const approval = context.approvalRouting;
430
502
  const interactionRouter = context.interactionRouter;
431
503
  if (!approval || !interactionRouter) {
@@ -480,7 +552,11 @@ export class PermissionGateway {
480
552
  bodyFormat: 'markdown',
481
553
  buttons: [
482
554
  { key: 'approve_once', label: '批准本次', style: 'primary' },
483
- { key: 'approve_session_30m', label: '本会话 30 分钟', style: 'default' },
555
+ {
556
+ key: 'approve_session_30m',
557
+ label: grantMatch.scope === 'file-change-paths' ? '同文件 30 分钟' : '本会话 30 分钟',
558
+ style: 'default',
559
+ },
484
560
  { key: 'deny', label: '拒绝', style: 'danger' },
485
561
  ],
486
562
  },
@@ -508,7 +584,7 @@ export class PermissionGateway {
508
584
  displaySummary,
509
585
  reason,
510
586
  resolve,
511
- inputFingerprint: permissionInputFingerprint(toolInput),
587
+ inputFingerprint: grantMatch.inputFingerprint,
512
588
  grantScope,
513
589
  approverPolicy: challenge.approverPolicy,
514
590
  approvalRouteKind: route.kind,
@@ -644,7 +720,7 @@ export class PermissionGateway {
644
720
  /**
645
721
  * 请求人工审批。返回三态决策。
646
722
  */
647
- async requestPermission(sessionId, toolName, toolInput, sendPrompt, context, summary, reason, grantScope = 'default', approverPolicy) {
723
+ async requestPermission(sessionId, toolName, toolInput, sendPrompt, context, summary, reason, grantScope = 'default', approverPolicy, matchOptions) {
648
724
  const effectiveApproverPolicy = approverPolicy
649
725
  ?? context?.approvalRouting?.approverPolicy
650
726
  ?? 'requester';
@@ -672,7 +748,8 @@ export class PermissionGateway {
672
748
  await sendPrompt('当前操作需要授权,但无法验证申请人身份。');
673
749
  return 'deny';
674
750
  }
675
- const temporaryGrant = this.getTemporaryGrant(sessionId, toolName, toolInput, grantScope);
751
+ const grantMatch = permissionGrantMatch(toolName, toolInput, matchOptions);
752
+ const temporaryGrant = this.getTemporaryGrant(sessionId, toolName, grantMatch.inputFingerprint, grantScope);
676
753
  if (temporaryGrant) {
677
754
  const consumeCausation = deriveCausation(normalizeCausation(temporaryGrant.causation)
678
755
  ?? normalizeCausation(context?.causation)
@@ -731,7 +808,7 @@ export class PermissionGateway {
731
808
  return 'deny';
732
809
  }
733
810
  if (route.kind === 'handoff') {
734
- return this.requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation);
811
+ return this.requestCrossSessionPermission(sessionId, challenge, route, sendPrompt, context, grantScope, requestCausation, grantMatch);
735
812
  }
736
813
  // 构造 ActionInteraction
737
814
  const interaction = {
@@ -743,7 +820,11 @@ export class PermissionGateway {
743
820
  body: `工具:${toolName}\n操作:${displaySummary}${reasonLine}`,
744
821
  buttons: [
745
822
  { key: 'allow', label: '✅ 允许本次', style: 'primary' },
746
- { key: 'always', label: '⏱ 同操作 30 分钟', style: 'default' },
823
+ {
824
+ key: 'always',
825
+ label: grantMatch.scope === 'file-change-paths' ? '⏱ 同文件 30 分钟' : '⏱ 同操作 30 分钟',
826
+ style: 'default',
827
+ },
747
828
  { key: 'deny', label: '❌ 拒绝', style: 'danger' },
748
829
  ],
749
830
  },
@@ -767,7 +848,7 @@ export class PermissionGateway {
767
848
  const pending = {
768
849
  sessionId,
769
850
  toolName,
770
- inputFingerprint: permissionInputFingerprint(toolInput),
851
+ inputFingerprint: grantMatch.inputFingerprint,
771
852
  grantScope,
772
853
  approverPolicy: challenge.approverPolicy,
773
854
  approvalRouteKind: route.kind,
@@ -839,7 +920,10 @@ export class PermissionGateway {
839
920
  replyContext: context.replyContext,
840
921
  causation: requestCausation,
841
922
  });
842
- const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm ${requestId} allow 允许本次 / /perm ${requestId} always 同操作授权 30 分钟 / /perm ${requestId} deny 拒绝`;
923
+ const temporaryGrantLabel = grantMatch.scope === 'file-change-paths'
924
+ ? '同文件授权 30 分钟'
925
+ : '同操作授权 30 分钟';
926
+ const fallbackText = `🔐 权限请求 - ${toolName}\n${displaySummary}${reasonLine}\n回复 /perm ${requestId} allow 允许本次 / /perm ${requestId} always ${temporaryGrantLabel} / /perm ${requestId} deny 拒绝`;
843
927
  const result = await sendInteractionPayload(context.adapter, envelope, interaction, fallbackText, context.replyContext);
844
928
  interactionSent = !!result;
845
929
  }
@@ -879,9 +963,8 @@ export class PermissionGateway {
879
963
  const normalizedDecision = decision === 'allow' || decision === 'always'
880
964
  ? decision
881
965
  : 'deny';
882
- // Legacy "always" now means this exact operation in this session for 30 minutes.
883
- // The backend receives a one-shot allow. Future identical operations must
884
- // return through this gateway so the exact fingerprint and TTL are checked.
966
+ // The backend receives a one-shot allow. Future requests must return
967
+ // through this gateway so the selected match scope and TTL are checked.
885
968
  this.clearPendingResources(pending);
886
969
  pending.interactionRouter?.cancel(requestId);
887
970
  pending.resolve(normalizedDecision === 'deny' ? 'deny' : 'allow');