@shgroup/dsh-serenity-hooks 1.16.4 → 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.4",
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
6
  import { homedir, platform } from "node:os";
7
7
  import { promisify } from "node:util";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
10
- import { randomBytes } from "node:crypto";
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));
@@ -1267,7 +1267,7 @@ function runKit(root, args) {
1267
1267
  function agentCwd$4(exec) {
1268
1268
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1269
1269
  }
1270
- function renderText$6(value) {
1270
+ function renderText$7(value) {
1271
1271
  return [{
1272
1272
  type: "text",
1273
1273
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1290,7 +1290,7 @@ const kitTool = defineTool({
1290
1290
  },
1291
1291
  output: {
1292
1292
  schema: { type: "json" },
1293
- render: (args, value) => renderText$6(value)
1293
+ render: (args, value) => renderText$7(value)
1294
1294
  },
1295
1295
  async execute(args, exec) {
1296
1296
  const root = findSerenityRoot(agentCwd$4(exec));
@@ -1403,7 +1403,7 @@ function runGit(root, args) {
1403
1403
  function agentCwd$3(exec) {
1404
1404
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1405
1405
  }
1406
- function renderText$5(value) {
1406
+ function renderText$6(value) {
1407
1407
  return [{
1408
1408
  type: "text",
1409
1409
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1430,7 +1430,7 @@ const gitTool = defineTool({
1430
1430
  },
1431
1431
  output: {
1432
1432
  schema: { type: "json" },
1433
- render: (args, value) => renderText$5(value)
1433
+ render: (args, value) => renderText$6(value)
1434
1434
  },
1435
1435
  async execute(args, exec) {
1436
1436
  const root = findSerenityRoot(agentCwd$3(exec));
@@ -1446,7 +1446,7 @@ const gitTool = defineTool({
1446
1446
  function agentCwd$2(exec) {
1447
1447
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1448
1448
  }
1449
- function renderText$4(value) {
1449
+ function renderText$5(value) {
1450
1450
  return [{
1451
1451
  type: "text",
1452
1452
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1490,7 +1490,7 @@ const msmTool = defineTool({
1490
1490
  },
1491
1491
  output: {
1492
1492
  schema: { type: "json" },
1493
- render: (args, value) => renderText$4(value)
1493
+ render: (args, value) => renderText$5(value)
1494
1494
  },
1495
1495
  async execute(args, exec) {
1496
1496
  const root = findSerenityRoot(agentCwd$2(exec));
@@ -1531,7 +1531,7 @@ CCC(home-serenity 等)认知内容编码为 skill/SESSION/设计文档。产
1531
1531
 
1532
1532
  ## 参考
1533
1533
  https://github.com/tellmewhattodo/theory-eap`;
1534
- function renderText$3(value) {
1534
+ function renderText$4(value) {
1535
1535
  return [{
1536
1536
  type: "text",
1537
1537
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1551,7 +1551,7 @@ const eapTool = defineTool({
1551
1551
  } },
1552
1552
  output: {
1553
1553
  schema: { type: "string" },
1554
- render: (_args, value) => renderText$3(value)
1554
+ render: (_args, value) => renderText$4(value)
1555
1555
  },
1556
1556
  async execute(args) {
1557
1557
  if (args.section === "variables") return EAP_CONTENT.split("## 输出前自检清单")[0];
@@ -1592,7 +1592,7 @@ const NEAT_CONTENT = `# Neat 设计协作协议
1592
1592
  2. 等待确认或修正(小步)
1593
1593
  3. 确认后记录决策(写入会话 SESSION.md 或设计文档)
1594
1594
  4. 推进到下一决策点`;
1595
- function renderText$2(value) {
1595
+ function renderText$3(value) {
1596
1596
  return [{
1597
1597
  type: "text",
1598
1598
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1608,7 +1608,7 @@ const neatTool = defineTool({
1608
1608
  } },
1609
1609
  output: {
1610
1610
  schema: { type: "string" },
1611
- render: (_args, value) => renderText$2(value)
1611
+ render: (_args, value) => renderText$3(value)
1612
1612
  },
1613
1613
  async execute(args) {
1614
1614
  if (args.section === "rules") return NEAT_CONTENT.match(/## 四条铁律[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
@@ -1671,7 +1671,7 @@ CCE 回答"有结构的知识应如何跨时间持续演化而不丧失连贯性
1671
1671
  ## 与 Serenity 的关系
1672
1672
  Serenity 的会话系统、会话追踪、熵管理机制(SQC 品质循环)都是 CCE 的工程实现;
1673
1673
  CCC 系统提示词中嵌入的行为约束即来自 CCE。`;
1674
- function renderText$1(value) {
1674
+ function renderText$2(value) {
1675
1675
  return [{
1676
1676
  type: "text",
1677
1677
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1692,7 +1692,7 @@ const cceTool = defineTool({
1692
1692
  } },
1693
1693
  output: {
1694
1694
  schema: { type: "string" },
1695
- render: (_args, value) => renderText$1(value)
1695
+ render: (_args, value) => renderText$2(value)
1696
1696
  },
1697
1697
  async execute(args) {
1698
1698
  const section = args.section;
@@ -1837,7 +1837,7 @@ ${resumeNote}
1837
1837
  function agentCwd$1(exec) {
1838
1838
  return (exec.agent?.session)?.header?.cwd ?? process.cwd();
1839
1839
  }
1840
- function renderText(value) {
1840
+ function renderText$1(value) {
1841
1841
  return [{
1842
1842
  type: "text",
1843
1843
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
@@ -1876,7 +1876,7 @@ function lastAssistantText(agent) {
1876
1876
  function createLoopTool(ctx) {
1877
1877
  return defineTool({
1878
1878
  name: "loop",
1879
- 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",
1880
1880
  parameters: {
1881
1881
  task: {
1882
1882
  type: "string",
@@ -1897,12 +1897,12 @@ function createLoopTool(ctx) {
1897
1897
  },
1898
1898
  maxRounds: {
1899
1899
  type: "integer",
1900
- description: "轮次上限(默认 100"
1900
+ description: "保险阀(防死循环,默认 100;调用者通常无需设置——loop 跑到完成或达此上限)"
1901
1901
  }
1902
1902
  },
1903
1903
  output: {
1904
1904
  schema: { type: "json" },
1905
- render: (_args, value) => renderText(value)
1905
+ render: (_args, value) => renderText$1(value)
1906
1906
  },
1907
1907
  async execute(args, exec) {
1908
1908
  const root = findSerenityRoot(agentCwd$1(exec));
@@ -1916,11 +1916,11 @@ function createLoopTool(ctx) {
1916
1916
  const { provider, model: modelName } = splitModel(model);
1917
1917
  const stopToken = newStopToken();
1918
1918
  let progress = readProgress(root, label);
1919
- const startRound = progress ? Math.min(progress.round + 1, maxRounds) : 1;
1919
+ const startRound = progress ? progress.round + 1 : 1;
1920
1920
  const parentCtx = exec.agent?.ctx;
1921
1921
  const inherited = loopPresetInheritance(parentCtx);
1922
1922
  if (!ctx.agents) throw new Error("loop: ctx.agents 不可用");
1923
- const sessionId = `loop-${label}`;
1923
+ const sessionId = `loop-${label}-${randomUUID()}`;
1924
1924
  const handle = await ctx.agents.create({
1925
1925
  sessionId,
1926
1926
  meta: {
@@ -1996,16 +1996,365 @@ function createLoopTool(ctx) {
1996
1996
  progressFile: json,
1997
1997
  lastResponse: lastResponse.slice(0, 2e3),
1998
1998
  usage: {
1999
- how: "loop 用指定模型(默认 M3)创建专用 agent 每轮独立执行任务,完成时输出停止标记即终止",
1999
+ how: "loop 内部硬性 while 驱动 agent 逐轮推进,agent 精确回显随机完成码即终止",
2000
2000
  progress: `进度在 AGENT_SESSIONS/loop-${label}.md 与 .json;同 label 再调 loop 会从下一轮续跑(不重做)`,
2001
2001
  constraints: "loop agent 受完整 Serenity 约束(ACC 身份/入口技能系统提示词/守卫)",
2002
- next: done ? "任务已完成;可查看进度文件收尾" : `任务未完成(${finalRound}/${maxRounds} 轮);可同 label 续跑或调整 maxRounds`
2002
+ next: done ? "任务已完成;可查看进度文件收尾" : `任务未完成(已达保险阀 ${maxRounds} 轮);可同 label 续跑`
2003
2003
  }
2004
2004
  };
2005
2005
  }
2006
2006
  });
2007
2007
  }
2008
2008
  //#endregion
2009
+ //#region src/localstore-ops.ts
2010
+ /**
2011
+ * localstore-ops.ts — localstore 纯逻辑层(零 DSH 依赖,可独立单测)
2012
+ *
2013
+ * ACC 标准本地凭据/配置存储(S133 设计):
2014
+ * - 一个工具管理两个命名空间:credential(凭据,0600)+ config(偏好,0644)
2015
+ * - 存储于 ~/.serenity/(平台感知:win %USERPROFILE%\.serenity\)
2016
+ * - 两个 YAML 文件:credentials.yaml(扁平 REF→value)+ settings.yaml(命名空间分节)
2017
+ * - 目录 0700
2018
+ *
2019
+ * YAML 用轻量自实现子集(零依赖):扁平 `KEY: value` 映射 + `#` 注释。
2020
+ * 凭据/配置本质是给 MSM/agent 用的普通 YAML 文件——agent 可按 doc 子命令
2021
+ * 说明直接用 fs 工具(read/write)操作;本工具是管理入口 + 规范文档。
2022
+ */
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;
2069
+ }
2070
+ return out;
2071
+ }
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;
2103
+ }
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)`);
2135
+ return {
2136
+ section,
2137
+ key
2138
+ };
2139
+ }
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}`));
2195
+ }
2196
+ /**
2197
+ * doc 说明文本:输出存储位置/格式/key 规范/权限/读写方法/安全边界。
2198
+ * agent 据此可直接用 fs 工具(read/write)自己读写凭据/配置。
2199
+ */
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");
2250
+ }
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)`);
2300
+ }
2301
+ }
2302
+ //#endregion
2303
+ //#region src/tools/localstore.ts
2304
+ /**
2305
+ * localstore.ts — localstore 真实 DSH 工具定义(defineTool)
2306
+ *
2307
+ * ACC 标准本地凭据/配置存储(S133 设计)。进程内注册,零 DSH 依赖逻辑在
2308
+ * localstore-ops.ts(可单测)。doc 子命令输出存储规范,agent 可直接用 fs
2309
+ * 工具(read/write)自己读写。
2310
+ */
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
+ }];
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
+ });
2357
+ //#endregion
2009
2358
  //#region src/seams/loop.ts
2010
2359
  /** 解析当前 dsh 会话 scope 的活跃会话 SESSION.md 绝对路径;无标记/越界返回 null */
2011
2360
  function resolveActiveSession(root, scope = DEFAULT_SESSION_SCOPE) {
@@ -2216,6 +2565,7 @@ function accBlock(root) {
2216
2565
  " neat — return the full Neat design collaboration protocol",
2217
2566
  " cce — return the full Cognitive Continuity Engineering framework",
2218
2567
  " loop — 牛马循环:指定模型专用 agent 反复执行",
2568
+ " localstore — ACC 本地凭据/配置存储(credential 0600 + config 0644,~/.serenity/);doc 子命令输出规范",
2219
2569
  "",
2220
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.",
2221
2571
  "",
@@ -2833,6 +3183,7 @@ function apply(ctx, config) {
2833
3183
  ctx.tools.register(neatTool);
2834
3184
  ctx.tools.register(cceTool);
2835
3185
  ctx.tools.register(createLoopTool(ctx));
3186
+ ctx.tools.register(localstoreTool);
2836
3187
  }
2837
3188
  if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
2838
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.4",
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": {