@shgroup/dsh-serenity-hooks 1.16.3 → 1.16.5

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/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.16.3",
3
+ "version": "1.16.5",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/loop + 拦截缝机械约束(safe-mode/路径守卫/会话落盘)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。私有(dsh-external 组织)。",
6
6
  "engines": {
@@ -16,7 +16,8 @@
16
16
  "eap",
17
17
  "neat",
18
18
  "cce",
19
- "loop"
19
+ "loop",
20
+ "localstore"
20
21
  ],
21
22
  "skills": []
22
23
  }
package/lib/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
- import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { appendFileSync, chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { execFile, execFileSync, spawnSync } from "node:child_process";
6
- import { platform } from "node:os";
6
+ import { homedir, platform } from "node:os";
7
7
  import { promisify } from "node:util";
8
- import { createUserMessage } from "@deepseek-ai/dsh-llm";
9
- import { randomBytes } from "node:crypto";
10
8
  import { fileURLToPath } from "node:url";
9
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
10
+ import { randomBytes, randomUUID } from "node:crypto";
11
11
  //#region src/ccc.ts
12
12
  /**
13
13
  * ccc.ts — CCC 纯逻辑层(零 DSH 依赖,可独立单测)
@@ -278,7 +278,7 @@ function runCcFs(root, args) {
278
278
  function agentCwd$6(exec) {
279
279
  return exec.agent?.session?.header?.cwd ?? process.cwd();
280
280
  }
281
- function renderText$8(value) {
281
+ function renderText$9(value) {
282
282
  return [{
283
283
  type: "text",
284
284
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -330,7 +330,7 @@ const ccFsTool = defineTool({
330
330
  },
331
331
  output: {
332
332
  schema: { type: "json" },
333
- render: (args, value) => renderText$8(value)
333
+ render: (args, value) => renderText$9(value)
334
334
  },
335
335
  async execute(args, exec) {
336
336
  const root = findSerenityRoot(agentCwd$6(exec));
@@ -909,7 +909,7 @@ function agentCwd$5(exec) {
909
909
  function agentScope$2(exec) {
910
910
  return exec.agent?.session?.id ?? "default";
911
911
  }
912
- function renderText$7(value) {
912
+ function renderText$8(value) {
913
913
  return [{
914
914
  type: "text",
915
915
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -940,7 +940,7 @@ const sessionTool = defineTool({
940
940
  },
941
941
  output: {
942
942
  schema: { type: "json" },
943
- render: (args, value) => renderText$7(value)
943
+ render: (args, value) => renderText$8(value)
944
944
  },
945
945
  async execute(args, exec) {
946
946
  const root = findSerenityRoot(agentCwd$5(exec));
@@ -986,6 +986,225 @@ const sessionTool = defineTool({
986
986
  }
987
987
  });
988
988
  //#endregion
989
+ //#region src/constants.ts
990
+ /** 常量(纯模块,零 DSH 依赖) */
991
+ /**
992
+ * ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
993
+ * 发布时只需改 package.json 的 version。
994
+ */
995
+ const ACC_VERSION = (() => {
996
+ try {
997
+ const here = dirname(fileURLToPath(import.meta.url));
998
+ return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8")).version ?? "0.0.0";
999
+ } catch {
1000
+ return "0.0.0";
1001
+ }
1002
+ })();
1003
+ //#endregion
1004
+ //#region src/seams/guards.ts
1005
+ /**
1006
+ * guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
1007
+ *
1008
+ * 纯决策逻辑(decideGuard)与 DSH 注册(registerGuards)分离:
1009
+ * 前者零 DSH 依赖可单测,后者把决策接到 tools/pre-execute 瀑布 + ctx.tools.guard 终局。
1010
+ *
1011
+ * 对应 opencode-serenity-plugin 的 tool.execute.before 路径守卫 + bash 开关 + 黑名单。
1012
+ */
1013
+ /**
1014
+ * 安全模式 + 黑名单 + P3 路径守卫的纯决策。
1015
+ * 对齐 opencode-serenity-plugin 标准:安全模式 = **bash 禁用** + 写入黑名单;
1016
+ * write/edit 等工具仅受路径逃逸与黑名单约束(不整体禁用)。
1017
+ * 优先级:safe-mode bash > 路径越界 > 黑名单命中。
1018
+ */
1019
+ function decideGuard(input) {
1020
+ const { root, toolName, safeModeOn, blacklist, pathArg } = input;
1021
+ if (safeModeOn && toolName === "bash") return {
1022
+ deny: `bash: 没有这个工具`,
1023
+ kind: "deny"
1024
+ };
1025
+ if (pathArg !== void 0) {
1026
+ const rel = relative(root, resolve(root, pathArg));
1027
+ if (rel.startsWith("..")) return {
1028
+ deny: `path escape blocked: "${pathArg}" 越出 CCC 根`,
1029
+ kind: "deny"
1030
+ };
1031
+ if (rel === ".serenity-safe-on" || rel === ".serenity" || rel.startsWith(".serenity-safe-on/") || rel.startsWith(".serenity/")) return {
1032
+ deny: `CCC 治理文件 "${rel}" 保留给用户,agent 不可写`,
1033
+ kind: "deny"
1034
+ };
1035
+ const hit = matchBlacklist(rel, blacklist);
1036
+ if (hit) return {
1037
+ deny: `blacklist blocked: "${pathArg}" 命中规则 "${hit}"`,
1038
+ kind: "deny"
1039
+ };
1040
+ }
1041
+ return { kind: "allow" };
1042
+ }
1043
+ /** 从 exec 提取 agent 会话 cwd(CCC 根检测基准);无则回退进程 cwd */
1044
+ function resolveAgentCwd(exec) {
1045
+ return exec.agent?.session?.header?.cwd ?? process.cwd();
1046
+ }
1047
+ /** 安全模式开启时从模型工具列表隐藏的工具(只隐藏 bash;write/edit 保留) */
1048
+ const SAFE_MODE_DENY_TOOLS = ["bash"];
1049
+ /** agent key → restrict disposer(安全模式工具隐藏状态) */
1050
+ const safeModeRestrictions = /* @__PURE__ */ new Map();
1051
+ const restrictDiag = {
1052
+ lastKey: null,
1053
+ lastAttemptAt: null,
1054
+ lastSuccess: null,
1055
+ lastError: null,
1056
+ activeKeys: []
1057
+ };
1058
+ function getRestrictDiagnostics() {
1059
+ return {
1060
+ ...restrictDiag,
1061
+ activeKeys: [...safeModeRestrictions.keys()]
1062
+ };
1063
+ }
1064
+ /** 诊断落盘:AGENT_SESSIONS/.restrict-diag.json(文件通道,避免 HTTP 自锁) */
1065
+ function writeRestrictDiag(root) {
1066
+ try {
1067
+ const dir = resolve(root, "AGENT_SESSIONS");
1068
+ mkdirSync(dir, { recursive: true });
1069
+ writeFileSync(resolve(dir, ".restrict-diag.json"), JSON.stringify({
1070
+ ...getRestrictDiagnostics(),
1071
+ cccRoot: root
1072
+ }, null, 2) + "\n", "utf-8");
1073
+ } catch {}
1074
+ }
1075
+ /**
1076
+ * 同步安全模式工具隐藏:标记存在 → agent.ctx.tools.restrict deny 隐藏写工具;
1077
+ * 标记消失 → 解除。pre-step 每步调用 → 切换实时生效。
1078
+ */
1079
+ function syncSafeModeRestriction(agent, root) {
1080
+ const key = agent.session.id ?? "global";
1081
+ const on = isSafeModeOn(root);
1082
+ const existing = safeModeRestrictions.get(key);
1083
+ if (on && !existing) try {
1084
+ const dispose = agent.ctx.tools.restrict({ deny: [...SAFE_MODE_DENY_TOOLS] });
1085
+ safeModeRestrictions.set(key, dispose);
1086
+ restrictDiag.lastKey = key;
1087
+ restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
1088
+ restrictDiag.lastSuccess = true;
1089
+ restrictDiag.lastError = null;
1090
+ } catch (e) {
1091
+ restrictDiag.lastKey = key;
1092
+ restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
1093
+ restrictDiag.lastSuccess = false;
1094
+ restrictDiag.lastError = e.message;
1095
+ console.error(`[serenity-hooks] restrict 失败 (key=${key}):`, e.message);
1096
+ }
1097
+ else if (!on && existing) {
1098
+ try {
1099
+ existing();
1100
+ } catch (e) {
1101
+ console.error(`[serenity-hooks] restrict 解除失败 (key=${key}):`, e.message);
1102
+ }
1103
+ safeModeRestrictions.delete(key);
1104
+ }
1105
+ writeRestrictDiag(root);
1106
+ }
1107
+ /** 从 exec 参数中提取常见路径字段(write/edit 工具);宽松读取 */
1108
+ function extractPathArg(exec) {
1109
+ const args = exec.arguments;
1110
+ if (args === null || typeof args !== "object") return void 0;
1111
+ const a = args;
1112
+ for (const key of [
1113
+ "path",
1114
+ "file_path",
1115
+ "target",
1116
+ "dst"
1117
+ ]) {
1118
+ const v = a[key];
1119
+ if (typeof v === "string") return v;
1120
+ }
1121
+ }
1122
+ /**
1123
+ * 注册守卫:tools/pre-execute 瀑布 + ctx.tools.guard 终局。
1124
+ * 两者都从 CCC 根实时读取配置(无状态、无缓存)。
1125
+ */
1126
+ function registerGuards(ctx, opts = {}) {
1127
+ const configPaths = opts.configPaths;
1128
+ const evaluate = (exec) => {
1129
+ const root = findSerenityRoot(resolveAgentCwd(exec));
1130
+ if (!root) return { kind: "allow" };
1131
+ const safeModeOn = isSafeModeOn(root);
1132
+ const blacklist = readBlacklist(root, configPaths);
1133
+ const pathArg = extractPathArg(exec);
1134
+ return decideGuard({
1135
+ root,
1136
+ toolName: exec.name,
1137
+ safeModeOn,
1138
+ blacklist,
1139
+ pathArg
1140
+ });
1141
+ };
1142
+ ctx.on("tools/pre-execute", async (exec, next) => {
1143
+ const d = evaluate(exec);
1144
+ if (d.deny) return {
1145
+ kind: "deny",
1146
+ reason: d.deny
1147
+ };
1148
+ return next();
1149
+ });
1150
+ ctx.tools.guard((exec) => {
1151
+ return evaluate(exec).deny;
1152
+ });
1153
+ }
1154
+ //#endregion
1155
+ //#region src/status.ts
1156
+ /**
1157
+ * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
1158
+ *
1159
+ * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / loop 模型。
1160
+ * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
1161
+ */
1162
+ /** 读取已安装 DSH CLI 版本(npm 全局 @deepseek-ai/dsh);读不到返回 null */
1163
+ function readDshVersion() {
1164
+ try {
1165
+ const pkg = JSON.parse(readFileSync(join(homedir(), ".npm-global", "lib", "node_modules", "@deepseek-ai", "dsh", "package.json"), "utf-8"));
1166
+ return typeof pkg.version === "string" ? pkg.version : null;
1167
+ } catch {
1168
+ return null;
1169
+ }
1170
+ }
1171
+ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
1172
+ const root = findSerenityRoot(cwd);
1173
+ const restrict = getRestrictDiagnostics();
1174
+ const common = {
1175
+ accVersion: ACC_VERSION,
1176
+ dshVersion: readDshVersion(),
1177
+ nodeVersion: process.version
1178
+ };
1179
+ if (!root) return {
1180
+ root: null,
1181
+ ...common,
1182
+ safeModeOn: false,
1183
+ blacklist: [],
1184
+ threshold: null,
1185
+ loopModel: null,
1186
+ restrict
1187
+ };
1188
+ const cfg = loadSerenityConfig(root, configPaths);
1189
+ return {
1190
+ root,
1191
+ ...common,
1192
+ safeModeOn: isSafeModeOn(root),
1193
+ blacklist: readBlacklist(root, configPaths),
1194
+ threshold: cfg.sessionKeeper?.threshold ?? null,
1195
+ loopModel: cfg.loop?.defaultModel ?? null,
1196
+ restrict
1197
+ };
1198
+ }
1199
+ /** 切换安全模式(写/删标记文件);返回实际生效状态 */
1200
+ function setSafeMode(root, on) {
1201
+ const marker = resolve(root, SAFE_MODE_MARKER);
1202
+ if (on) {
1203
+ if (!existsSync(marker)) writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
1204
+ } else rmSync(marker, { force: true });
1205
+ return { on: isSafeModeOn(root) };
1206
+ }
1207
+ //#endregion
989
1208
  //#region src/kit-ops.ts
990
1209
  /**
991
1210
  * kit-ops.ts — acc_kit 纯操作层(零 DSH 依赖)
@@ -1025,7 +1244,9 @@ function runKit(root, args) {
1025
1244
  configPath,
1026
1245
  p1: findSerenityRoot(root) !== null,
1027
1246
  p2: gitRoot !== null,
1028
- p3: "enforced-by-dsh-fs-sandbox"
1247
+ p3: "enforced-by-dsh-fs-sandbox",
1248
+ accVersion: ACC_VERSION,
1249
+ dshVersion: readDshVersion()
1029
1250
  };
1030
1251
  }
1031
1252
  case "time": return (/* @__PURE__ */ new Date()).toISOString();
@@ -1046,7 +1267,7 @@ function runKit(root, args) {
1046
1267
  function agentCwd$4(exec) {
1047
1268
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1048
1269
  }
1049
- function renderText$6(value) {
1270
+ function renderText$7(value) {
1050
1271
  return [{
1051
1272
  type: "text",
1052
1273
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1069,7 +1290,7 @@ const kitTool = defineTool({
1069
1290
  },
1070
1291
  output: {
1071
1292
  schema: { type: "json" },
1072
- render: (args, value) => renderText$6(value)
1293
+ render: (args, value) => renderText$7(value)
1073
1294
  },
1074
1295
  async execute(args, exec) {
1075
1296
  const root = findSerenityRoot(agentCwd$4(exec));
@@ -1182,7 +1403,7 @@ function runGit(root, args) {
1182
1403
  function agentCwd$3(exec) {
1183
1404
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1184
1405
  }
1185
- function renderText$5(value) {
1406
+ function renderText$6(value) {
1186
1407
  return [{
1187
1408
  type: "text",
1188
1409
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1209,7 +1430,7 @@ const gitTool = defineTool({
1209
1430
  },
1210
1431
  output: {
1211
1432
  schema: { type: "json" },
1212
- render: (args, value) => renderText$5(value)
1433
+ render: (args, value) => renderText$6(value)
1213
1434
  },
1214
1435
  async execute(args, exec) {
1215
1436
  const root = findSerenityRoot(agentCwd$3(exec));
@@ -1225,7 +1446,7 @@ const gitTool = defineTool({
1225
1446
  function agentCwd$2(exec) {
1226
1447
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1227
1448
  }
1228
- function renderText$4(value) {
1449
+ function renderText$5(value) {
1229
1450
  return [{
1230
1451
  type: "text",
1231
1452
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1269,7 +1490,7 @@ const msmTool = defineTool({
1269
1490
  },
1270
1491
  output: {
1271
1492
  schema: { type: "json" },
1272
- render: (args, value) => renderText$4(value)
1493
+ render: (args, value) => renderText$5(value)
1273
1494
  },
1274
1495
  async execute(args, exec) {
1275
1496
  const root = findSerenityRoot(agentCwd$2(exec));
@@ -1310,7 +1531,7 @@ CCC(home-serenity 等)认知内容编码为 skill/SESSION/设计文档。产
1310
1531
 
1311
1532
  ## 参考
1312
1533
  https://github.com/tellmewhattodo/theory-eap`;
1313
- function renderText$3(value) {
1534
+ function renderText$4(value) {
1314
1535
  return [{
1315
1536
  type: "text",
1316
1537
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1330,7 +1551,7 @@ const eapTool = defineTool({
1330
1551
  } },
1331
1552
  output: {
1332
1553
  schema: { type: "string" },
1333
- render: (_args, value) => renderText$3(value)
1554
+ render: (_args, value) => renderText$4(value)
1334
1555
  },
1335
1556
  async execute(args) {
1336
1557
  if (args.section === "variables") return EAP_CONTENT.split("## 输出前自检清单")[0];
@@ -1371,7 +1592,7 @@ const NEAT_CONTENT = `# Neat 设计协作协议
1371
1592
  2. 等待确认或修正(小步)
1372
1593
  3. 确认后记录决策(写入会话 SESSION.md 或设计文档)
1373
1594
  4. 推进到下一决策点`;
1374
- function renderText$2(value) {
1595
+ function renderText$3(value) {
1375
1596
  return [{
1376
1597
  type: "text",
1377
1598
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1387,7 +1608,7 @@ const neatTool = defineTool({
1387
1608
  } },
1388
1609
  output: {
1389
1610
  schema: { type: "string" },
1390
- render: (_args, value) => renderText$2(value)
1611
+ render: (_args, value) => renderText$3(value)
1391
1612
  },
1392
1613
  async execute(args) {
1393
1614
  if (args.section === "rules") return NEAT_CONTENT.match(/## 四条铁律[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
@@ -1450,7 +1671,7 @@ CCE 回答"有结构的知识应如何跨时间持续演化而不丧失连贯性
1450
1671
  ## 与 Serenity 的关系
1451
1672
  Serenity 的会话系统、会话追踪、熵管理机制(SQC 品质循环)都是 CCE 的工程实现;
1452
1673
  CCC 系统提示词中嵌入的行为约束即来自 CCE。`;
1453
- function renderText$1(value) {
1674
+ function renderText$2(value) {
1454
1675
  return [{
1455
1676
  type: "text",
1456
1677
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1471,7 +1692,7 @@ const cceTool = defineTool({
1471
1692
  } },
1472
1693
  output: {
1473
1694
  schema: { type: "string" },
1474
- render: (_args, value) => renderText$1(value)
1695
+ render: (_args, value) => renderText$2(value)
1475
1696
  },
1476
1697
  async execute(args) {
1477
1698
  const section = args.section;
@@ -1616,7 +1837,7 @@ ${resumeNote}
1616
1837
  function agentCwd$1(exec) {
1617
1838
  return (exec.agent?.session)?.header?.cwd ?? process.cwd();
1618
1839
  }
1619
- function renderText(value) {
1840
+ function renderText$1(value) {
1620
1841
  return [{
1621
1842
  type: "text",
1622
1843
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1655,7 +1876,7 @@ function lastAssistantText(agent) {
1655
1876
  function createLoopTool(ctx) {
1656
1877
  return defineTool({
1657
1878
  name: "loop",
1658
- description: "牛马循环(老 loop 等效):用指定模型创建专用 agent 反复执行任务直到完成或达轮次上限。\n用法:loop 接受 task(要完成的目标)或依赖 session 上下文;模型缺省读 .dsh/serenity.json 的 loop.defaultModel(当前 minimax-cn-coding-plan/MiniMax-M3,廉价牛马)。\n行为:每轮创建全新 agent 工作(读文件/改代码/执行命令),汇报进度后进入下一轮;完成时输出停止标记即终止。**每轮等待无超时**(loop 永续:agent 工作多久等多久,不被超时打断)。\n进度:写入 AGENT_SESSIONS/loop-<label>.md/.json;同 label 再次调用从上次轮次续跑(不重做)。\n约束:loop agent 受完整 Serenity 约束(ACC 身份/入口技能系统提示词/守卫/session-keeper)。\n示例:loop 执行「扫描 SQC 并修复 DC 问题」,label: sqc-scan,maxRounds: 5",
1879
+ description: "牛马循环(老 loop 等效):用指定模型创建专用 agent 反复执行任务直到完成。\n用法:loop 接受 task(要完成的目标)或依赖 session 上下文;模型缺省读 .dsh/serenity.json 的 loop.defaultModel(当前 minimax-cn-coding-plan/MiniMax-M3,廉价牛马)。\n行为:内部硬性 while 循环驱动 agent 逐轮推进任务,每轮等待无超时(agent 工作多久等多久)。唯一完成条件 = agent 精确回显本轮随机完成码(stop token),防止低智能模型提前结束。调用者不关心轮数——任务交给 loop,完成即返回。\n进度:写入 AGENT_SESSIONS/loop-<label>.md/.json;同 label 再次调用从上次轮次续跑(不重做)。\n约束:loop agent 受完整 Serenity 约束(ACC 身份/入口技能系统提示词/守卫/session-keeper)。\n示例:loop 执行「扫描 SQC 并修复 DC 问题」,label: sqc-scan",
1659
1880
  parameters: {
1660
1881
  task: {
1661
1882
  type: "string",
@@ -1676,12 +1897,12 @@ function createLoopTool(ctx) {
1676
1897
  },
1677
1898
  maxRounds: {
1678
1899
  type: "integer",
1679
- description: "轮次上限(默认 100"
1900
+ description: "保险阀(防死循环,默认 100;调用者通常无需设置——loop 跑到完成或达此上限)"
1680
1901
  }
1681
1902
  },
1682
1903
  output: {
1683
1904
  schema: { type: "json" },
1684
- render: (_args, value) => renderText(value)
1905
+ render: (_args, value) => renderText$1(value)
1685
1906
  },
1686
1907
  async execute(args, exec) {
1687
1908
  const root = findSerenityRoot(agentCwd$1(exec));
@@ -1695,11 +1916,11 @@ function createLoopTool(ctx) {
1695
1916
  const { provider, model: modelName } = splitModel(model);
1696
1917
  const stopToken = newStopToken();
1697
1918
  let progress = readProgress(root, label);
1698
- const startRound = progress ? Math.min(progress.round + 1, maxRounds) : 1;
1919
+ const startRound = progress ? progress.round + 1 : 1;
1699
1920
  const parentCtx = exec.agent?.ctx;
1700
1921
  const inherited = loopPresetInheritance(parentCtx);
1701
1922
  if (!ctx.agents) throw new Error("loop: ctx.agents 不可用");
1702
- const sessionId = `loop-${label}`;
1923
+ const sessionId = `loop-${label}-${randomUUID()}`;
1703
1924
  const handle = await ctx.agents.create({
1704
1925
  sessionId,
1705
1926
  meta: {
@@ -1775,166 +1996,364 @@ function createLoopTool(ctx) {
1775
1996
  progressFile: json,
1776
1997
  lastResponse: lastResponse.slice(0, 2e3),
1777
1998
  usage: {
1778
- how: "loop 用指定模型(默认 M3)创建专用 agent 每轮独立执行任务,完成时输出停止标记即终止",
1999
+ how: "loop 内部硬性 while 驱动 agent 逐轮推进,agent 精确回显随机完成码即终止",
1779
2000
  progress: `进度在 AGENT_SESSIONS/loop-${label}.md 与 .json;同 label 再调 loop 会从下一轮续跑(不重做)`,
1780
2001
  constraints: "loop agent 受完整 Serenity 约束(ACC 身份/入口技能系统提示词/守卫)",
1781
- next: done ? "任务已完成;可查看进度文件收尾" : `任务未完成(${finalRound}/${maxRounds} 轮);可同 label 续跑或调整 maxRounds`
2002
+ next: done ? "任务已完成;可查看进度文件收尾" : `任务未完成(已达保险阀 ${maxRounds} 轮);可同 label 续跑`
1782
2003
  }
1783
2004
  };
1784
2005
  }
1785
2006
  });
1786
2007
  }
1787
2008
  //#endregion
1788
- //#region src/seams/guards.ts
2009
+ //#region src/localstore-ops.ts
1789
2010
  /**
1790
- * guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
2011
+ * localstore-ops.ts — localstore 纯逻辑层(零 DSH 依赖,可独立单测)
1791
2012
  *
1792
- * 纯决策逻辑(decideGuard)与 DSH 注册(registerGuards)分离:
1793
- * 前者零 DSH 依赖可单测,后者把决策接到 tools/pre-execute 瀑布 + ctx.tools.guard 终局。
2013
+ * ACC 标准本地凭据/配置存储(S133 设计):
2014
+ * - 一个工具管理两个命名空间:credential(凭据,0600)+ config(偏好,0644)
2015
+ * - 存储于 ~/.serenity/(平台感知:win %USERPROFILE%\.serenity\)
2016
+ * - 两个 YAML 文件:credentials.yaml(扁平 REF→value)+ settings.yaml(命名空间分节)
2017
+ * - 目录 0700
1794
2018
  *
1795
- * 对应 opencode-serenity-plugin tool.execute.before 路径守卫 + bash 开关 + 黑名单。
1796
- */
1797
- /**
1798
- * 安全模式 + 黑名单 + P3 路径守卫的纯决策。
1799
- * 对齐 opencode-serenity-plugin 标准:安全模式 = **bash 禁用** + 写入黑名单;
1800
- * write/edit 等工具仅受路径逃逸与黑名单约束(不整体禁用)。
1801
- * 优先级:safe-mode bash > 路径越界 > 黑名单命中。
2019
+ * YAML 用轻量自实现子集(零依赖):扁平 `KEY: value` 映射 + `#` 注释。
2020
+ * 凭据/配置本质是给 MSM/agent 用的普通 YAML 文件——agent 可按 doc 子命令
2021
+ * 说明直接用 fs 工具(read/write)操作;本工具是管理入口 + 规范文档。
1802
2022
  */
1803
- function decideGuard(input) {
1804
- const { root, toolName, safeModeOn, blacklist, pathArg } = input;
1805
- if (safeModeOn && toolName === "bash") return {
1806
- deny: `bash: 没有这个工具`,
1807
- kind: "deny"
1808
- };
1809
- if (pathArg !== void 0) {
1810
- const rel = relative(root, resolve(root, pathArg));
1811
- if (rel.startsWith("..")) return {
1812
- deny: `path escape blocked: "${pathArg}" 越出 CCC 根`,
1813
- kind: "deny"
1814
- };
1815
- if (rel === ".serenity-safe-on" || rel === ".serenity" || rel.startsWith(".serenity-safe-on/") || rel.startsWith(".serenity/")) return {
1816
- deny: `CCC 治理文件 "${rel}" 保留给用户,agent 不可写`,
1817
- kind: "deny"
1818
- };
1819
- const hit = matchBlacklist(rel, blacklist);
1820
- if (hit) return {
1821
- deny: `blacklist blocked: "${pathArg}" 命中规则 "${hit}"`,
1822
- kind: "deny"
1823
- };
2023
+ const LOCALSTORE_SCOPES = ["credential", "config"];
2024
+ /** 根目录名(~/.serenity) */
2025
+ const STORE_DIR_NAME = ".serenity";
2026
+ /** 凭据文件(0600) */
2027
+ const CREDENTIALS_FILENAME = "credentials.yaml";
2028
+ /** 配置文件(0644) */
2029
+ const SETTINGS_FILENAME = "settings.yaml";
2030
+ /** 凭据 key 规范:大写蛇形(命名空间前缀),如 HOME_GITLAB_TOKEN */
2031
+ const CREDENTIAL_KEY_RE = /^[A-Z][A-Z0-9_]*$/;
2032
+ /** 配置节名规范:小写字母数字连字符 */
2033
+ const CONFIG_SECTION_RE = /^[a-z][a-z0-9-]*$/;
2034
+ /** 配置 key 规范(节内):小驼峰(首字母小写,可含大写),如 defaultModel */
2035
+ const CONFIG_KEY_RE = /^[a-z][a-zA-Z0-9_]*$/;
2036
+ /** 解析 ~/.serenity 根(平台感知:os.homedir() 三平台统一) */
2037
+ function serenityDir() {
2038
+ return join(homedir(), STORE_DIR_NAME);
2039
+ }
2040
+ /** 各命名空间的文件绝对路径 */
2041
+ function storeFilePath(scope) {
2042
+ return scope === "credential" ? join(serenityDir(), CREDENTIALS_FILENAME) : join(serenityDir(), SETTINGS_FILENAME);
2043
+ }
2044
+ /** 确保目录存在并设 0700 */
2045
+ function ensureDir(dir) {
2046
+ mkdirSync(dir, { recursive: true });
2047
+ try {
2048
+ chmodSync(dir, 448);
2049
+ } catch {}
2050
+ }
2051
+ /** 设置文件权限:credential 0600 / config 0644 */
2052
+ function applyFileMode(scope, path) {
2053
+ try {
2054
+ chmodSync(path, scope === "credential" ? 384 : 420);
2055
+ } catch {}
2056
+ }
2057
+ /** 解析扁平映射 YAML → Record<string,string>(忽略 # 注释与空行;值去引号) */
2058
+ function parseFlatYaml(text) {
2059
+ const out = {};
2060
+ for (const rawLine of text.split("\n")) {
2061
+ const line = rawLine.trim();
2062
+ if (line === "" || line.startsWith("#")) continue;
2063
+ const idx = line.indexOf(":");
2064
+ if (idx <= 0) continue;
2065
+ const key = line.slice(0, idx).trim();
2066
+ let value = line.slice(idx + 1).trim();
2067
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2068
+ out[key] = value;
1824
2069
  }
1825
- return { kind: "allow" };
2070
+ return out;
1826
2071
  }
1827
- /** exec 提取 agent 会话 cwd(CCC 根检测基准);无则回退进程 cwd */
1828
- function resolveAgentCwd(exec) {
1829
- return exec.agent?.session?.header?.cwd ?? process.cwd();
2072
+ /** 序列化扁平映射 YAML(保持 key 顺序,值引号包裹含特殊字符的) */
2073
+ function renderFlatYaml(entries) {
2074
+ const lines = [];
2075
+ for (const [key, value] of Object.entries(entries)) {
2076
+ const rendered = /[:#\n]/.test(value) || value === "" || /^\s/.test(value) || /\s$/.test(value) ? JSON.stringify(value) : value;
2077
+ lines.push(`${key}: ${rendered}`);
2078
+ }
2079
+ return lines.join("\n") + (lines.length > 0 ? "\n" : "");
2080
+ }
2081
+ /** 解析分节 YAML(config):节 → key → value */
2082
+ function parseSectionedYaml(text) {
2083
+ const out = {};
2084
+ let section = "";
2085
+ for (const rawLine of text.split("\n")) {
2086
+ const line = rawLine.trim();
2087
+ if (line === "" || line.startsWith("#")) continue;
2088
+ if (rawLine.length - rawLine.trimStart().length === 0 && line.endsWith(":")) {
2089
+ section = line.slice(0, -1).trim();
2090
+ if (!out[section]) out[section] = {};
2091
+ continue;
2092
+ }
2093
+ if (section === "") continue;
2094
+ const idx = line.indexOf(":");
2095
+ if (idx <= 0) continue;
2096
+ const key = line.slice(0, idx).trim();
2097
+ let value = line.slice(idx + 1).trim();
2098
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
2099
+ const sectionMap = out[section] ??= {};
2100
+ sectionMap[key] = value;
2101
+ }
2102
+ return out;
1830
2103
  }
1831
- /** 安全模式开启时从模型工具列表隐藏的工具(只隐藏 bash;write/edit 保留) */
1832
- const SAFE_MODE_DENY_TOOLS = ["bash"];
1833
- /** agent key → restrict disposer(安全模式工具隐藏状态) */
1834
- const safeModeRestrictions = /* @__PURE__ */ new Map();
1835
- const restrictDiag = {
1836
- lastKey: null,
1837
- lastAttemptAt: null,
1838
- lastSuccess: null,
1839
- lastError: null,
1840
- activeKeys: []
1841
- };
1842
- function getRestrictDiagnostics() {
2104
+ /** 序列化分节 YAML(config):节 key → value */
2105
+ function renderSectionedYaml(sections) {
2106
+ const lines = [];
2107
+ for (const [section, entries] of Object.entries(sections)) {
2108
+ lines.push(`${section}:`);
2109
+ for (const [key, value] of Object.entries(entries)) {
2110
+ const rendered = /[:#\n]/.test(value) || value === "" || /^\s/.test(value) || /\s$/.test(value) ? JSON.stringify(value) : value;
2111
+ lines.push(` ${key}: ${rendered}`);
2112
+ }
2113
+ }
2114
+ return lines.join("\n") + (lines.length > 0 ? "\n" : "");
2115
+ }
2116
+ /** 读取命名空间全部条目(文件不存在返回空) */
2117
+ function readStore(scope) {
2118
+ const path = storeFilePath(scope);
2119
+ if (!existsSync(path)) return scope === "credential" ? {} : {};
2120
+ const text = readFileSync(path, "utf-8");
2121
+ return scope === "credential" ? parseFlatYaml(text) : parseSectionedYaml(text);
2122
+ }
2123
+ /** 校验凭据 key 合法(大写蛇形);不合法抛错 */
2124
+ function assertCredentialKey(key) {
2125
+ if (!CREDENTIAL_KEY_RE.test(key)) throw new Error(`credential key "${key}" 必须匹配大写蛇形 ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)`);
2126
+ }
2127
+ /** 校验 config 路径(节.key);不合法抛错 */
2128
+ function assertConfigPath(path) {
2129
+ const idx = path.indexOf(".");
2130
+ if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" 必须为 section.key(如 loop.defaultModel)`);
2131
+ const section = path.slice(0, idx);
2132
+ const key = path.slice(idx + 1);
2133
+ if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config 节 "${section}" 必须匹配 ^[a-z][a-z0-9-]*$`);
2134
+ if (!CONFIG_KEY_RE.test(key)) throw new Error(`config key "${key}" 必须匹配小驼峰 ^[a-z][a-zA-Z0-9_]*$(如 defaultModel)`);
1843
2135
  return {
1844
- ...restrictDiag,
1845
- activeKeys: [...safeModeRestrictions.keys()]
2136
+ section,
2137
+ key
1846
2138
  };
1847
2139
  }
1848
- /** 诊断落盘:AGENT_SESSIONS/.restrict-diag.json(文件通道,避免 HTTP 自锁) */
1849
- function writeRestrictDiag(root) {
1850
- try {
1851
- const dir = resolve(root, "AGENT_SESSIONS");
1852
- mkdirSync(dir, { recursive: true });
1853
- writeFileSync(resolve(dir, ".restrict-diag.json"), JSON.stringify({
1854
- ...getRestrictDiagnostics(),
1855
- cccRoot: root
1856
- }, null, 2) + "\n", "utf-8");
1857
- } catch {}
2140
+ /** 写入单个条目(自动建目录/设权限;保留其他条目与注释行外内容) */
2141
+ function writeEntry(scope, name, value) {
2142
+ const path = storeFilePath(scope);
2143
+ ensureDir(serenityDir());
2144
+ if (scope === "credential") {
2145
+ assertCredentialKey(name);
2146
+ const entries = parseFlatYaml(existsSync(path) ? readFileSync(path, "utf-8") : "");
2147
+ entries[name] = value;
2148
+ writeFileSync(path, renderFlatYaml(entries), "utf-8");
2149
+ } else {
2150
+ const { section, key } = assertConfigPath(name);
2151
+ const sections = parseSectionedYaml(existsSync(path) ? readFileSync(path, "utf-8") : "");
2152
+ if (!sections[section]) sections[section] = {};
2153
+ sections[section][key] = value;
2154
+ writeFileSync(path, renderSectionedYaml(sections), "utf-8");
2155
+ }
2156
+ applyFileMode(scope, path);
2157
+ }
2158
+ /** 删除单个条目(不存在返回 false) */
2159
+ function unsetEntry(scope, name) {
2160
+ const path = storeFilePath(scope);
2161
+ if (!existsSync(path)) return false;
2162
+ if (scope === "credential") {
2163
+ assertCredentialKey(name);
2164
+ const entries = parseFlatYaml(readFileSync(path, "utf-8"));
2165
+ if (!(name in entries)) return false;
2166
+ delete entries[name];
2167
+ writeFileSync(path, renderFlatYaml(entries), "utf-8");
2168
+ return true;
2169
+ }
2170
+ const { section, key } = assertConfigPath(name);
2171
+ const sections = parseSectionedYaml(readFileSync(path, "utf-8"));
2172
+ const sectionMap = sections[section];
2173
+ if (!sectionMap || !(key in sectionMap)) return false;
2174
+ delete sectionMap[key];
2175
+ if (Object.keys(sectionMap).length === 0) delete sections[section];
2176
+ writeFileSync(path, renderSectionedYaml(sections), "utf-8");
2177
+ return true;
2178
+ }
2179
+ /** 读取单个条目值;不存在返回 null */
2180
+ function getEntry(scope, name) {
2181
+ const path = storeFilePath(scope);
2182
+ if (!existsSync(path)) return null;
2183
+ if (scope === "credential") {
2184
+ assertCredentialKey(name);
2185
+ return parseFlatYaml(readFileSync(path, "utf-8"))[name] ?? null;
2186
+ }
2187
+ const { section, key } = assertConfigPath(name);
2188
+ return parseSectionedYaml(readFileSync(path, "utf-8"))[section]?.[key] ?? null;
2189
+ }
2190
+ /** 列出 key(凭据只返回 key 名,不返回值) */
2191
+ function listKeys(scope) {
2192
+ const data = readStore(scope);
2193
+ if (scope === "credential") return Object.keys(data);
2194
+ return Object.entries(data).flatMap(([section, entries]) => Object.keys(entries).map((key) => `${section}.${key}`));
1858
2195
  }
1859
2196
  /**
1860
- * 同步安全模式工具隐藏:标记存在 agent.ctx.tools.restrict deny 隐藏写工具;
1861
- * 标记消失 解除。pre-step 每步调用 → 切换实时生效。
2197
+ * doc 说明文本:输出存储位置/格式/key 规范/权限/读写方法/安全边界。
2198
+ * agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
1862
2199
  */
1863
- function syncSafeModeRestriction(agent, root) {
1864
- const key = agent.session.id ?? "global";
1865
- const on = isSafeModeOn(root);
1866
- const existing = safeModeRestrictions.get(key);
1867
- if (on && !existing) try {
1868
- const dispose = agent.ctx.tools.restrict({ deny: [...SAFE_MODE_DENY_TOOLS] });
1869
- safeModeRestrictions.set(key, dispose);
1870
- restrictDiag.lastKey = key;
1871
- restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
1872
- restrictDiag.lastSuccess = true;
1873
- restrictDiag.lastError = null;
1874
- } catch (e) {
1875
- restrictDiag.lastKey = key;
1876
- restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
1877
- restrictDiag.lastSuccess = false;
1878
- restrictDiag.lastError = e.message;
1879
- console.error(`[serenity-hooks] restrict 失败 (key=${key}):`, e.message);
1880
- }
1881
- else if (!on && existing) {
1882
- try {
1883
- existing();
1884
- } catch (e) {
1885
- console.error(`[serenity-hooks] restrict 解除失败 (key=${key}):`, e.message);
1886
- }
1887
- safeModeRestrictions.delete(key);
1888
- }
1889
- writeRestrictDiag(root);
2200
+ function docText() {
2201
+ const dir = serenityDir();
2202
+ const credPath = storeFilePath("credential");
2203
+ const cfgPath = storeFilePath("config");
2204
+ return [
2205
+ "# localstore ACC 本地凭据/配置存储(标准)",
2206
+ "",
2207
+ "一个工具管理两个命名空间:credential(凭据,0600)+ config(偏好,0644)。",
2208
+ "存储于用户主目录,不在任何 CCC git 仓库内。",
2209
+ "",
2210
+ "## 存储位置(平台感知)",
2211
+ `- 根目录: ${dir} (0700)`,
2212
+ `- 凭据: ${credPath} (0600)`,
2213
+ `- 配置: ${cfgPath} (0644)`,
2214
+ "- Windows: %USERPROFILE%\\.serenity\\...(与 Linux/macOS 的 $HOME/.serenity 同构)",
2215
+ "",
2216
+ "## 格式",
2217
+ "credentials.yaml(扁平映射,key = 大写蛇形命名空间前缀):",
2218
+ "```yaml",
2219
+ "HOME_GITLAB_TOKEN: xxx",
2220
+ "SSH_UBUNTU_PASSWORD: xxx",
2221
+ "ANYSEARCH_API_KEY: xxx",
2222
+ "```",
2223
+ "settings.yaml(命名空间分节):",
2224
+ "```yaml",
2225
+ "loop:",
2226
+ " defaultModel: minimax-cn-coding-plan/MiniMax-M3",
2227
+ "ui:",
2228
+ " theme: dark",
2229
+ "```",
2230
+ "",
2231
+ "## key 规范",
2232
+ "- credential key: ^[A-Z][A-Z0-9_]*$(如 HOME_GITLAB_TOKEN)",
2233
+ "- config path: section.key(节 ^[a-z][a-z0-9-]*$,key 小驼峰 ^[a-z][a-zA-Z0-9_]*$,如 loop.defaultModel)",
2234
+ "",
2235
+ "## 读取方法(agent 可直接用 fs 工具)",
2236
+ "- 用 read 工具读对应文件 → 按上面格式解析 YAML",
2237
+ "- 或用 localstore get <name> [--scope credential|config]",
2238
+ "",
2239
+ "## 写入方法",
2240
+ "- 推荐:localstore set <name> <value> [--scope ...](自动建目录/设权限/保留其他条目)",
2241
+ "- 或直接用 write/edit 工具修改文件,保持 YAML 合法(键值冒号分隔)",
2242
+ "",
2243
+ "## 安全边界",
2244
+ "- list/show 对 credential 只返回 key 名,不返回值",
2245
+ "- 凭据值应在 agent 内部使用,不要写入对话/日志",
2246
+ "- 文件权限不符(非 0600/0644)时 get/set 会提示 chmod 修复",
2247
+ "- 文件在用户主目录,天然不进入任何 git 仓库",
2248
+ ""
2249
+ ].join("\n");
1890
2250
  }
1891
- /** exec 参数中提取常见路径字段(write/edit 工具);宽松读取 */
1892
- function extractPathArg(exec) {
1893
- const args = exec.arguments;
1894
- if (args === null || typeof args !== "object") return void 0;
1895
- const a = args;
1896
- for (const key of [
1897
- "path",
1898
- "file_path",
1899
- "target",
1900
- "dst"
1901
- ]) {
1902
- const v = a[key];
1903
- if (typeof v === "string") return v;
2251
+ /** 运行 localstore 操作(纯逻辑;返回 JSON 值供工具 render) */
2252
+ function runLocalStore(args) {
2253
+ const scope = args.scope === "config" ? "config" : "credential";
2254
+ switch (args.action) {
2255
+ case "list": return {
2256
+ scope,
2257
+ keys: listKeys(scope)
2258
+ };
2259
+ case "get": {
2260
+ if (!args.name) throw new Error("get 需要 name");
2261
+ const value = getEntry(scope, args.name);
2262
+ if (value === null) throw new Error(`not found: ${args.name}(scope=${scope})`);
2263
+ return {
2264
+ scope,
2265
+ name: args.name,
2266
+ value,
2267
+ source: scope
2268
+ };
2269
+ }
2270
+ case "set":
2271
+ if (!args.name) throw new Error("set 需要 name");
2272
+ if (args.value === void 0) throw new Error("set 需要 value");
2273
+ writeEntry(scope, args.name, args.value);
2274
+ return {
2275
+ scope,
2276
+ name: args.name,
2277
+ set: true
2278
+ };
2279
+ case "unset": {
2280
+ if (!args.name) throw new Error("unset 需要 name");
2281
+ const removed = unsetEntry(scope, args.name);
2282
+ return {
2283
+ scope,
2284
+ name: args.name,
2285
+ removed
2286
+ };
2287
+ }
2288
+ case "show": {
2289
+ if (!args.name) throw new Error("show 需要 name");
2290
+ const exists = getEntry(scope, args.name) !== null;
2291
+ return {
2292
+ scope,
2293
+ name: args.name,
2294
+ exists,
2295
+ path: storeFilePath(scope)
2296
+ };
2297
+ }
2298
+ case "doc": return { doc: docText() };
2299
+ default: throw new Error(`未知子命令: ${args.action}(可用 list/get/set/unset/show/doc)`);
1904
2300
  }
1905
2301
  }
2302
+ //#endregion
2303
+ //#region src/tools/localstore.ts
1906
2304
  /**
1907
- * 注册守卫:tools/pre-execute 瀑布 + ctx.tools.guard 终局。
1908
- * 两者都从 CCC 根实时读取配置(无状态、无缓存)。
2305
+ * localstore.ts localstore 真实 DSH 工具定义(defineTool)
2306
+ *
2307
+ * ACC 标准本地凭据/配置存储(S133 设计)。进程内注册,零 DSH 依赖逻辑在
2308
+ * localstore-ops.ts(可单测)。doc 子命令输出存储规范,agent 可直接用 fs
2309
+ * 工具(read/write)自己读写。
1909
2310
  */
1910
- function registerGuards(ctx, opts = {}) {
1911
- const configPaths = opts.configPaths;
1912
- const evaluate = (exec) => {
1913
- const root = findSerenityRoot(resolveAgentCwd(exec));
1914
- if (!root) return { kind: "allow" };
1915
- const safeModeOn = isSafeModeOn(root);
1916
- const blacklist = readBlacklist(root, configPaths);
1917
- const pathArg = extractPathArg(exec);
1918
- return decideGuard({
1919
- root,
1920
- toolName: exec.name,
1921
- safeModeOn,
1922
- blacklist,
1923
- pathArg
1924
- });
1925
- };
1926
- ctx.on("tools/pre-execute", async (exec, next) => {
1927
- const d = evaluate(exec);
1928
- if (d.deny) return {
1929
- kind: "deny",
1930
- reason: d.deny
1931
- };
1932
- return next();
1933
- });
1934
- ctx.tools.guard((exec) => {
1935
- return evaluate(exec).deny;
1936
- });
2311
+ const ACTIONS = [
2312
+ "list",
2313
+ "get",
2314
+ "set",
2315
+ "unset",
2316
+ "show",
2317
+ "doc"
2318
+ ];
2319
+ function renderText(value) {
2320
+ return [{
2321
+ type: "text",
2322
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
2323
+ }];
1937
2324
  }
2325
+ const localstoreTool = defineTool({
2326
+ name: "localstore",
2327
+ description: "ACC 标准本地凭据/配置存储:一个工具管理两个命名空间 credential(凭据,0600)与 config(本地偏好,0644)。存储于用户主目录 ~/.serenity/(平台感知,不在任何 git 仓库内)。子命令:list(列 key,凭据不返回值)/ get <name>(读值)/ set <name> <value>(写)/ unset <name>(删)/ show <name>(元数据,凭据不打印值)/ doc(输出存储规范——路径/格式/key 规范/权限,agent 可按说明直接用 read/write 操作文件)。默认 scope=credential;config 需传 --scope config(路径为 section.key,如 loop.defaultModel)。",
2328
+ parameters: {
2329
+ action: {
2330
+ type: "string",
2331
+ enum: [...ACTIONS],
2332
+ required: true,
2333
+ description: "子命令:list/get/set/unset/show/doc"
2334
+ },
2335
+ name: {
2336
+ type: "string",
2337
+ description: "条目名(credential 用大写蛇形;config 用 section.key)"
2338
+ },
2339
+ value: {
2340
+ type: "string",
2341
+ description: "set 的值"
2342
+ },
2343
+ scope: {
2344
+ type: "string",
2345
+ enum: [...LOCALSTORE_SCOPES],
2346
+ description: "命名空间 credential|config(默认 credential)"
2347
+ }
2348
+ },
2349
+ output: {
2350
+ schema: { type: "json" },
2351
+ render: (args, value) => renderText(value)
2352
+ },
2353
+ async execute(args) {
2354
+ return runLocalStore(args);
2355
+ }
2356
+ });
1938
2357
  //#endregion
1939
2358
  //#region src/seams/loop.ts
1940
2359
  /** 解析当前 dsh 会话 scope 的活跃会话 SESSION.md 绝对路径;无标记/越界返回 null */
@@ -2045,21 +2464,6 @@ function registerKeeper(ctx, opts = {}) {
2045
2464
  });
2046
2465
  }
2047
2466
  //#endregion
2048
- //#region src/constants.ts
2049
- /** 常量(纯模块,零 DSH 依赖) */
2050
- /**
2051
- * ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
2052
- * 发布时只需改 package.json 的 version。
2053
- */
2054
- const ACC_VERSION = (() => {
2055
- try {
2056
- const here = dirname(fileURLToPath(import.meta.url));
2057
- return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8")).version ?? "0.0.0";
2058
- } catch {
2059
- return "0.0.0";
2060
- }
2061
- })();
2062
- //#endregion
2063
2467
  //#region src/skills-discovery.ts
2064
2468
  /**
2065
2469
  * skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
@@ -2161,6 +2565,7 @@ function accBlock(root) {
2161
2565
  " neat — return the full Neat design collaboration protocol",
2162
2566
  " cce — return the full Cognitive Continuity Engineering framework",
2163
2567
  " loop — 牛马循环:指定模型专用 agent 反复执行",
2568
+ " localstore — ACC 本地凭据/配置存储(credential 0600 + config 0644,~/.serenity/);doc 子命令输出规范",
2164
2569
  "",
2165
2570
  "The DSH platform tools remain available too (read/write/edit/glob/grep/web_search/ask_user_question/subagent/workflow/goal and more) — the ACC tools above are the serenity-native layer, not the only tools.",
2166
2571
  "",
@@ -2233,6 +2638,22 @@ function constraintsBlock(root) {
2233
2638
  ""
2234
2639
  ].join("\n");
2235
2640
  }
2641
+ /**
2642
+ * EAP 自检提示块(DSH 扩展,无 osp 对应——osp 无此块)。
2643
+ * 每次输出前的机械自检清单,强化 EAP 表现(E↑ 显式/R↓ 可重建/S↑ 稳定)。
2644
+ * 独立块而非塞进 CCE/Constraints:后两者受 osp-alignment 逐字节断言约束。
2645
+ */
2646
+ function eapBlock() {
2647
+ return [
2648
+ "",
2649
+ "=== Serenity EAP ===",
2650
+ "每次输出前自检(显式抽象原则:思维的价值 = 外部可重建性):",
2651
+ " • E↑ 显式 — 变量/实体明确定义,关系指明方向/基数,边界划定;不用歧义词(\"处理\"\"优化\"→具体化)",
2652
+ " • R↓ 可重建 — 关键决策记录理由与备选,不跳级讨论(先对齐上层再进下层)",
2653
+ " • S↑ 稳定 — 结构可重复生成,避免依赖隐含上下文",
2654
+ ""
2655
+ ].join("\n");
2656
+ }
2236
2657
  /** 4) SKILL.md 全文:该 CCC 顶层入口 skill 原文(对齐 osp:原文直推,无包裹头;仅过滤治理内容) */
2237
2658
  function entrySkillSectionText(root) {
2238
2659
  const skills = findEntrySkills(root);
@@ -2283,12 +2704,13 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
2283
2704
  ""
2284
2705
  ].join("\n");
2285
2706
  }
2286
- /** 完整系统提示词注入文本:ACC + CCE + Constraints + SKILL 全文 + Session(osp 顺序) */
2707
+ /** 完整系统提示词注入文本:ACC + CCE + Constraints + EAP + SKILL 全文 + Session(osp 顺序 + EAP 扩展) */
2287
2708
  function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
2288
2709
  const parts = [
2289
2710
  accBlock(root),
2290
2711
  cceBlock(),
2291
- constraintsBlock(root)
2712
+ constraintsBlock(root),
2713
+ eapBlock()
2292
2714
  ];
2293
2715
  const skill = entrySkillSectionText(root);
2294
2716
  if (skill) parts.push(skill);
@@ -2296,6 +2718,31 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
2296
2718
  if (session) parts.push(session);
2297
2719
  return parts.join("\n\n");
2298
2720
  }
2721
+ /**
2722
+ * Code Mode 适配行:当前 scope 以 Code Mode 呈现工具时(run_code 可见),
2723
+ * ACC 块按 native 语义指引"直接调用工具"会与"只有 run_code 可直接调用"的执行
2724
+ * 塌缩冲突(模型直呼工具名 → UNKNOWN_TOOL,拒绝信息误导)。追加一行说明,
2725
+ * 引导经 run_code 程序内 tools.* 调用。both 模式不塌缩,该行不误导(程序内
2726
+ * 调用同样合法),故按 run_code 可见性(code|both)统一附加。
2727
+ * @param ctx - 插件 ctx(读 tools 注册表)。
2728
+ * @param scope - 装配 scope(agent);无则按全局视图判断。
2729
+ * @returns 适配行(含换行前缀);非 code/both 返回空串。
2730
+ */
2731
+ function codeModeAdaptationLine(ctx, scope) {
2732
+ try {
2733
+ if (!(ctx.tools.get("run_code", scope) !== void 0)) return "";
2734
+ } catch {
2735
+ return "";
2736
+ }
2737
+ return [
2738
+ "",
2739
+ "=== Serenity Code Mode ===",
2740
+ "当前会话以 Code Mode 呈现工具:模型直接调用 ACC 工具名(cc_fs/acc_msm 等)会被拒绝(UNKNOWN_TOOL)。",
2741
+ "请在一个 run_code 程序内经生成的 SDK 绑定调用:`await tools.cc_fs(...)`、`await tools.acc_msm(...)` 等。",
2742
+ "程序只返回你 print/return 的内容——务必 curate 输出。",
2743
+ ""
2744
+ ].join("\n");
2745
+ }
2299
2746
  /** 从 assembly context 解析 agent cwd(subagent/后台 agent 同样带 agent) */
2300
2747
  function agentCwd(context) {
2301
2748
  return (context.agent?.session)?.header?.cwd;
@@ -2319,7 +2766,9 @@ function registerEntrySkillSectionGlobal(ctx) {
2319
2766
  if (!cwd) return "";
2320
2767
  const root = findSerenityRoot(cwd);
2321
2768
  if (!root) return "";
2322
- return serenitySystemPrompt(root, agentScope$1(context));
2769
+ const base = serenitySystemPrompt(root, agentScope$1(context));
2770
+ const codeLine = codeModeAdaptationLine(ctx, context.scope);
2771
+ return codeLine ? `${base}\n${codeLine}` : base;
2323
2772
  }
2324
2773
  });
2325
2774
  console.log("[serenity-hooks] ✓ 全局入口 skill section 已注册(systemPrompt 就绪)");
@@ -2327,6 +2776,39 @@ function registerEntrySkillSectionGlobal(ctx) {
2327
2776
  console.error(`[serenity-hooks] ✗ 全局入口 skill section 注册失败: ${err.message}(检查 inject 是否含 systemPrompt)`);
2328
2777
  }
2329
2778
  }
2779
+ /**
2780
+ * agent 级 scoped 注册(P0-1:抗 preset/动态插件同名 shadow)。
2781
+ *
2782
+ * 为什么 scoped:DSH 的 systemPrompt section 支持 scoped 层同名 shadow 全局
2783
+ * (agent.ctx.systemPrompt.section),且 scope 链最近层胜出。若 ACC 只注册全局
2784
+ * section,preset 或动态 Cordis 插件可在 scoped 层注册同名 `serenity-entry`
2785
+ * 遮蔽 ACC 身份。scoped 注册在 agent 自身层 = 最近层,任何外部组合无法覆盖。
2786
+ * 全局注册保留为 fallback(未走 session-start 的 agent / 冷恢复路径)。
2787
+ *
2788
+ * text 回调闭包持有 root 与 agent:content 固定(该 agent 的 CCC 身份),
2789
+ * code-mode 适配按装配 context.scope 判断(与全局版一致)。
2790
+ */
2791
+ const sectionedAgents = /* @__PURE__ */ new Set();
2792
+ function registerEntrySkillSection(agent, root) {
2793
+ const key = agent.session.id ?? "global";
2794
+ if (sectionedAgents.has(key)) return false;
2795
+ try {
2796
+ const scope = agent.session.id ?? "default";
2797
+ agent.ctx.systemPrompt.section({
2798
+ name: "serenity-entry",
2799
+ order: -50,
2800
+ text: (context) => {
2801
+ const base = serenitySystemPrompt(root, scope);
2802
+ const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
2803
+ return codeLine ? `${base}\n${codeLine}` : base;
2804
+ }
2805
+ });
2806
+ sectionedAgents.add(key);
2807
+ return true;
2808
+ } catch {
2809
+ return false;
2810
+ }
2811
+ }
2330
2812
  //#endregion
2331
2813
  //#region src/seams/context.ts
2332
2814
  const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
@@ -2383,6 +2865,7 @@ function registerContext(ctx, opts = {}) {
2383
2865
  const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
2384
2866
  if (!root) return;
2385
2867
  const key = agentKey(agent);
2868
+ registerEntrySkillSection(agent, root);
2386
2869
  if (injected.has(key)) return;
2387
2870
  injected.add(key);
2388
2871
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars, agentScope(agent)));
@@ -2398,9 +2881,12 @@ function registerContext(ctx, opts = {}) {
2398
2881
  const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
2399
2882
  const key = agentKey(agent);
2400
2883
  const downstream = await next();
2401
- if (root) try {
2402
- syncSafeModeRestriction(agent, root);
2403
- } catch {}
2884
+ if (root) {
2885
+ try {
2886
+ syncSafeModeRestriction(agent, root);
2887
+ } catch {}
2888
+ registerEntrySkillSection(agent, root);
2889
+ }
2404
2890
  if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
2405
2891
  injected.add(key);
2406
2892
  return {
@@ -2436,45 +2922,6 @@ function registerCompactRetention(ctx, opts = {}) {
2436
2922
  });
2437
2923
  }
2438
2924
  //#endregion
2439
- //#region src/status.ts
2440
- /**
2441
- * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
2442
- *
2443
- * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / loop 模型。
2444
- * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
2445
- */
2446
- function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
2447
- const root = findSerenityRoot(cwd);
2448
- const restrict = getRestrictDiagnostics();
2449
- if (!root) return {
2450
- root: null,
2451
- accVersion: ACC_VERSION,
2452
- safeModeOn: false,
2453
- blacklist: [],
2454
- threshold: null,
2455
- loopModel: null,
2456
- restrict
2457
- };
2458
- const cfg = loadSerenityConfig(root, configPaths);
2459
- return {
2460
- root,
2461
- accVersion: ACC_VERSION,
2462
- safeModeOn: isSafeModeOn(root),
2463
- blacklist: readBlacklist(root, configPaths),
2464
- threshold: cfg.sessionKeeper?.threshold ?? null,
2465
- loopModel: cfg.loop?.defaultModel ?? null,
2466
- restrict
2467
- };
2468
- }
2469
- /** 切换安全模式(写/删标记文件);返回实际生效状态 */
2470
- function setSafeMode(root, on) {
2471
- const marker = resolve(root, SAFE_MODE_MARKER);
2472
- if (on) {
2473
- if (!existsSync(marker)) writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2474
- } else rmSync(marker, { force: true });
2475
- return { on: isSafeModeOn(root) };
2476
- }
2477
- //#endregion
2478
2925
  //#region src/api.ts
2479
2926
  const ROUTE_PATH = "/serenity/status";
2480
2927
  function readBody(req) {
@@ -2517,10 +2964,18 @@ function registerStatusApi(ctx, opts = {}) {
2517
2964
  try {
2518
2965
  if (req.method === "GET") {
2519
2966
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
2520
- sendJson(res, 200, getStatus(resolveWorkspace(ctx, {
2967
+ const status = getStatus(resolveWorkspace(ctx, {
2521
2968
  sessionId: url.searchParams.get("sessionId") ?? void 0,
2522
2969
  workspace: url.searchParams.get("workspace") ?? void 0
2523
- }), configPaths));
2970
+ }), configPaths);
2971
+ const runtime = ctx.get("codeRuntime");
2972
+ sendJson(res, 200, {
2973
+ ...status,
2974
+ ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
2975
+ language: runtime.language,
2976
+ isolation: runtime.isolation
2977
+ } }
2978
+ });
2524
2979
  return;
2525
2980
  }
2526
2981
  if (req.method === "POST") {
@@ -2700,6 +3155,7 @@ const inject = [
2700
3155
  "shellEnv",
2701
3156
  "skills",
2702
3157
  "agentLoop",
3158
+ "agents",
2703
3159
  "systemPrompt"
2704
3160
  ];
2705
3161
  const Config = z.object({
@@ -2727,6 +3183,7 @@ function apply(ctx, config) {
2727
3183
  ctx.tools.register(neatTool);
2728
3184
  ctx.tools.register(cceTool);
2729
3185
  ctx.tools.register(createLoopTool(ctx));
3186
+ ctx.tools.register(localstoreTool);
2730
3187
  }
2731
3188
  if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
2732
3189
  if (config.turnFlush) registerTurnFlush(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.16.3",
3
+ "version": "1.16.5",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {