evolcore 0.0.14 → 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.
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
本文件记录 EvolCore 的重要变更。EvolClaw 版本线的历史记录已归档至
|
|
4
4
|
[`docs/_archive/CHANGELOG-evolclaw.md`](docs/_archive/CHANGELOG-evolclaw.md)。
|
|
5
5
|
|
|
6
|
+
## 0.0.15 (2026-08-25)
|
|
7
|
+
|
|
8
|
+
### Codex 文件授权
|
|
9
|
+
|
|
10
|
+
- 文件变更的临时授权按 canonical 化后的完整目标文件集合匹配,变更内容或顺序调整不再扩大授权范围。
|
|
11
|
+
- 统一 Codex 文件授权的交互反馈,明确区分同文件临时授权与同操作临时授权,并收紧跨会话授权契约。
|
|
12
|
+
|
|
13
|
+
### 运行安全与审计
|
|
14
|
+
|
|
15
|
+
- 为 Claude、Codex 和 ecagent 的工具预检拒绝统一补充结构化授权审计,便于追踪策略、请求和会话上下文。
|
|
16
|
+
- 规范 Codex 审批命令与委派校验,拒绝无法规范化的复合命令和空字节输入,并修正 Windows 受管临时目录的私有性校验。
|
|
17
|
+
|
|
6
18
|
## 0.0.14 (2026-08-24)
|
|
7
19
|
|
|
8
20
|
### Codex 受管运行与安全
|
|
@@ -2430,6 +2430,9 @@ export class CodexRunner {
|
|
|
2430
2430
|
if (requiresSandboxExpansionApproval) {
|
|
2431
2431
|
if (!this.permissionGateway || !permissionPrompt)
|
|
2432
2432
|
return 'deny';
|
|
2433
|
+
if (toolName === 'FileChange') {
|
|
2434
|
+
return this.permissionGateway.requestPermission(sessionKey, toolName, checkedInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope, undefined, { fileChangeCwd: workspacePath });
|
|
2435
|
+
}
|
|
2433
2436
|
return this.permissionGateway.requestPermission(sessionKey, toolName, checkedInput, permissionPrompt, this.permissionContexts.get(sessionKey), summary, reason || 'Codex 请求扩大当前命令或 turn 的文件系统权限', grantScope);
|
|
2434
2437
|
}
|
|
2435
2438
|
if (mode === 'request' || mode === 'bypass') {
|
package/dist/channels/feishu.js
CHANGED
|
@@ -1467,9 +1467,14 @@ export function buildCardV2(interaction, opts) {
|
|
|
1467
1467
|
export function buildResolvedV2(interaction, response) {
|
|
1468
1468
|
const action = response.action;
|
|
1469
1469
|
const kind = interaction.kind;
|
|
1470
|
+
const temporaryGrantButton = kind.kind === 'action'
|
|
1471
|
+
? kind.buttons.find(button => button.key === 'always' || button.key === 'approve_session_30m')
|
|
1472
|
+
: undefined;
|
|
1473
|
+
const temporaryGrantIsFileScoped = temporaryGrantButton?.label.includes('同文件') === true;
|
|
1470
1474
|
const labelMap = {
|
|
1471
1475
|
'allow': '✅ 已允许',
|
|
1472
|
-
'always': '⏱ 已授权同操作 30 分钟',
|
|
1476
|
+
'always': temporaryGrantIsFileScoped ? '⏱ 已授权同文件 30 分钟' : '⏱ 已授权同操作 30 分钟',
|
|
1477
|
+
'approve_session_30m': temporaryGrantIsFileScoped ? '⏱ 已授权同文件 30 分钟' : '⏱ 已授权本会话 30 分钟',
|
|
1473
1478
|
'deny': '❌ 已拒绝',
|
|
1474
1479
|
'cancel': '取消',
|
|
1475
1480
|
};
|
package/dist/cli/task-context.js
CHANGED
|
@@ -47,10 +47,14 @@ function isPrivateDirectory(directory) {
|
|
|
47
47
|
try {
|
|
48
48
|
const resolved = path.resolve(directory);
|
|
49
49
|
const stat = fs.lstatSync(resolved);
|
|
50
|
+
// Windows does not expose Unix ownership and permission bits through
|
|
51
|
+
// Stats.mode. The ACL is enforced by the platform, so applying the
|
|
52
|
+
// POSIX 0700 check here rejects otherwise valid Windows directories.
|
|
53
|
+
const isWindows = process.platform === 'win32';
|
|
50
54
|
if (!stat.isDirectory()
|
|
51
55
|
|| stat.isSymbolicLink()
|
|
52
|
-
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
53
|
-
|| (stat.mode & 0o077) !== 0)
|
|
56
|
+
|| (!isWindows && typeof process.getuid === 'function' && stat.uid !== process.getuid())
|
|
57
|
+
|| (!isWindows && (stat.mode & 0o077) !== 0))
|
|
54
58
|
return false;
|
|
55
59
|
// A private leaf below a symlinked ancestor can still escape the managed
|
|
56
60
|
// namespace. Walk the existing ancestor chain and reject such paths while
|
|
@@ -705,7 +705,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
705
705
|
const args = normalizedContent.slice(5).trim();
|
|
706
706
|
const permissionDecisionLabels = {
|
|
707
707
|
allow: '✓ 已授权(本次),继续执行……',
|
|
708
|
-
always: '✓
|
|
708
|
+
always: '✓ 已授权(当前会话临时授权 30 分钟),继续执行……',
|
|
709
709
|
deny: '✓ 已拒绝',
|
|
710
710
|
};
|
|
711
711
|
// Explicit request IDs are globally unique and already bound to an exact
|
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
{
|
|
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:
|
|
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
|
|
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
|
-
{
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
883
|
-
//
|
|
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');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evolcore",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "AI Agent gateway connecting Claude, Codex, Gemini, and the bundled ecagent runner to messaging channels with multi-project session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|