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
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { normalizeSessionLoopConfig, resolveSessionLoopTarget, sessionLoopError } from './session-idle-monitor.js';
|
|
2
2
|
import { ensureDir } from '../../utils/atomic-write.js';
|
|
3
|
-
import { resolvePaths } from '../../paths.js';
|
|
3
|
+
import { resolvePaths, agentThreadIndexPath } from '../../paths.js';
|
|
4
4
|
import { logger } from '../../utils/logger.js';
|
|
5
5
|
import { encodePath } from '../../utils/cross-platform.js';
|
|
6
6
|
import { readRunnerEventSummary } from './runner-event-summary.js';
|
|
7
7
|
import { isInternalInjectedUserMessage, isToolResultMessage } from './session-transcript.js';
|
|
8
|
-
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, } from './session-fs-store.js';
|
|
8
|
+
import { chatDirPath, generateSessionId, formatTimestamp, atomicWriteJson, atomicWriteText, appendJsonl, appendJsonlDurable, fsyncJsonlFile, readJsonFile, readLastJsonlLine, readLastJsonlLineStrict, readAllJsonlLines, scanChatDirs, scanMetaFiles, ensureChatDir, readThreadIndex, writeThreadIndex, compactJsonlTail, SESSION_META_COMPACT_POLICY, } from './session-fs-store.js';
|
|
9
9
|
import { sessionToFile, fileToSession, formatSessionKey, DEFAULT_THREAD_ID, normalizeTopicLabel, normalizeTopicStatus, } from './session-mapper.js';
|
|
10
10
|
import { tryParseChannelKey } from '../channel-loader.js';
|
|
11
11
|
import { isSystemControlChannel } from '../system-channels.js';
|
|
@@ -261,6 +261,9 @@ export class SessionManager {
|
|
|
261
261
|
identityResolver;
|
|
262
262
|
fileAdapters = new Map();
|
|
263
263
|
sessionEncryptState = new Map();
|
|
264
|
+
/** findSessionFileById 的查找缓存:命中前一律 stat 校验,不作为真相源。 */
|
|
265
|
+
sessionFileIndex = new Map();
|
|
266
|
+
knownChatDirs = null;
|
|
264
267
|
constructor(sessionsDir, eventBus, identityResolver) {
|
|
265
268
|
ensureDir(sessionsDir);
|
|
266
269
|
this.sessionsDir = sessionsDir;
|
|
@@ -627,6 +630,13 @@ export class SessionManager {
|
|
|
627
630
|
appendJsonlDurable(metaPath, file);
|
|
628
631
|
else
|
|
629
632
|
appendJsonl(metaPath, file);
|
|
633
|
+
// 本次快照已落盘;压缩只回收历史行,失败不影响写入结果,原文件保持不变。
|
|
634
|
+
try {
|
|
635
|
+
compactJsonlTail(metaPath, SESSION_META_COMPACT_POLICY);
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
logger.warn(`[SessionManager] meta compaction skipped for ${metaPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
639
|
+
}
|
|
630
640
|
}
|
|
631
641
|
/**
|
|
632
642
|
* Session 持久化的唯一事务原语。所有 session 写操作都应经此,业务层不直接调
|
|
@@ -817,24 +827,84 @@ export class SessionManager {
|
|
|
817
827
|
}
|
|
818
828
|
return results;
|
|
819
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* 按 sessionId 定位 meta 文件。逐个 id 全量 scanChatDirs 在几百个 chat 目录下
|
|
832
|
+
* 单次约 50ms,启动时批量查询(SessionIdleMonitor 恢复 170+ 会话)会同步阻塞数秒,
|
|
833
|
+
* 所以首次查询时按 readdir 建一份 id→路径索引,之后:
|
|
834
|
+
* - 命中:stat 校验仍在才返回(文件被移进 _trash/目录被迁移时自然失效);
|
|
835
|
+
* - 未命中:先在已知目录里按原规则 stat 探测(覆盖索引之后新建的会话),
|
|
836
|
+
* 再重扫目录列表、只探测新出现的目录。不存在的 id 开销与原实现相当。
|
|
837
|
+
* 目录顺序与「同目录先主文件后 _threads」的优先级保持原实现语义。
|
|
838
|
+
*/
|
|
820
839
|
findSessionFileById(sessionId) {
|
|
821
|
-
const
|
|
822
|
-
|
|
840
|
+
const cached = this.sessionFileIndex.get(sessionId);
|
|
841
|
+
if (cached) {
|
|
842
|
+
try {
|
|
843
|
+
fs.statSync(cached.metaPath);
|
|
844
|
+
return cached;
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
this.sessionFileIndex.delete(sessionId);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (!this.knownChatDirs) {
|
|
851
|
+
this.rebuildSessionFileIndex();
|
|
852
|
+
const indexed = this.sessionFileIndex.get(sessionId);
|
|
853
|
+
if (indexed)
|
|
854
|
+
return indexed;
|
|
855
|
+
}
|
|
856
|
+
const probed = this.probeSessionFile(sessionId, this.knownChatDirs);
|
|
857
|
+
if (probed)
|
|
858
|
+
return probed;
|
|
859
|
+
const previous = new Set(this.knownChatDirs);
|
|
860
|
+
this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
|
|
861
|
+
return this.probeSessionFile(sessionId, this.knownChatDirs.filter(dir => !previous.has(dir)));
|
|
862
|
+
}
|
|
863
|
+
probeSessionFile(sessionId, chatDirs) {
|
|
864
|
+
for (const dirPath of chatDirs) {
|
|
823
865
|
const mainPath = path.join(dirPath, `${sessionId}.jsonl`);
|
|
824
866
|
try {
|
|
825
867
|
fs.statSync(mainPath);
|
|
826
|
-
return { chatDir: dirPath, metaPath: mainPath, isThread: false };
|
|
868
|
+
return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: mainPath, isThread: false });
|
|
827
869
|
}
|
|
828
870
|
catch { }
|
|
829
871
|
const threadPath = path.join(dirPath, '_threads', `${sessionId}.jsonl`);
|
|
830
872
|
try {
|
|
831
873
|
fs.statSync(threadPath);
|
|
832
|
-
return { chatDir: dirPath, metaPath: threadPath, isThread: true };
|
|
874
|
+
return this.rememberSessionFile(sessionId, { chatDir: dirPath, metaPath: threadPath, isThread: true });
|
|
833
875
|
}
|
|
834
876
|
catch { }
|
|
835
877
|
}
|
|
836
878
|
return undefined;
|
|
837
879
|
}
|
|
880
|
+
rememberSessionFile(sessionId, found) {
|
|
881
|
+
this.sessionFileIndex.set(sessionId, found);
|
|
882
|
+
return found;
|
|
883
|
+
}
|
|
884
|
+
rebuildSessionFileIndex() {
|
|
885
|
+
this.sessionFileIndex.clear();
|
|
886
|
+
this.knownChatDirs = scanChatDirs(this.sessionsDir).map(entry => entry.dirPath);
|
|
887
|
+
for (const dirPath of this.knownChatDirs) {
|
|
888
|
+
for (const [dir, isThread] of [[dirPath, false], [path.join(dirPath, '_threads'), true]]) {
|
|
889
|
+
let names;
|
|
890
|
+
try {
|
|
891
|
+
names = fs.readdirSync(dir);
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
for (const name of names) {
|
|
897
|
+
if (!name.endsWith('.jsonl'))
|
|
898
|
+
continue;
|
|
899
|
+
const id = name.slice(0, -'.jsonl'.length);
|
|
900
|
+
// 先到先得:与逐目录探测时的优先级一致。
|
|
901
|
+
if (!this.sessionFileIndex.has(id)) {
|
|
902
|
+
this.sessionFileIndex.set(id, { chatDir: dirPath, metaPath: path.join(dir, name), isThread });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
838
908
|
// ─── Public API ───
|
|
839
909
|
getKnownThreadIds(channel) {
|
|
840
910
|
const chatDirs = scanChatDirs(this.sessionsDir);
|
|
@@ -1485,6 +1555,20 @@ export class SessionManager {
|
|
|
1485
1555
|
metaFile: `${session.id}.jsonl`,
|
|
1486
1556
|
};
|
|
1487
1557
|
writeThreadIndex(chatDir, threadIndex);
|
|
1558
|
+
// 被动建档(收到消息时自动创建话题会话)也必须同步到 agent 级话题索引,
|
|
1559
|
+
// 否则 `ec thread rename current` / list 等按索引寻址的操作会报"话题不存在"。
|
|
1560
|
+
this.upsertAgentThreadIndex({
|
|
1561
|
+
selfAid: selfAID ?? '',
|
|
1562
|
+
threadId,
|
|
1563
|
+
sessionId: session.id,
|
|
1564
|
+
peer: channelId,
|
|
1565
|
+
channelType: channelType ?? 'aun',
|
|
1566
|
+
peerKind: effectiveChatType === 'group' ? 'group' : 'private',
|
|
1567
|
+
name: session.name,
|
|
1568
|
+
label: normalizeTopicLabel(label) ?? null,
|
|
1569
|
+
topicStatus: session.topicStatus ?? 'pending',
|
|
1570
|
+
now: session.createdAt ?? Date.now(),
|
|
1571
|
+
});
|
|
1488
1572
|
this.eventBus.publish({
|
|
1489
1573
|
type: 'session:created',
|
|
1490
1574
|
sessionId: session.id,
|
|
@@ -1497,6 +1581,47 @@ export class SessionManager {
|
|
|
1497
1581
|
});
|
|
1498
1582
|
return session;
|
|
1499
1583
|
}
|
|
1584
|
+
/**
|
|
1585
|
+
* 把一条话题会话合并进 agent 级索引(`data/threads/index.json`)。
|
|
1586
|
+
*
|
|
1587
|
+
* 全局索引是 `ec thread` 各命令的寻址源;被动建档只写过 chatDir 下的
|
|
1588
|
+
* `_threads/thread-index.json`,导致 `rename current` 之类找不到自己。
|
|
1589
|
+
* 这里按 (peer, threadId) 幂等 upsert:已存在则只补缺失字段,不覆盖
|
|
1590
|
+
* 名称/摘要等既有属性。
|
|
1591
|
+
*/
|
|
1592
|
+
upsertAgentThreadIndex(input) {
|
|
1593
|
+
if (!input.selfAid || !input.threadId)
|
|
1594
|
+
return;
|
|
1595
|
+
try {
|
|
1596
|
+
const indexPath = agentThreadIndexPath(input.selfAid);
|
|
1597
|
+
const raw = readJsonFile(indexPath);
|
|
1598
|
+
const index = raw && Array.isArray(raw.threads) ? raw : { version: 1, threads: [] };
|
|
1599
|
+
const existing = index.threads.find((t) => t.threadId === input.threadId
|
|
1600
|
+
&& String(t.peer ?? '').toLowerCase() === input.peer.toLowerCase());
|
|
1601
|
+
if (existing) {
|
|
1602
|
+
existing.sessionId = existing.sessionId || input.sessionId;
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
index.threads.push({
|
|
1606
|
+
threadId: input.threadId,
|
|
1607
|
+
sessionId: input.sessionId,
|
|
1608
|
+
peer: input.peer,
|
|
1609
|
+
channelType: input.channelType,
|
|
1610
|
+
peerKind: input.peerKind,
|
|
1611
|
+
name: input.name || '话题会话',
|
|
1612
|
+
label: input.label ?? null,
|
|
1613
|
+
topicStatus: input.topicStatus,
|
|
1614
|
+
createdAt: input.now,
|
|
1615
|
+
lastActiveAt: input.now,
|
|
1616
|
+
});
|
|
1617
|
+
ensureDir(path.dirname(indexPath));
|
|
1618
|
+
atomicWriteJson(indexPath, index);
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
// 索引同步失败不能阻断消息处理:会话本身已落盘,rebuild-index 可补齐。
|
|
1622
|
+
logger.warn(`[SessionManager] agent thread index upsert failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1500
1625
|
async switchProject(channel, channelId, newProjectPath, baseagent) {
|
|
1501
1626
|
if (!baseagent) {
|
|
1502
1627
|
throw new Error(`[SessionManager] switchProject: baseagent is empty for channel=${channel} channelId=${channelId}`);
|
|
@@ -94,6 +94,31 @@ export function buildCliSessionPreview(messages, backend, options = {}) {
|
|
|
94
94
|
: null;
|
|
95
95
|
return { title, hasReal: !!summary.firstRealMessage };
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* 会话/话题名称的长度常量,自动生成与手动改名共用。
|
|
99
|
+
*
|
|
100
|
+
* - `SESSION_TITLE_MAX_LEN`(80):正文字数,超长文本截到这里再加省略号;
|
|
101
|
+
* - `SESSION_TITLE_STORED_MAX_LEN`(83):落盘上限 = 80 + `...`。
|
|
102
|
+
*
|
|
103
|
+
* 80~83 字符的名字本身就是合法落盘形态,不再二次截断——
|
|
104
|
+
* 否则「系统自动生成的名字」会变成改一次短一点。
|
|
105
|
+
*/
|
|
106
|
+
export const SESSION_TITLE_MAX_LEN = 80;
|
|
107
|
+
export const SESSION_TITLE_ELLIPSIS = '...';
|
|
108
|
+
/** 落盘名称的硬上限(截断后的最长长度)。 */
|
|
109
|
+
export const SESSION_TITLE_STORED_MAX_LEN = SESSION_TITLE_MAX_LEN + SESSION_TITLE_ELLIPSIS.length;
|
|
110
|
+
/**
|
|
111
|
+
* 截断到落盘上限并补省略号;未超长时原样返回。
|
|
112
|
+
*
|
|
113
|
+
* 判据是 `SESSION_TITLE_STORED_MAX_LEN`(83)而不是正文上限(80):
|
|
114
|
+
* 80~83 字符的名称本来就是合法落盘形态(含历史遗留),截断它们会把
|
|
115
|
+
* 「系统生成的名字原样改回去」变成「改一次短一点」。
|
|
116
|
+
*/
|
|
117
|
+
export function truncateSessionTitle(title) {
|
|
118
|
+
return title.length > SESSION_TITLE_STORED_MAX_LEN
|
|
119
|
+
? `${title.slice(0, SESSION_TITLE_MAX_LEN)}${SESSION_TITLE_ELLIPSIS}`
|
|
120
|
+
: title;
|
|
121
|
+
}
|
|
97
122
|
export function sanitizeSessionTitle(title) {
|
|
98
123
|
if (typeof title !== 'string')
|
|
99
124
|
return undefined;
|
|
@@ -109,7 +134,7 @@ export function sanitizeSessionTitle(title) {
|
|
|
109
134
|
return undefined;
|
|
110
135
|
if (/^\.+$/.test(cleanTitle))
|
|
111
136
|
return undefined;
|
|
112
|
-
return cleanTitle
|
|
137
|
+
return truncateSessionTitle(cleanTitle);
|
|
113
138
|
}
|
|
114
139
|
export function displaySessionTitle(title, fallback = '默认会话') {
|
|
115
140
|
return sanitizeSessionTitle(title) || sanitizeSessionTitle(fallback) || fallback;
|
|
@@ -3,6 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import { parseDocument } from 'yaml';
|
|
4
4
|
import { resolveSkills } from './switch-resolver.js';
|
|
5
5
|
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import { isDevMode } from '../../paths.js';
|
|
7
|
+
import { isShippedPath, readVerifiedText } from '../file-integrity/read.js';
|
|
6
8
|
/**
|
|
7
9
|
* 生成索引文件(汇总 → 分组 → 落两文件)。
|
|
8
10
|
*
|
|
@@ -68,11 +70,22 @@ function generateOneIndex(indexDir, filename, skills, label) {
|
|
|
68
70
|
*
|
|
69
71
|
* 直接按目录读,不走 discoverSkillDirectories——后者按父目录扫描再按 name 匹配,
|
|
70
72
|
* 同名技能(本机覆盖内置)会读到另一份的 description。
|
|
73
|
+
*
|
|
74
|
+
* 内置技能是随包发布的内容:安装模式下按台账校验,带外改动会被恢复成登记版本,
|
|
75
|
+
* 恢复不了就不进索引(返回空描述),与 ECK 文档同一套策略。开发模式下不校验,
|
|
76
|
+
* agent 随手改技能可以立刻生效。
|
|
77
|
+
*
|
|
78
|
+
* 注意:这里保护的是索引里那行描述。模型拿到索引后是用 Read 工具读 SKILL.md 正文的,
|
|
79
|
+
* 那条路径不经过本函数,目前不受台账保护(等单独的 SKILL.md 加载工具接手)。
|
|
71
80
|
*/
|
|
72
81
|
export function readSkillDescription(skillDir) {
|
|
82
|
+
const file = path.join(skillDir, 'SKILL.md');
|
|
83
|
+
const verify = isShippedPath(file) && !isDevMode();
|
|
73
84
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
85
|
+
const checked = readVerifiedText(file, { verify, tag: 'skill' });
|
|
86
|
+
if (!checked)
|
|
87
|
+
return '';
|
|
88
|
+
const match = checked.text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
76
89
|
if (!match)
|
|
77
90
|
return '';
|
|
78
91
|
const doc = parseDocument(match[1]);
|
|
@@ -81,7 +94,8 @@ export function readSkillDescription(skillDir) {
|
|
|
81
94
|
const meta = doc.toJS();
|
|
82
95
|
return typeof meta?.description === 'string' ? meta.description.trim() : '';
|
|
83
96
|
}
|
|
84
|
-
catch {
|
|
97
|
+
catch (err) {
|
|
98
|
+
logger.warn(`[skill] read description failed path=${file} ${err instanceof Error ? err.message : String(err)}`);
|
|
85
99
|
return '';
|
|
86
100
|
}
|
|
87
101
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* system-session/grants.ts — 写入目标与模块声明的可写文件逐个精确比对
|
|
3
|
+
*
|
|
4
|
+
* 输入只有 canonicalToolCall 给出的写入目标和 grants().writeFiles,不读任何 runner 的工具入参。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { isSameOrDescendant, resolveProtectedCandidateWithoutFinalSymlink, } from '../permission/protected-paths.js';
|
|
8
|
+
/**
|
|
9
|
+
* 每个写入目标都必须正好是某个声明的可写文件:
|
|
10
|
+
* - 祖先目录按 realpath 解析、最后一段按字面比较(目标若是符号链接,比较的是链接本身,不跟随);
|
|
11
|
+
* - 目标已存在时必须是普通文件且只有一个硬链接,链接、目录、硬链接一律拒绝;
|
|
12
|
+
* - 相对路径按会话 cwd 解析。目标列表为空表示取不到路径,按拒绝处理。
|
|
13
|
+
*/
|
|
14
|
+
export function checkWriteTargets(targets, writeFiles, cwd) {
|
|
15
|
+
if (targets.length === 0)
|
|
16
|
+
return { ok: false, reason: 'no-target' };
|
|
17
|
+
const allowed = writeFiles.map(file => resolveProtectedCandidateWithoutFinalSymlink(file));
|
|
18
|
+
for (const target of targets) {
|
|
19
|
+
const resolved = resolveProtectedCandidateWithoutFinalSymlink(target, cwd);
|
|
20
|
+
if (!allowed.some(file => samePath(resolved, file))) {
|
|
21
|
+
return { ok: false, reason: 'outside-grant', path: resolved };
|
|
22
|
+
}
|
|
23
|
+
let stat;
|
|
24
|
+
try {
|
|
25
|
+
stat = fs.lstatSync(resolved);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// 尚未创建是正常情况;其余错误(权限、父路径是文件等)一律按拒绝处理。
|
|
29
|
+
if (error.code === 'ENOENT')
|
|
30
|
+
continue;
|
|
31
|
+
return { ok: false, reason: 'not-regular-file', path: resolved };
|
|
32
|
+
}
|
|
33
|
+
if (!stat.isFile() || stat.nlink > 1)
|
|
34
|
+
return { ok: false, reason: 'not-regular-file', path: resolved };
|
|
35
|
+
}
|
|
36
|
+
return { ok: true };
|
|
37
|
+
}
|
|
38
|
+
function samePath(a, b) {
|
|
39
|
+
return isSameOrDescendant(a, b) && isSameOrDescendant(b, a);
|
|
40
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* system-session/memory.ts — 记忆整理会话模块
|
|
3
|
+
*
|
|
4
|
+
* 写入只有两条路:记忆走 `ec memory create|update|archive`;整理反馈写 B/scratch/feedback.txt,
|
|
5
|
+
* 这是本会话唯一的可写文件(见 memoryGrants)。其余路径全部只读:
|
|
6
|
+
*
|
|
7
|
+
* B = 批次目录 R = relations/<peer>/memory A = personal/memory
|
|
8
|
+
* O = memory/owner/<owner> M = 本对话 messages.jsonl(单文件)
|
|
9
|
+
*
|
|
10
|
+
* notes B, R/processing, R/consolidated, R/MEMORY.md, M
|
|
11
|
+
* generalize B, A/consolidated, A/MEMORY.md, R/consolidated, M
|
|
12
|
+
* promote B, O/consolidated, O/MEMORY.md, R/consolidated, M
|
|
13
|
+
* profile B, R/consolidated, R/MEMORY.md, M
|
|
14
|
+
* forget B, X/consolidated, X/MEMORY.md(X=触发层级),X=relation 时加 M
|
|
15
|
+
*
|
|
16
|
+
* 详见 docs/memory/system-session-plugin-design.md §5-§8
|
|
17
|
+
*/
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import { resolveRoot } from '../../paths.js';
|
|
21
|
+
import { parseCodexToolCommand } from '../permission/ec-command-parser.js';
|
|
22
|
+
import { canonicalToolCall } from '../permission/tool-canonical.js';
|
|
23
|
+
import { checkReadonly } from '../permission/tool-policy.js';
|
|
24
|
+
import { analyzeReadonlyShellQuery } from '../permission/readonly-shell-query.js';
|
|
25
|
+
import { CONSOLIDATION_MODES } from '../memory/consolidation-context.js';
|
|
26
|
+
import { feedbackFilePath } from '../memory/consolidation-ops-records.js';
|
|
27
|
+
import { checkWriteTargets } from './grants.js';
|
|
28
|
+
export const MEMORY_SESSION_DENY_MESSAGE = '整理会话不允许执行此命令';
|
|
29
|
+
const MEMORY_MANIFEST = 'eck_manifest.memory.json';
|
|
30
|
+
const ALLOWED_MEMORY_SUBCOMMANDS = new Set(['create', 'update', 'archive']);
|
|
31
|
+
const BASH_COMMAND_RULE = 'Bash 只允许两类命令:单条 `ec memory create|update|archive`(不能与其他命令拼接);'
|
|
32
|
+
+ '或路径落在可读根内、可证明只读的查询(如 ls / cat / grep),多条查询可用 `&&` 或 `;` 连接,最多 8 段。'
|
|
33
|
+
+ '读文件请用 Read / Glob / Grep;整理反馈用文件写入工具写到任务书指定的反馈文件。';
|
|
34
|
+
function layerDir(layer, ctx) {
|
|
35
|
+
const root = resolveRoot();
|
|
36
|
+
if (layer === 'agent')
|
|
37
|
+
return path.join(root, 'agents', ctx.selfAid, 'personal', 'memory');
|
|
38
|
+
if (layer === 'relation') {
|
|
39
|
+
return ctx.peerKey ? path.join(root, 'agents', ctx.selfAid, 'relations', ctx.peerKey, 'memory') : undefined;
|
|
40
|
+
}
|
|
41
|
+
return ctx.ownerAid ? path.join(root, 'memory', 'owner', ctx.ownerAid) : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** 本模式的可读根。缺少输入事实(如无 peerKey)的条目直接不给,fail-closed。 */
|
|
44
|
+
export function memoryReadRoots(ctx) {
|
|
45
|
+
const roots = [];
|
|
46
|
+
const add = (...values) => {
|
|
47
|
+
for (const value of values)
|
|
48
|
+
if (value && path.isAbsolute(value))
|
|
49
|
+
roots.push(value);
|
|
50
|
+
};
|
|
51
|
+
const consolidated = (layer) => {
|
|
52
|
+
const dir = layerDir(layer, ctx);
|
|
53
|
+
return dir ? [path.join(dir, 'consolidated'), path.join(dir, 'MEMORY.md')] : [];
|
|
54
|
+
};
|
|
55
|
+
const relation = layerDir('relation', ctx);
|
|
56
|
+
const messages = ctx.chatDir ? path.join(ctx.chatDir, 'messages.jsonl') : undefined;
|
|
57
|
+
// 批次目录已覆盖反馈文件,不必单独列出
|
|
58
|
+
add(ctx.batchDir);
|
|
59
|
+
switch (ctx.subtype) {
|
|
60
|
+
case 'notes':
|
|
61
|
+
add(relation && path.join(relation, 'processing'), ...consolidated('relation'), messages);
|
|
62
|
+
break;
|
|
63
|
+
case 'generalize':
|
|
64
|
+
add(...consolidated('agent'), relation && path.join(relation, 'consolidated'), messages);
|
|
65
|
+
break;
|
|
66
|
+
case 'promote':
|
|
67
|
+
add(...consolidated('owner'), relation && path.join(relation, 'consolidated'), messages);
|
|
68
|
+
break;
|
|
69
|
+
case 'profile':
|
|
70
|
+
add(...consolidated('relation'), messages);
|
|
71
|
+
break;
|
|
72
|
+
case 'forget':
|
|
73
|
+
if (ctx.memoryScope === 'agent') {
|
|
74
|
+
// agent 级 forget:给所有 relation 的 consolidated 目录 + agent 自身的 consolidated
|
|
75
|
+
const relationsRoot = path.join(resolveRoot(), 'agents', ctx.selfAid, 'relations');
|
|
76
|
+
if (fs.existsSync(relationsRoot)) {
|
|
77
|
+
for (const peerKey of fs.readdirSync(relationsRoot)) {
|
|
78
|
+
const consolidatedDir = path.join(relationsRoot, peerKey, 'memory', 'consolidated');
|
|
79
|
+
const indexPath = path.join(relationsRoot, peerKey, 'memory', 'MEMORY.md');
|
|
80
|
+
if (fs.existsSync(consolidatedDir)) {
|
|
81
|
+
add(consolidatedDir, indexPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
add(...consolidated('agent'));
|
|
86
|
+
}
|
|
87
|
+
else if (ctx.memoryScope) {
|
|
88
|
+
// relation / owner 级 forget:只给当前层级
|
|
89
|
+
add(...consolidated(ctx.memoryScope));
|
|
90
|
+
if (ctx.memoryScope === 'relation')
|
|
91
|
+
add(messages);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
return [...new Set(roots)];
|
|
98
|
+
}
|
|
99
|
+
/** 文件权限声明:读按路径矩阵,写只有本批次的反馈文件一个。缺批次目录时不给任何可写文件。 */
|
|
100
|
+
export function memoryGrants(ctx) {
|
|
101
|
+
return {
|
|
102
|
+
readRoots: memoryReadRoots(ctx),
|
|
103
|
+
writeFiles: ctx.batchDir && path.isAbsolute(ctx.batchDir) ? [feedbackFilePath(ctx.batchDir)] : [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function deny(code, reason) {
|
|
107
|
+
return { allow: false, code, reason: `${MEMORY_SESSION_DENY_MESSAGE}:${reason}` };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 把 shell 解析失败翻译成可照做的补救动作。
|
|
111
|
+
*
|
|
112
|
+
* `ec memory create … "……"` 因正文含反引号被拒时,泛化文案会把病因指向"拼接了别的命令",
|
|
113
|
+
* 真正的病因是正文的引号选错了,所以按 issue 精确归因。
|
|
114
|
+
*/
|
|
115
|
+
function shellIssueRemedy(issue) {
|
|
116
|
+
switch (issue) {
|
|
117
|
+
case 'unsafe-expansion':
|
|
118
|
+
return '自由文本参数(--desc、正文、--keywords)里的反引号或 `$` 出现在双引号内,'
|
|
119
|
+
+ '会被 shell 当作命令替换或变量展开,所以无法确认它实际会执行什么。'
|
|
120
|
+
+ '请把这些参数改用单引号包住(如 --desc \'……\');'
|
|
121
|
+
+ '若正文本身含单引号,改用 $\'…\' 语法传入(支持 \\n \\t \\\\ \\\')。';
|
|
122
|
+
case 'invalid-quote':
|
|
123
|
+
return '命令引号未闭合或存在错误嵌套。'
|
|
124
|
+
+ '普通单引号内部不能再出现单引号——含单引号的正文改用 $\'…\' 语法:'
|
|
125
|
+
+ '用 \\\' 转义内部的单引号,如 $\'it\\\'s fine\'。';
|
|
126
|
+
case 'structural-newline':
|
|
127
|
+
return '命令里存在未闭合的换行结构。把正文放进单引号或 $\'…\' 内即可直接换行($\'…\' 支持 \\n 转义)。';
|
|
128
|
+
case 'shell-composition':
|
|
129
|
+
return '命令里含有 shell 拼接、管道或重定向(`;` `&&` `|` `$()` 等)。整理会话只允许单条 ec memory 命令。';
|
|
130
|
+
case 'unsupported-escape':
|
|
131
|
+
return '命令里使用了不受支持的转义写法。请改用单引号包住正文;若正文含单引号,改用 $\'…\' 语法传入。';
|
|
132
|
+
case 'lossy-cmd-shim':
|
|
133
|
+
return '命令经过 Windows cmd 包装后无法无歧义还原。请把正文改成不含换行的单条命令。';
|
|
134
|
+
case 'untrusted-shell':
|
|
135
|
+
return '命令带有不受信任的 shell 包装。请直接写 `ec memory …`,不要经由其他 shell 转发。';
|
|
136
|
+
default:
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function looksLikeMemoryCommand(command) {
|
|
141
|
+
return /^\s*ec(?:\.cmd|\.exe|\.ps1)?\s+memory\s+\S/.test(command);
|
|
142
|
+
}
|
|
143
|
+
/** 只读路径判定:复用 checkReadonly,可读根只来自 memoryGrants。 */
|
|
144
|
+
function checkReadWithinMatrix(toolName, input, ctx) {
|
|
145
|
+
return checkReadonly(toolName, input, ctx.projectPath, {
|
|
146
|
+
sessionId: ctx.sessionId,
|
|
147
|
+
selfAid: ctx.selfAid,
|
|
148
|
+
managedTempDir: ctx.managedTempDir,
|
|
149
|
+
grantedReadRoots: memoryGrants(ctx).readRoots,
|
|
150
|
+
excludeWorkspace: true,
|
|
151
|
+
allowProtectedMetadata: false,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** 写入判定只看 canonicalToolCall 给出的目标,不读工具入参。 */
|
|
155
|
+
function checkWrite(call, ctx) {
|
|
156
|
+
// shell 里的 apply_patch 跑的是 runner 自带的补丁器还是任意可执行文件,取决于 runner;
|
|
157
|
+
// 需要它的 runner 由适配层先转成 FileChange 再交给模块,模块这里一律不认。
|
|
158
|
+
if (call.via === 'shell') {
|
|
159
|
+
return deny('memory_session_shell_write_forbidden', '不能经由 shell 写文件,整理反馈请用文件写入工具写到任务书指定的反馈文件');
|
|
160
|
+
}
|
|
161
|
+
const { writeFiles } = memoryGrants(ctx);
|
|
162
|
+
if (writeFiles.length === 0) {
|
|
163
|
+
return deny('memory_session_write_no_target', '当前会话缺少批次目录,没有可写的反馈文件');
|
|
164
|
+
}
|
|
165
|
+
const result = checkWriteTargets(call.paths, writeFiles, ctx.projectPath);
|
|
166
|
+
if (result.ok)
|
|
167
|
+
return { allow: true };
|
|
168
|
+
const reason = result.reason === 'no-target'
|
|
169
|
+
? '无法确定写入目标'
|
|
170
|
+
: result.reason === 'not-regular-file'
|
|
171
|
+
? `写入目标不是普通文件(链接、目录或硬链接):${result.path}`
|
|
172
|
+
: `只能写反馈文件 ${writeFiles[0]},不能写 ${result.path}`;
|
|
173
|
+
return deny('memory_session_write_outside_grant', reason);
|
|
174
|
+
}
|
|
175
|
+
function isAllowedMemoryCommand(argv) {
|
|
176
|
+
return argv[0] === 'ec' && argv[1] === 'memory' && ALLOWED_MEMORY_SUBCOMMANDS.has(argv[2] ?? '');
|
|
177
|
+
}
|
|
178
|
+
function checkShell(input, ctx) {
|
|
179
|
+
const parsed = parseCodexToolCommand(input);
|
|
180
|
+
if (parsed.ok && isAllowedMemoryCommand(parsed.argv))
|
|
181
|
+
return { allow: true, ecCommand: true };
|
|
182
|
+
// Codex 没有 Read 工具,读任务书只能靠可证明只读的 shell 查询
|
|
183
|
+
const readonly = checkReadWithinMatrix('Bash', input, ctx);
|
|
184
|
+
if (readonly.behavior === 'allow')
|
|
185
|
+
return { allow: true };
|
|
186
|
+
const rawCommand = typeof input.command === 'string' ? input.command : '';
|
|
187
|
+
// ec 命令按 ec 解析器归因;ec 解析器的 issue 只描述 ec 命令,不能拿来解释普通 shell 查询
|
|
188
|
+
if (looksLikeMemoryCommand(rawCommand)) {
|
|
189
|
+
const remedy = parsed.ok ? undefined : shellIssueRemedy(parsed.issue);
|
|
190
|
+
if (remedy) {
|
|
191
|
+
return deny('memory_session_command_forbidden', `这条命令本身就是 \`ec memory\`,但未能解析成单条字面命令。${remedy}`);
|
|
192
|
+
}
|
|
193
|
+
return deny('memory_session_command_forbidden', `${BASH_COMMAND_RULE} 具体原因:ec memory 只允许 create / update / archive 三个子命令。`);
|
|
194
|
+
}
|
|
195
|
+
if ((parsed.ok && parsed.argv[0] === 'ec') || looksLikeEcCommand(rawCommand)) {
|
|
196
|
+
return deny('memory_session_command_forbidden', `${BASH_COMMAND_RULE} 具体原因:整理会话不能执行 ec memory create / update / archive 以外的 ec 命令。`);
|
|
197
|
+
}
|
|
198
|
+
// 普通 shell 查询:原因取自只读判定本身
|
|
199
|
+
return deny('memory_session_command_forbidden', `${BASH_COMMAND_RULE} 具体原因:${readonlyDenialHint(rawCommand, readonly.policyCode, ctx)}`);
|
|
200
|
+
}
|
|
201
|
+
function looksLikeEcCommand(command) {
|
|
202
|
+
return /^\s*ec(?:\.cmd|\.exe|\.ps1)?\s/.test(command);
|
|
203
|
+
}
|
|
204
|
+
/** 只读查询被拒的原因:与 checkReadonly 的判定分支一一对应。 */
|
|
205
|
+
function readonlyDenialHint(command, policyCode, ctx) {
|
|
206
|
+
switch (policyCode) {
|
|
207
|
+
case 'readonly_workspace_escape':
|
|
208
|
+
return '命令可证明只读,但读取的路径不在可读根内。用任务书「可读根」列出的绝对路径;相对路径按会话 cwd 解析,cwd 本身不可读。';
|
|
209
|
+
case 'readonly_h_class_read':
|
|
210
|
+
case 'readonly_l_class_read':
|
|
211
|
+
return '命令读取了受保护路径,整理会话不能读。';
|
|
212
|
+
default:
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
const analysis = analyzeReadonlyShellQuery(command, { managedTempDir: ctx.managedTempDir });
|
|
216
|
+
if (analysis.kind !== 'unproven')
|
|
217
|
+
return '命令无法证明只读。';
|
|
218
|
+
switch (analysis.reason) {
|
|
219
|
+
case 'unsupported-shell-structure':
|
|
220
|
+
return '命令含有不支持的 shell 结构:后台执行 `&`、`||`、输入重定向 `<`、换行,'
|
|
221
|
+
+ '或除 `2>/dev/null` 以外的重定向(如 `2>&1`、`> file`),或连接段数、管道级数超限。'
|
|
222
|
+
+ '多条查询只能用 `&&` 或 `;` 连接,最多 8 段。';
|
|
223
|
+
case 'non-literal-command':
|
|
224
|
+
return '命令参数不是字面量:未加引号的通配符(`*` `?` `[`)、变量 `$`、命令替换或无法解析的引号,'
|
|
225
|
+
+ '静态确定不了会读哪些文件。列文件请用 Glob 并给绝对 path。';
|
|
226
|
+
case 'unsupported-command-or-option':
|
|
227
|
+
return '命令或选项不在只读查询白名单内。不需要 `cd`:把绝对路径直接写进命令里。'
|
|
228
|
+
+ '读文件用 Read,列文件用 Glob,搜索用 Grep。';
|
|
229
|
+
case 'unsupported-pipeline-filter':
|
|
230
|
+
return '管道 `|` 后面的命令不是纯 stdin 过滤命令。';
|
|
231
|
+
case 'pipeline-filter-must-use-stdin':
|
|
232
|
+
return '管道 `|` 后面的过滤命令只能处理上一级的输出,不能再带文件路径参数。';
|
|
233
|
+
default:
|
|
234
|
+
return `命令无法证明只读(${analysis.reason})。`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function checkTool(rawToolName, input, ctx) {
|
|
238
|
+
const call = canonicalToolCall(rawToolName, input);
|
|
239
|
+
switch (call.access) {
|
|
240
|
+
case 'read': {
|
|
241
|
+
const result = checkReadWithinMatrix(call.name, input, ctx);
|
|
242
|
+
if (result.behavior !== 'allow') {
|
|
243
|
+
// 给出更具体的提示
|
|
244
|
+
let hint = '';
|
|
245
|
+
if (call.name === 'Glob' && !input.path) {
|
|
246
|
+
hint = '。Glob 需要传绝对路径给 path 参数(不传时起点落在 cwd,整理会话的 cwd 不可读)';
|
|
247
|
+
}
|
|
248
|
+
else if (call.name === 'Grep' && !input.path) {
|
|
249
|
+
hint = '。Grep 需要传绝对路径给 path 参数';
|
|
250
|
+
}
|
|
251
|
+
return deny(result.policyCode ?? 'memory_session_read_outside_matrix', `只能读取本次整理的批次目录、对应记忆目录与本对话流水${hint}`);
|
|
252
|
+
}
|
|
253
|
+
return { allow: true };
|
|
254
|
+
}
|
|
255
|
+
case 'write':
|
|
256
|
+
// 引号内恰好出现 apply_patch 字样的 ec memory 命令可能被保守识别成 shell 写入,先按 ec 命令认定
|
|
257
|
+
if (call.via === 'shell') {
|
|
258
|
+
const parsed = parseCodexToolCommand(input);
|
|
259
|
+
if (parsed.ok && isAllowedMemoryCommand(parsed.argv))
|
|
260
|
+
return { allow: true, ecCommand: true };
|
|
261
|
+
}
|
|
262
|
+
return checkWrite(call, ctx);
|
|
263
|
+
case 'shell':
|
|
264
|
+
return checkShell(input, ctx);
|
|
265
|
+
default:
|
|
266
|
+
return deny('memory_session_tool_forbidden', `整理会话不能使用 ${rawToolName};写记忆用 \`ec memory create|update|archive\`,整理反馈写到任务书指定的反馈文件`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function checkCommand(argv) {
|
|
270
|
+
if (argv[0] === 'memory' && ALLOWED_MEMORY_SUBCOMMANDS.has(argv[1] ?? ''))
|
|
271
|
+
return { allow: true };
|
|
272
|
+
return deny('memory_session_command_forbidden', '只允许 `ec memory create|update|archive`');
|
|
273
|
+
}
|
|
274
|
+
export const memorySystemSession = {
|
|
275
|
+
type: 'memory',
|
|
276
|
+
subtypes: CONSOLIDATION_MODES,
|
|
277
|
+
supportedRunners: ['claude', 'codex', 'ecagent'],
|
|
278
|
+
manifest: () => MEMORY_MANIFEST,
|
|
279
|
+
// 不启用 runner 自带的 OS 沙箱:整理会话要读写的路径在 $EVOLCORE_HOME 下,
|
|
280
|
+
// 与会话 cwd 互为兄弟目录,沙箱的「项目目录」模型套不上。边界改由 grants 单独管。
|
|
281
|
+
runnerLaunch: (_runner, ctx) => ({
|
|
282
|
+
sandbox: 'off',
|
|
283
|
+
runnerMode: 'readonly',
|
|
284
|
+
grants: memoryGrants(ctx),
|
|
285
|
+
}),
|
|
286
|
+
grants: memoryGrants,
|
|
287
|
+
checkTool,
|
|
288
|
+
checkCommand: argv => checkCommand(argv),
|
|
289
|
+
};
|