evolcore 0.0.33 → 0.0.35-test.0
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 +59 -0
- package/bin/ec.js +3 -2
- package/bin/install-codex-managed-hooks.mjs +38 -4
- package/dist/agents/baseagent-availability.js +20 -36
- package/dist/agents/claude-runner.js +93 -14
- package/dist/agents/codex/diagnostics.js +79 -22
- package/dist/agents/codex-runner.js +245 -47
- package/dist/agents/ecagent-runner.js +90 -27
- package/dist/agents/runner-types.js +5 -6
- package/dist/aun/aid/agentmd.js +13 -2
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/msg/managed-operation.js +16 -0
- package/dist/aun/msg/p2p.js +18 -2
- package/dist/channels/aun.js +123 -7
- package/dist/channels/daemon.js +6 -3
- package/dist/cli/aun-commands.js +39 -14
- package/dist/cli/ctl-command.js +3 -1
- package/dist/cli/daemon-commands.js +130 -23
- package/dist/cli/fs-command.js +2 -1
- package/dist/cli/getmeta-command.js +211 -0
- package/dist/cli/index.js +26 -6
- package/dist/cli/integrity-command.js +196 -0
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/memory-command.js +200 -156
- package/dist/cli/thread-command.js +17 -7
- package/dist/cli/trigger-command.js +6 -1
- package/dist/config/config-field-policy.js +4 -0
- package/dist/config/config-manager.js +19 -0
- package/dist/config/contact-book-store.js +5 -0
- package/dist/config/resolved-config-op.js +25 -8
- package/dist/config/role-service.js +13 -0
- package/dist/config/runtime-env.js +16 -0
- package/dist/config-store.js +30 -0
- package/dist/core/audit/log-integrity.js +3 -1
- package/dist/core/auth/agent-delegation.js +108 -35
- package/dist/core/auth/delegation-diagnostics.js +414 -0
- package/dist/core/auth/operation-authorizer.js +22 -3
- package/dist/core/auth/operation-catalog.js +16 -0
- package/dist/core/channel-runtime.js +1 -1
- package/dist/core/command/agent-control.js +18 -26
- package/dist/core/command/command-handler.js +145 -5
- package/dist/core/command/daemon-diagnostics.js +1 -0
- package/dist/core/command/menu-catalog.js +3 -0
- package/dist/core/command/menu-handler.js +24 -7
- package/dist/core/command/menu-protocol.js +1 -0
- package/dist/core/command/slash-handler.js +2 -0
- package/dist/core/command/topic-slash.js +4 -7
- package/dist/core/daemon-bootstrap.js +4 -7
- package/dist/core/event-catalog.js +14 -0
- package/dist/core/file-integrity/read.js +61 -0
- package/dist/core/file-integrity/store.js +262 -0
- package/dist/core/ipc-registration.js +47 -2
- package/dist/core/memory/batch-meta.js +133 -0
- package/dist/core/memory/consolidation-context.js +132 -28
- package/dist/core/memory/consolidation-ops-records.js +14 -16
- package/dist/core/memory/consolidation-trigger.js +15 -7
- package/dist/core/memory/consolidation.js +146 -26
- package/dist/core/memory/index-manager.js +19 -28
- package/dist/core/memory/memory-admin.js +19 -14
- package/dist/core/memory/memory-document.js +53 -9
- package/dist/core/memory/memory-writer.js +50 -19
- package/dist/core/message/aun-ipc-sender.js +19 -1
- package/dist/core/message/im-renderer.js +20 -4
- package/dist/core/message/message-bridge.js +5 -4
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/peer-mode.js +17 -0
- package/dist/core/message/response-engine.js +598 -50
- package/dist/core/permission/ec-command-parser.js +169 -44
- package/dist/core/permission/protected-paths.js +5 -1
- package/dist/core/permission/readonly-shell-query.js +43 -5
- package/dist/core/permission/session-path-grants.js +334 -0
- package/dist/core/permission/tool-canonical.js +226 -0
- package/dist/core/permission/tool-policy.js +60 -62
- package/dist/core/relation/peer-identity.js +87 -12
- package/dist/core/role/runtime-policy.js +21 -10
- package/dist/core/session/session-fs-store.js +123 -8
- package/dist/core/session/session-manager.js +131 -6
- package/dist/core/session/session-title.js +26 -1
- package/dist/core/skill/index-generator.js +17 -3
- package/dist/core/system-session/grants.js +40 -0
- package/dist/core/system-session/memory.js +289 -0
- package/dist/core/system-session/registry.js +74 -0
- package/dist/core/system-session/types.js +14 -0
- package/dist/core/thread/command-parser.js +6 -1
- package/dist/core/thread/ipc-operation.js +33 -16
- package/dist/core/thread/thread-operation.js +62 -24
- package/dist/core/thread/thread-service.js +258 -51
- package/dist/eck/kit-renderer.js +7 -2
- package/dist/eck/manifest-engine.js +24 -4
- package/dist/index.js +11 -2
- package/dist/ipc.js +51 -40
- package/dist/paths.js +70 -0
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/feedback.js +6 -1
- package/dist/trigger/parser.js +8 -1
- package/dist/trigger/system-definitions.js +3 -3
- package/dist/utils/cross-platform.js +17 -5
- package/dist/utils/error-dict.json +18 -4
- package/dist/utils/error-utils.js +121 -0
- package/dist/utils/instance-registry.js +13 -8
- package/dist/utils/log-writer.js +12 -0
- package/dist/utils/process-introspect.js +11 -0
- package/kits/docs/context-assembly.md +0 -1
- package/kits/docs/evolcore/agent-create.md +5 -4
- package/kits/docs/evolcore/agent.md +2 -2
- package/kits/docs/evolcore/config.md +2 -2
- package/kits/docs/evolcore/ctl.md +2 -2
- package/kits/docs/evolcore/ec-capabilities.md +1 -1
- package/kits/docs/evolcore/fs-architecture.md +9 -10
- package/kits/docs/evolcore/fs.md +2 -2
- package/kits/docs/evolcore/group-collaboration.md +3 -3
- package/kits/docs/evolcore/group.md +14 -12
- package/kits/docs/evolcore/memory-architecture.md +4 -5
- package/kits/docs/evolcore/memory-consolidation.md +19 -16
- package/kits/docs/evolcore/memory-tags.md +3 -1
- package/kits/docs/evolcore/memory.md +24 -12
- package/kits/docs/evolcore/model.md +16 -0
- package/kits/docs/evolcore/msg.md +7 -7
- package/kits/docs/evolcore/stats.md +1 -1
- package/kits/docs/evolcore/thread-chain.md +30 -14
- package/kits/docs/prompt-loading-architecture.md +0 -1
- package/kits/eck_manifest.auxiliary.json +9 -0
- package/kits/eck_manifest.json +20 -20
- package/kits/eck_manifest.memory.json +20 -12
- package/kits/rules/01-overview.md +11 -0
- package/kits/rules/03-identity.md +6 -3
- package/kits/rules/06-channel.md +1 -1
- package/kits/rules/07-memory-and-thread.md +13 -16
- package/kits/schemas/_meta.json +7 -1
- package/kits/schemas/agent-config.schema.13.json +1 -1
- package/kits/schemas/daemon.schema.7.json +140 -0
- package/kits/skills/ec-group/SKILL.md +8 -5
- package/kits/skills/ec-skill/scripts/cli.mjs +1 -1
- package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
- package/kits/skills/thread-ops/SKILL.md +18 -18
- package/kits/templates/memory-consolidation/forget.md +2 -2
- package/kits/templates/memory-consolidation/generalize.md +4 -4
- package/kits/templates/memory-consolidation/notes.md +12 -12
- package/kits/templates/memory-consolidation/profile.md +3 -3
- package/kits/templates/memory-consolidation/promote.md +3 -3
- package/kits/templates/memory-consolidation/workflow.md +42 -7
- package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
- package/kits/templates/system-fragments/bash-description.md +9 -0
- package/kits/templates/system-fragments/bootstrap.md +4 -3
- package/kits/templates/system-fragments/channel.md +2 -2
- package/kits/templates/system-fragments/memory.md +16 -9
- package/kits/templates/system-fragments/reply.md +6 -6
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +4 -3
- package/dist/core/memory/memory-session-policy.js +0 -41
- package/dist/core/memory/memory-signature.js +0 -257
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-path-grants.ts — 用户会话路径授权表
|
|
3
|
+
*
|
|
4
|
+
* 形状:路径模板 → [{ identities, access }],identities 为角色名(owner/admin/member/visitor)。
|
|
5
|
+
*
|
|
6
|
+
* 只服务用户会话。系统会话(记忆整理等)与角色体系无关,路径边界由
|
|
7
|
+
* src/core/system-session/ 下的模块在代码里决定,不走本表、不并运行时授予。
|
|
8
|
+
*
|
|
9
|
+
* 判定规则(两条,别加第三条):
|
|
10
|
+
* 1. 同会话多条命中 → 取最大权限(none 显式覆盖通配)
|
|
11
|
+
* 2. 与权限模式取交集:readonly 下 write 降级为 read
|
|
12
|
+
*
|
|
13
|
+
* 多表合并(mergeSessionPathGrantTables)同路径追加而非替换。
|
|
14
|
+
* 运行时动态授予(ec ctl path grant)走 grantSessionPath(),按 sessionId 持久化。
|
|
15
|
+
*
|
|
16
|
+
* 详见 docs/memory/system-session-plugin-design.md
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import { resolveRoot } from '../../paths.js';
|
|
21
|
+
import { resolveProtectedCandidate, isSameOrDescendant } from './protected-paths.js';
|
|
22
|
+
import { logger } from '../../utils/logger.js';
|
|
23
|
+
import { registerContent, unregisterFile } from '../file-integrity/store.js';
|
|
24
|
+
const ACCESS_RANK = { none: -1, read: 1, write: 2 };
|
|
25
|
+
/**
|
|
26
|
+
* 内置默认授权表。当前为空:用户会话缺省只能读工作目录,
|
|
27
|
+
* 额外路径由 owner/admin 经 `ec ctl path grant` 按会话授予。
|
|
28
|
+
*/
|
|
29
|
+
export const BUILTIN_SESSION_PATH_GRANTS = {};
|
|
30
|
+
/**
|
|
31
|
+
* 合并多张授权表:同路径模板的条目**追加**,不替换。
|
|
32
|
+
*
|
|
33
|
+
* 对象展开 `{ ...a, ...b }` 在同键时会整组丢弃前者——会话表里一旦出现同名路径,
|
|
34
|
+
* 前一张表的授予就静默消失。追加后判定仍走「同路径取最大、none 锁定」,
|
|
35
|
+
* 所以想收窄只能显式写 none,不会因为键名巧合丢权限。
|
|
36
|
+
*/
|
|
37
|
+
export function mergeSessionPathGrantTables(...tables) {
|
|
38
|
+
const merged = {};
|
|
39
|
+
for (const table of tables) {
|
|
40
|
+
if (!table)
|
|
41
|
+
continue;
|
|
42
|
+
for (const [template, entries] of Object.entries(table)) {
|
|
43
|
+
if (!Array.isArray(entries))
|
|
44
|
+
continue;
|
|
45
|
+
merged[template] = [...(merged[template] ?? []), ...entries];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return merged;
|
|
49
|
+
}
|
|
50
|
+
/** 判断一份授权表里的条目是否合法。 */
|
|
51
|
+
export function isValidGrantEntry(entry) {
|
|
52
|
+
return Array.isArray(entry.identities)
|
|
53
|
+
&& entry.identities.length > 0
|
|
54
|
+
&& entry.identities.every(value => typeof value === 'string' && value.trim().length > 0)
|
|
55
|
+
&& (entry.access === 'none' || entry.access === 'read' || entry.access === 'write');
|
|
56
|
+
}
|
|
57
|
+
/** 替换路径模板里的变量。未知变量保持原样,由调用方按「不存在」处理。 */
|
|
58
|
+
export function substitutePathTemplate(template, ctx, vars) {
|
|
59
|
+
return template.replace(/\$([A-Z_]+)/g, (match, name) => vars[name] ?? match);
|
|
60
|
+
}
|
|
61
|
+
function buildVars(ctx) {
|
|
62
|
+
return {
|
|
63
|
+
HOME: resolveRoot(),
|
|
64
|
+
SELF_AID: ctx.selfAid,
|
|
65
|
+
PEER: ctx.peerKey,
|
|
66
|
+
OWNER: ctx.ownerAid,
|
|
67
|
+
PROJECT: ctx.projectPath,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** 模板里出现未解析的变量时视为不可用条目。展开后的路径必须是绝对路径。 */
|
|
71
|
+
function resolveTemplate(template, ctx) {
|
|
72
|
+
const resolved = substitutePathTemplate(template, ctx, buildVars(ctx));
|
|
73
|
+
const unresolved = resolved.match(/\$[A-Z_]+/g);
|
|
74
|
+
if (unresolved) {
|
|
75
|
+
// 未知变量(拼错)与已知变量缺值(如私聊会话没有 $OWNER)都会走到这里;
|
|
76
|
+
// 两者都让整条授予失效,留一行日志,否则下游只剩一个原因不明的读拒绝。
|
|
77
|
+
logger.warn(`[path-grants] 授权模板含未解析变量,条目跳过: template=${template} unresolved=${[...new Set(unresolved)].join(',')} identity=${ctx.identity ?? ''}`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (!path.isAbsolute(resolved)) {
|
|
81
|
+
logger.warn(`[path-grants] 授权模板展开后不是绝对路径,条目跳过: template=${template} resolved=${resolved}`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return resolveProtectedCandidate(resolved);
|
|
85
|
+
}
|
|
86
|
+
/** 当前会话是否命中某条授予的身份。 */
|
|
87
|
+
function identityMatches(entry, identity) {
|
|
88
|
+
const normalized = identity.trim().toLowerCase();
|
|
89
|
+
return entry.identities.some(value => value.trim().toLowerCase() === normalized);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 权限模式交集。系统会话不传 permissionMode,按授予原样生效。
|
|
93
|
+
* readonly 下 write 降级为 read;none 永不升级。
|
|
94
|
+
*/
|
|
95
|
+
function applyPermissionModeCeiling(access, permissionMode) {
|
|
96
|
+
if (access === 'none' || !permissionMode)
|
|
97
|
+
return access;
|
|
98
|
+
return permissionMode === 'readonly' ? 'read' : access;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 计算当前会话在给定授权表下的**有效路径集合**。
|
|
102
|
+
*
|
|
103
|
+
* 返回 path → access 的映射,只含 access 非 none 的条目。同路径多条命中取最大权限
|
|
104
|
+
* (none 除外——任一命中 none 即整体拒绝)。
|
|
105
|
+
*/
|
|
106
|
+
export function resolveEffectivePathGrants(table, ctx) {
|
|
107
|
+
const effective = new Map();
|
|
108
|
+
const identity = (ctx.identity ?? '').trim();
|
|
109
|
+
if (!identity)
|
|
110
|
+
return effective;
|
|
111
|
+
for (const [template, entries] of Object.entries(table)) {
|
|
112
|
+
if (!Array.isArray(entries))
|
|
113
|
+
continue;
|
|
114
|
+
const hits = entries.filter(entry => isValidGrantEntry(entry) && identityMatches(entry, identity));
|
|
115
|
+
if (hits.length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
const resolved = resolveTemplate(template, ctx);
|
|
118
|
+
if (!resolved)
|
|
119
|
+
continue;
|
|
120
|
+
const previous = effective.get(resolved);
|
|
121
|
+
// none 是显式拒绝,一旦出现就锁定,不被同路径的其它授予放宽
|
|
122
|
+
if (previous === 'none')
|
|
123
|
+
continue;
|
|
124
|
+
let next = 'none';
|
|
125
|
+
for (const hit of hits) {
|
|
126
|
+
const grant = applyPermissionModeCeiling(hit.access, ctx.permissionMode);
|
|
127
|
+
if (grant === 'none') {
|
|
128
|
+
next = 'none';
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (ACCESS_RANK[grant] > ACCESS_RANK[next])
|
|
132
|
+
next = grant;
|
|
133
|
+
}
|
|
134
|
+
if (next === 'none')
|
|
135
|
+
effective.set(resolved, 'none');
|
|
136
|
+
else
|
|
137
|
+
effective.set(resolved, next);
|
|
138
|
+
}
|
|
139
|
+
return effective;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 有效路径授权表的可读根目录列表(含只读授予)。
|
|
143
|
+
*
|
|
144
|
+
* 判定语义是「本路径落在任一可读根之下即为可读」,因此这里连同 write 授予一起返回——
|
|
145
|
+
* write 蕴含 read。
|
|
146
|
+
*/
|
|
147
|
+
export function resolveGrantedReadRoots(table, ctx) {
|
|
148
|
+
const roots = [];
|
|
149
|
+
for (const [value, access] of resolveEffectivePathGrants(table, ctx)) {
|
|
150
|
+
if (access === 'read' || access === 'write')
|
|
151
|
+
roots.push(value);
|
|
152
|
+
}
|
|
153
|
+
return roots;
|
|
154
|
+
}
|
|
155
|
+
/** 候选路径是否落在任一授权根之下(含根自身)。 */
|
|
156
|
+
export function isPathGranted(candidate, roots) {
|
|
157
|
+
const resolved = resolveProtectedCandidate(candidate);
|
|
158
|
+
return roots.some(root => isSameOrDescendant(resolved, root));
|
|
159
|
+
}
|
|
160
|
+
const runtimeGrants = new Map();
|
|
161
|
+
/**
|
|
162
|
+
* 持久化位置:$HOME/data/path/grants/<sessionId>.json —— **每会话一个文件**。
|
|
163
|
+
*
|
|
164
|
+
* 不用单个共用大文件:那样一份损坏或半写会让**所有**会话的授予一起丢,
|
|
165
|
+
* 而且清理某个会话要重写整文件。一会话一文件让隔离和回收都变成本地操作。
|
|
166
|
+
*
|
|
167
|
+
* 代价是首次加载要扫目录。授予是低频操作,目录规模等于活跃会话数,可接受。
|
|
168
|
+
*/
|
|
169
|
+
function runtimeGrantDir() {
|
|
170
|
+
return path.join(resolveRoot(), 'data', 'path', 'grants');
|
|
171
|
+
}
|
|
172
|
+
/** sessionId 形如 meta_YYYYMMDD_<epochMs>,字符集安全;仍做一次防御性收敛。 */
|
|
173
|
+
function runtimeGrantFilePath(sessionId) {
|
|
174
|
+
const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
175
|
+
return path.join(runtimeGrantDir(), `${safe}.json`);
|
|
176
|
+
}
|
|
177
|
+
/** 原子写:同目录临时文件 + rename,避免读到半写的 JSON。 */
|
|
178
|
+
function persistSessionGrants(sessionId) {
|
|
179
|
+
const file = runtimeGrantFilePath(sessionId);
|
|
180
|
+
try {
|
|
181
|
+
const list = runtimeGrants.get(sessionId);
|
|
182
|
+
if (!list || list.length === 0) {
|
|
183
|
+
fs.rmSync(file, { force: true });
|
|
184
|
+
try {
|
|
185
|
+
unregisterFile(file);
|
|
186
|
+
}
|
|
187
|
+
catch { /* best-effort */ }
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
191
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
192
|
+
const content = `${JSON.stringify(list, null, 2)}\n`;
|
|
193
|
+
// 先登记,再写盘:避免 CLI 进程读到"新内容 + 旧台账"的窗口
|
|
194
|
+
try {
|
|
195
|
+
registerContent(file, content);
|
|
196
|
+
}
|
|
197
|
+
catch { /* best-effort */ }
|
|
198
|
+
fs.writeFileSync(tmp, content, 'utf-8');
|
|
199
|
+
fs.renameSync(tmp, file);
|
|
200
|
+
}
|
|
201
|
+
catch { /* 持久化失败不阻断本次会话:内存授予仍然生效 */ }
|
|
202
|
+
}
|
|
203
|
+
let runtimeGrantsLoaded = false;
|
|
204
|
+
/** 首次使用时从磁盘加载一次。单个文件损坏只影响它自己那个会话。 */
|
|
205
|
+
function ensureRuntimeGrantsLoaded() {
|
|
206
|
+
if (runtimeGrantsLoaded)
|
|
207
|
+
return;
|
|
208
|
+
runtimeGrantsLoaded = true;
|
|
209
|
+
let files;
|
|
210
|
+
try {
|
|
211
|
+
files = fs.readdirSync(runtimeGrantDir()).filter(name => name.endsWith('.json'));
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return; // 首次运行:目录还不存在
|
|
215
|
+
}
|
|
216
|
+
for (const name of files) {
|
|
217
|
+
const sessionId = name.slice(0, -'.json'.length);
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(runtimeGrantDir(), name), 'utf-8'));
|
|
220
|
+
if (Array.isArray(parsed) && parsed.length > 0)
|
|
221
|
+
runtimeGrants.set(sessionId, parsed);
|
|
222
|
+
}
|
|
223
|
+
catch { /* 该会话的授权文件损坏:跳过它,不影响其它会话 */ }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** 供测试与重载清空(含持久化,避免下个用例读到上个用例的残留)。 */
|
|
227
|
+
export function clearRuntimePathGrants(sessionId) {
|
|
228
|
+
ensureRuntimeGrantsLoaded();
|
|
229
|
+
if (sessionId) {
|
|
230
|
+
runtimeGrants.delete(sessionId);
|
|
231
|
+
persistSessionGrants(sessionId);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const id of [...runtimeGrants.keys()]) {
|
|
235
|
+
runtimeGrants.delete(id);
|
|
236
|
+
persistSessionGrants(id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** 登记一条运行时授予。授权校验由 IPC 层负责(仅 owner/admin)。 */
|
|
240
|
+
export function grantSessionPath(record) {
|
|
241
|
+
ensureRuntimeGrantsLoaded();
|
|
242
|
+
const list = runtimeGrants.get(record.sessionId) ?? [];
|
|
243
|
+
// 同路径同身份重复登记按覆盖处理,避免列表无限增长
|
|
244
|
+
const kept = list.filter(existing => !(existing.path === record.path && existing.identity === record.identity));
|
|
245
|
+
kept.push(record);
|
|
246
|
+
runtimeGrants.set(record.sessionId, kept);
|
|
247
|
+
persistSessionGrants(record.sessionId);
|
|
248
|
+
}
|
|
249
|
+
/** 撤销一条运行时授予,返回是否命中。 */
|
|
250
|
+
export function revokeSessionPath(sessionId, template, identity) {
|
|
251
|
+
ensureRuntimeGrantsLoaded();
|
|
252
|
+
const list = runtimeGrants.get(sessionId);
|
|
253
|
+
if (!list)
|
|
254
|
+
return false;
|
|
255
|
+
const before = list.length;
|
|
256
|
+
const kept = list.filter(record => !(record.path === template && (identity === undefined || record.identity === identity)));
|
|
257
|
+
if (kept.length === before)
|
|
258
|
+
return false;
|
|
259
|
+
if (kept.length === 0)
|
|
260
|
+
runtimeGrants.delete(sessionId);
|
|
261
|
+
else
|
|
262
|
+
runtimeGrants.set(sessionId, kept);
|
|
263
|
+
persistSessionGrants(sessionId);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
/** 列出某会话的运行时授予。 */
|
|
267
|
+
export function listRuntimePathGrants(sessionId) {
|
|
268
|
+
ensureRuntimeGrantsLoaded();
|
|
269
|
+
return runtimeGrants.get(sessionId) ?? [];
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* 会话结束时的清理入口。
|
|
273
|
+
*
|
|
274
|
+
* 只清**该会话自己的**运行时授予:`ec ctl path grant` 是以当前会话为归属登记的,
|
|
275
|
+
* 会话结束后不再有会话身份去消费它。
|
|
276
|
+
*/
|
|
277
|
+
export function releaseSessionPathGrants(sessionId) {
|
|
278
|
+
ensureRuntimeGrantsLoaded();
|
|
279
|
+
if (!runtimeGrants.delete(sessionId))
|
|
280
|
+
return;
|
|
281
|
+
persistSessionGrants(sessionId);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* 把某会话的运行时授予并入授权表。
|
|
285
|
+
*
|
|
286
|
+
* 运行时授予以**会话身份**登记:identity 缺省时表示「该会话自身」,
|
|
287
|
+
* 亦即用调用方的 identity 匹配。
|
|
288
|
+
*/
|
|
289
|
+
export function mergeRuntimeGrantsForSession(table, sessionId, identity) {
|
|
290
|
+
if (!sessionId)
|
|
291
|
+
return table;
|
|
292
|
+
ensureRuntimeGrantsLoaded();
|
|
293
|
+
const list = runtimeGrants.get(sessionId);
|
|
294
|
+
if (!list || list.length === 0)
|
|
295
|
+
return table;
|
|
296
|
+
const merged = { ...table };
|
|
297
|
+
for (const record of list) {
|
|
298
|
+
if (!path.isAbsolute(record.path))
|
|
299
|
+
continue;
|
|
300
|
+
if (record.identity && record.identity.trim().toLowerCase() !== identity.trim().toLowerCase())
|
|
301
|
+
continue;
|
|
302
|
+
const existing = merged[record.path];
|
|
303
|
+
merged[record.path] = [...(existing ?? []), { identities: [identity], access: record.access }];
|
|
304
|
+
}
|
|
305
|
+
return merged;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 会话运行目录本身恒为可读根:它是本会话私有的临时根,runner 自己的临时产物
|
|
309
|
+
* 都落在这里(见 task-context.ts 的 ensureSessionRuntimeDir)。
|
|
310
|
+
*
|
|
311
|
+
* TMPDIR 写能力另由 managedTempDir 负责,不在这里表达。
|
|
312
|
+
*/
|
|
313
|
+
export function sessionOwnTempReadRoot(sessionRuntimeDir) {
|
|
314
|
+
if (!sessionRuntimeDir || !path.isAbsolute(sessionRuntimeDir))
|
|
315
|
+
return [];
|
|
316
|
+
return [resolveProtectedCandidate(sessionRuntimeDir)];
|
|
317
|
+
}
|
|
318
|
+
/** 缺省的文件系统根:会话工作目录。 */
|
|
319
|
+
export function defaultProjectRoot(projectPath) {
|
|
320
|
+
if (!projectPath || !path.isAbsolute(projectPath))
|
|
321
|
+
return [];
|
|
322
|
+
return [resolveProtectedCandidate(projectPath)];
|
|
323
|
+
}
|
|
324
|
+
/** 供诊断输出:当前会话的有效路径集合(含运行时授予)。 */
|
|
325
|
+
export function describeEffectivePaths(table, ctx, sessionRuntimeDir) {
|
|
326
|
+
const result = [];
|
|
327
|
+
for (const [value, access] of resolveEffectivePathGrants(table, ctx)) {
|
|
328
|
+
result.push({ path: value, access });
|
|
329
|
+
}
|
|
330
|
+
for (const value of sessionOwnTempReadRoot(sessionRuntimeDir)) {
|
|
331
|
+
result.push({ path: value, access: 'read' });
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-canonical.ts — runner-agnostic canonical tool name mapping
|
|
3
|
+
*
|
|
4
|
+
* Different runners surface the same capability under different names:
|
|
5
|
+
*
|
|
6
|
+
* canonical ← runner aliases
|
|
7
|
+
* ─────────────────────────────────────────────────────────────────
|
|
8
|
+
* Bash ← Shell (Codex), exec_command (Codex legacy)
|
|
9
|
+
* Write ← FileChange (new-file intent, Codex)
|
|
10
|
+
* Edit ← FileChange (patch intent, Codex), apply_patch (Codex)
|
|
11
|
+
* Read ← read_file (Codex legacy)
|
|
12
|
+
*
|
|
13
|
+
* All policy enforcement (system-session modules, evaluateToolPreflight,
|
|
14
|
+
* checkReadonly) should call canonicalToolName() once at the entry point
|
|
15
|
+
* and operate on the result, rather than handling runner-specific aliases
|
|
16
|
+
* scattered throughout.
|
|
17
|
+
*
|
|
18
|
+
* D1-A: FileChange / apply_patch require the tool input to distinguish
|
|
19
|
+
* Write vs Edit intent; the mapping function accepts an optional `input`
|
|
20
|
+
* parameter for that purpose.
|
|
21
|
+
*
|
|
22
|
+
* canonicalToolCall() goes one step further and also normalises the input:
|
|
23
|
+
* every runner's write shape (Claude/ecagent file_path, Codex fileChanges,
|
|
24
|
+
* standalone or shell-wrapped apply_patch) yields the same list of write
|
|
25
|
+
* targets, so policy modules never read runner-specific input fields.
|
|
26
|
+
*/
|
|
27
|
+
import { parseApplyPatchDocument, parseShellApplyPatchInvocation, } from './ec-command-parser.js';
|
|
28
|
+
/**
|
|
29
|
+
* Normalise a runner-specific tool name to its canonical form.
|
|
30
|
+
*
|
|
31
|
+
* This is a projection of canonicalToolCall(): both read the same
|
|
32
|
+
* `resolveToolIntent` result, so a name-only caller can never disagree with a
|
|
33
|
+
* caller that judges operands. Pass `input` when the runner tool name alone is
|
|
34
|
+
* ambiguous (FileChange, apply_patch, and shell-carried apply_patch).
|
|
35
|
+
*/
|
|
36
|
+
export function canonicalToolName(toolName, input) {
|
|
37
|
+
const intent = resolveToolIntent(toolName, input);
|
|
38
|
+
switch (intent.access) {
|
|
39
|
+
case 'write':
|
|
40
|
+
case 'read':
|
|
41
|
+
return intent.name;
|
|
42
|
+
case 'shell':
|
|
43
|
+
return 'Bash';
|
|
44
|
+
default:
|
|
45
|
+
return toolName;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns true iff the canonical name represents a shell execution tool.
|
|
50
|
+
* Consolidates the ad-hoc `isShellTool` checks spread across runners.
|
|
51
|
+
*/
|
|
52
|
+
export function isShellToolName(toolName) {
|
|
53
|
+
const canon = canonicalToolName(toolName);
|
|
54
|
+
return canon === 'Bash';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns true iff the canonical name represents a file-write tool.
|
|
58
|
+
* Used by system-session modules and readonly checks.
|
|
59
|
+
*/
|
|
60
|
+
export function isWriteToolName(toolName, input) {
|
|
61
|
+
return canonicalToolCall(toolName, input).access === 'write';
|
|
62
|
+
}
|
|
63
|
+
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Codex FileChange intent: if fileChanges contains an apply-patch style entry
|
|
66
|
+
* (has an `oldContent` / `patch` key) it is Edit; otherwise Write.
|
|
67
|
+
*/
|
|
68
|
+
function resolveFileChangeName(input) {
|
|
69
|
+
if (!input)
|
|
70
|
+
return 'Write'; // conservative fallback
|
|
71
|
+
const changes = input.fileChanges;
|
|
72
|
+
if (!Array.isArray(changes) || changes.length === 0)
|
|
73
|
+
return 'Write';
|
|
74
|
+
const first = changes[0];
|
|
75
|
+
if (first && typeof first === 'object') {
|
|
76
|
+
const obj = first;
|
|
77
|
+
// A patch/edit operation carries 'patch', 'oldContent', or 'type': 'edit'
|
|
78
|
+
if ('patch' in obj || 'oldContent' in obj || obj.type === 'edit')
|
|
79
|
+
return 'Edit';
|
|
80
|
+
}
|
|
81
|
+
return 'Write';
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* apply_patch: if the patch string starts with '--- ' or 'diff ' it is an
|
|
85
|
+
* edit; otherwise (e.g. a full file replacement) treat as Write.
|
|
86
|
+
*/
|
|
87
|
+
function resolveApplyPatchName(input) {
|
|
88
|
+
if (!input)
|
|
89
|
+
return 'Edit'; // apply_patch is most commonly a diff edit
|
|
90
|
+
const patch = typeof input.patch === 'string' ? input.patch : '';
|
|
91
|
+
if (patch.startsWith('--- ') || patch.startsWith('diff '))
|
|
92
|
+
return 'Edit';
|
|
93
|
+
return 'Write';
|
|
94
|
+
}
|
|
95
|
+
const READ_TOOL_NAMES = new Set(['Read', 'Glob', 'Grep']);
|
|
96
|
+
/** Keys a write tool may carry its target in (Claude / ecagent file tools). */
|
|
97
|
+
const FILE_WRITE_PATH_KEYS = ['file_path', 'notebook_path', 'path'];
|
|
98
|
+
/** Keys a standalone apply_patch call may carry its patch document in. */
|
|
99
|
+
const APPLY_PATCH_DOCUMENT_KEYS = ['patch', 'input', 'command'];
|
|
100
|
+
/** Runner tool names that mean "shell". */
|
|
101
|
+
const SHELL_TOOL_NAMES = new Set(['Shell', 'exec_command']);
|
|
102
|
+
/**
|
|
103
|
+
* The single place that decides what a tool call is. Every public helper here
|
|
104
|
+
* projects this result, so no two callers can classify the same call
|
|
105
|
+
* differently. `paths` is only populated for `write`; read scope stays with
|
|
106
|
+
* checkReadonly, which owns the Glob/Grep rules.
|
|
107
|
+
*/
|
|
108
|
+
function resolveToolIntent(toolName, input = {}) {
|
|
109
|
+
if (toolName === 'FileChange') {
|
|
110
|
+
const paths = [];
|
|
111
|
+
if (typeof input.grantRoot === 'string' && input.grantRoot)
|
|
112
|
+
paths.push(input.grantRoot);
|
|
113
|
+
collectFileChangePaths(input.fileChanges, paths);
|
|
114
|
+
return { access: 'write', name: resolveFileChangeName(input), via: 'tool', paths: unique(paths) };
|
|
115
|
+
}
|
|
116
|
+
if (toolName === 'apply_patch') {
|
|
117
|
+
return applyPatchCall(standaloneApplyPatchChanges(input), 'tool');
|
|
118
|
+
}
|
|
119
|
+
if (toolName === 'Bash' || SHELL_TOOL_NAMES.has(toolName)) {
|
|
120
|
+
const parsed = parseShellApplyPatchInvocation(input);
|
|
121
|
+
if (parsed.kind === 'file-change')
|
|
122
|
+
return applyPatchCall(parsed.fileChanges, 'shell');
|
|
123
|
+
// A recognised but malformed apply_patch is still a write attempt.
|
|
124
|
+
if (parsed.kind === 'invalid')
|
|
125
|
+
return { access: 'write', name: 'Edit', via: 'shell', paths: [] };
|
|
126
|
+
return { access: 'shell', name: 'Bash' };
|
|
127
|
+
}
|
|
128
|
+
// read_file is Codex's legacy read alias; Claude / ecagent use the name directly.
|
|
129
|
+
const name = toolName === 'read_file' ? 'Read' : toolName;
|
|
130
|
+
if (READ_TOOL_NAMES.has(name))
|
|
131
|
+
return { access: 'read', name: name };
|
|
132
|
+
if (name === 'Write' || name === 'Edit' || name === 'MultiEdit' || name === 'NotebookEdit') {
|
|
133
|
+
const paths = [];
|
|
134
|
+
for (const key of FILE_WRITE_PATH_KEYS) {
|
|
135
|
+
if (typeof input[key] === 'string' && input[key])
|
|
136
|
+
paths.push(input[key]);
|
|
137
|
+
}
|
|
138
|
+
return { access: 'write', name: name === 'Write' ? 'Write' : 'Edit', via: 'tool', paths: unique(paths) };
|
|
139
|
+
}
|
|
140
|
+
return { access: 'other', name: toolName };
|
|
141
|
+
}
|
|
142
|
+
export function canonicalToolCall(toolName, input = {}) {
|
|
143
|
+
return resolveToolIntent(toolName, input);
|
|
144
|
+
}
|
|
145
|
+
function applyPatchCall(changes, via) {
|
|
146
|
+
if (!changes || changes.length === 0)
|
|
147
|
+
return { access: 'write', name: 'Edit', via, paths: [] };
|
|
148
|
+
const name = changes.every(change => change.kind === 'add') ? 'Write' : 'Edit';
|
|
149
|
+
return { access: 'write', name, via, paths: unique(applyPatchChangePaths(changes)) };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A standalone apply_patch call is only accepted when every key that carries a
|
|
153
|
+
* patch document parses to the same change set; otherwise the paths approved
|
|
154
|
+
* here might not be the ones the runner applies.
|
|
155
|
+
*/
|
|
156
|
+
function standaloneApplyPatchChanges(input) {
|
|
157
|
+
let found;
|
|
158
|
+
let signature;
|
|
159
|
+
for (const key of APPLY_PATCH_DOCUMENT_KEYS) {
|
|
160
|
+
const value = input[key];
|
|
161
|
+
if (typeof value !== 'string' || !value.trim())
|
|
162
|
+
continue;
|
|
163
|
+
const changes = parseApplyPatchDocument(value);
|
|
164
|
+
if (!changes)
|
|
165
|
+
return undefined;
|
|
166
|
+
const next = JSON.stringify(changes);
|
|
167
|
+
if (signature !== undefined && signature !== next)
|
|
168
|
+
return undefined;
|
|
169
|
+
signature = next;
|
|
170
|
+
found = changes;
|
|
171
|
+
}
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
function unique(paths) {
|
|
175
|
+
return [...new Set(paths)];
|
|
176
|
+
}
|
|
177
|
+
// ── Write-target extraction shared with tool-policy ──────────────────────────
|
|
178
|
+
function collectExplicitFileChangePaths(record, output) {
|
|
179
|
+
const explicitPathKeys = ['path', 'filePath', 'file_path', 'movePath', 'move_path', 'destinationPath', 'targetPath'];
|
|
180
|
+
const hasExplicitPath = explicitPathKeys.some(key => typeof record[key] === 'string');
|
|
181
|
+
for (const key of explicitPathKeys) {
|
|
182
|
+
if (typeof record[key] === 'string' && record[key])
|
|
183
|
+
output.push(record[key]);
|
|
184
|
+
}
|
|
185
|
+
if (record.kind && typeof record.kind === 'object') {
|
|
186
|
+
collectExplicitFileChangePaths(record.kind, output);
|
|
187
|
+
}
|
|
188
|
+
return hasExplicitPath;
|
|
189
|
+
}
|
|
190
|
+
/** Collect every target path from a Codex / Claude-style `fileChanges` payload. */
|
|
191
|
+
export function collectFileChangePaths(value, output) {
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
for (const entry of value) {
|
|
194
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
195
|
+
collectExplicitFileChangePaths(entry, output);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!value || typeof value !== 'object')
|
|
201
|
+
return;
|
|
202
|
+
const record = value;
|
|
203
|
+
const hasExplicitPath = collectExplicitFileChangePaths(record, output);
|
|
204
|
+
// Claude-style inputs are maps keyed by path; Codex v2 uses an array of
|
|
205
|
+
// records with an explicit path field. Only treat keys as paths for the map
|
|
206
|
+
// shape so diff text and metadata never become path candidates.
|
|
207
|
+
if (!hasExplicitPath) {
|
|
208
|
+
for (const [filePath, change] of Object.entries(record)) {
|
|
209
|
+
if (filePath !== 'kind' && filePath !== 'type')
|
|
210
|
+
output.push(filePath);
|
|
211
|
+
if (change && typeof change === 'object' && !Array.isArray(change)) {
|
|
212
|
+
collectExplicitFileChangePaths(change, output);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Every path an apply_patch change set touches, including move destinations. */
|
|
218
|
+
export function applyPatchChangePaths(changes) {
|
|
219
|
+
const paths = [];
|
|
220
|
+
for (const change of changes) {
|
|
221
|
+
paths.push(change.path);
|
|
222
|
+
if (change.movePath)
|
|
223
|
+
paths.push(change.movePath);
|
|
224
|
+
}
|
|
225
|
+
return paths;
|
|
226
|
+
}
|