@shgroup/dsh-serenity-hooks 1.16.2 → 1.16.4

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.2",
3
+ "version": "1.16.4",
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": {
package/lib/index.js CHANGED
@@ -3,11 +3,11 @@ import { defineTool } from "@deepseek-ai/dsh-tools";
3
3
  import { appendFileSync, 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 { fileURLToPath } from "node:url";
8
9
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
9
10
  import { randomBytes } from "node:crypto";
10
- import { fileURLToPath } from "node:url";
11
11
  //#region src/ccc.ts
12
12
  /**
13
13
  * ccc.ts — CCC 纯逻辑层(零 DSH 依赖,可独立单测)
@@ -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();
@@ -1498,6 +1719,24 @@ const cceTool = defineTool({
1498
1719
  }
1499
1720
  });
1500
1721
  //#endregion
1722
+ //#region src/loop-preset-inherit.ts
1723
+ /**
1724
+ * 解析 loop agent 从父 agent 继承的 preset,并组装创建 setup 钩子。
1725
+ * @param parentCtx - 发起 loop 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
1726
+ * @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
1727
+ */
1728
+ function loopPresetInheritance(parentCtx) {
1729
+ if (parentCtx === void 0) return {};
1730
+ const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
1731
+ if (agentPreset === void 0) return {};
1732
+ return {
1733
+ agentPreset,
1734
+ setup: (childCtx) => {
1735
+ childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
1736
+ }
1737
+ };
1738
+ }
1739
+ //#endregion
1501
1740
  //#region src/loop-ops.ts
1502
1741
  /**
1503
1742
  * loop-ops.ts — acc_loop 纯逻辑层(零 DSH 依赖,可独立单测)
@@ -1587,9 +1826,13 @@ ${resumeNote}
1587
1826
  * model(选)provider/model(如 minimax-cn-coding-plan/MiniMax-M3);缺省读 loop.defaultModel
1588
1827
  * maxRounds(默认 100)轮次上限;每轮等待 agent **无超时**(loop 可永续,agent 工作多久等多久)
1589
1828
  *
1590
- * 机制:ctx.agentLoop.create({provider, model}) 创建专用 agent(进程内),
1829
+ * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
1591
1830
  * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 → 续跑。
1592
1831
  * 工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
1832
+ *
1833
+ * preset 继承:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐 subagent 先例),
1834
+ * 使 loop agent 继承发起方会话的 agent preset 工具(read/write/edit 等 preset 层工具)。
1835
+ * agentPresets 是可选服务——无 preset 装配的环境(无 roster 部署)退化为空工具层(历史行为)。
1593
1836
  */
1594
1837
  function agentCwd$1(exec) {
1595
1838
  return (exec.agent?.session)?.header?.cwd ?? process.cwd();
@@ -1629,6 +1872,7 @@ function lastAssistantText(agent) {
1629
1872
  return "";
1630
1873
  }
1631
1874
  /** 创建 loop 工具(闭包捕获插件 ctx → 可访问 ctx.agentLoop) */
1875
+ /** 创建 loop 工具(闭包捕获插件 ctx → 可访问 ctx.agentLoop) */
1632
1876
  function createLoopTool(ctx) {
1633
1877
  return defineTool({
1634
1878
  name: "loop",
@@ -1673,10 +1917,23 @@ function createLoopTool(ctx) {
1673
1917
  const stopToken = newStopToken();
1674
1918
  let progress = readProgress(root, label);
1675
1919
  const startRound = progress ? Math.min(progress.round + 1, maxRounds) : 1;
1676
- const loopAgent = ctx.agentLoop.create(`loop-${label}`, {
1677
- provider,
1678
- model: modelName
1679
- }, { cwd: root });
1920
+ const parentCtx = exec.agent?.ctx;
1921
+ const inherited = loopPresetInheritance(parentCtx);
1922
+ if (!ctx.agents) throw new Error("loop: ctx.agents 不可用");
1923
+ const sessionId = `loop-${label}`;
1924
+ const handle = await ctx.agents.create({
1925
+ sessionId,
1926
+ meta: {
1927
+ cwd: root,
1928
+ ...inherited.agentPreset === void 0 ? {} : { agentPreset: inherited.agentPreset }
1929
+ },
1930
+ agentOptions: {
1931
+ provider,
1932
+ model: modelName
1933
+ },
1934
+ ...inherited.setup === void 0 ? {} : { setup: inherited.setup }
1935
+ });
1936
+ const loopAgent = handle.agent;
1680
1937
  let done = false;
1681
1938
  let lastResponse = progress?.lastResponse ?? "";
1682
1939
  let finalRound = startRound - 1;
@@ -1727,7 +1984,9 @@ function createLoopTool(ctx) {
1727
1984
  updated: (/* @__PURE__ */ new Date()).toISOString(),
1728
1985
  lastResponse
1729
1986
  });
1730
- } finally {}
1987
+ } finally {
1988
+ await handle.dispose().catch(() => {});
1989
+ }
1731
1990
  const { json } = loopProgressPaths(root, label);
1732
1991
  return {
1733
1992
  done,
@@ -1747,157 +2006,6 @@ function createLoopTool(ctx) {
1747
2006
  });
1748
2007
  }
1749
2008
  //#endregion
1750
- //#region src/seams/guards.ts
1751
- /**
1752
- * guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
1753
- *
1754
- * 纯决策逻辑(decideGuard)与 DSH 注册(registerGuards)分离:
1755
- * 前者零 DSH 依赖可单测,后者把决策接到 tools/pre-execute 瀑布 + ctx.tools.guard 终局。
1756
- *
1757
- * 对应 opencode-serenity-plugin 的 tool.execute.before 路径守卫 + bash 开关 + 黑名单。
1758
- */
1759
- /**
1760
- * 安全模式 + 黑名单 + P3 路径守卫的纯决策。
1761
- * 对齐 opencode-serenity-plugin 标准:安全模式 = **bash 禁用** + 写入黑名单;
1762
- * write/edit 等工具仅受路径逃逸与黑名单约束(不整体禁用)。
1763
- * 优先级:safe-mode bash > 路径越界 > 黑名单命中。
1764
- */
1765
- function decideGuard(input) {
1766
- const { root, toolName, safeModeOn, blacklist, pathArg } = input;
1767
- if (safeModeOn && toolName === "bash") return {
1768
- deny: `bash: 没有这个工具`,
1769
- kind: "deny"
1770
- };
1771
- if (pathArg !== void 0) {
1772
- const rel = relative(root, resolve(root, pathArg));
1773
- if (rel.startsWith("..")) return {
1774
- deny: `path escape blocked: "${pathArg}" 越出 CCC 根`,
1775
- kind: "deny"
1776
- };
1777
- if (rel === ".serenity-safe-on" || rel === ".serenity" || rel.startsWith(".serenity-safe-on/") || rel.startsWith(".serenity/")) return {
1778
- deny: `CCC 治理文件 "${rel}" 保留给用户,agent 不可写`,
1779
- kind: "deny"
1780
- };
1781
- const hit = matchBlacklist(rel, blacklist);
1782
- if (hit) return {
1783
- deny: `blacklist blocked: "${pathArg}" 命中规则 "${hit}"`,
1784
- kind: "deny"
1785
- };
1786
- }
1787
- return { kind: "allow" };
1788
- }
1789
- /** 从 exec 提取 agent 会话 cwd(CCC 根检测基准);无则回退进程 cwd */
1790
- function resolveAgentCwd(exec) {
1791
- return exec.agent?.session?.header?.cwd ?? process.cwd();
1792
- }
1793
- /** 安全模式开启时从模型工具列表隐藏的工具(只隐藏 bash;write/edit 保留) */
1794
- const SAFE_MODE_DENY_TOOLS = ["bash"];
1795
- /** agent key → restrict disposer(安全模式工具隐藏状态) */
1796
- const safeModeRestrictions = /* @__PURE__ */ new Map();
1797
- const restrictDiag = {
1798
- lastKey: null,
1799
- lastAttemptAt: null,
1800
- lastSuccess: null,
1801
- lastError: null,
1802
- activeKeys: []
1803
- };
1804
- function getRestrictDiagnostics() {
1805
- return {
1806
- ...restrictDiag,
1807
- activeKeys: [...safeModeRestrictions.keys()]
1808
- };
1809
- }
1810
- /** 诊断落盘:AGENT_SESSIONS/.restrict-diag.json(文件通道,避免 HTTP 自锁) */
1811
- function writeRestrictDiag(root) {
1812
- try {
1813
- const dir = resolve(root, "AGENT_SESSIONS");
1814
- mkdirSync(dir, { recursive: true });
1815
- writeFileSync(resolve(dir, ".restrict-diag.json"), JSON.stringify({
1816
- ...getRestrictDiagnostics(),
1817
- cccRoot: root
1818
- }, null, 2) + "\n", "utf-8");
1819
- } catch {}
1820
- }
1821
- /**
1822
- * 同步安全模式工具隐藏:标记存在 → agent.ctx.tools.restrict deny 隐藏写工具;
1823
- * 标记消失 → 解除。pre-step 每步调用 → 切换实时生效。
1824
- */
1825
- function syncSafeModeRestriction(agent, root) {
1826
- const key = agent.session.id ?? "global";
1827
- const on = isSafeModeOn(root);
1828
- const existing = safeModeRestrictions.get(key);
1829
- if (on && !existing) try {
1830
- const dispose = agent.ctx.tools.restrict({ deny: [...SAFE_MODE_DENY_TOOLS] });
1831
- safeModeRestrictions.set(key, dispose);
1832
- restrictDiag.lastKey = key;
1833
- restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
1834
- restrictDiag.lastSuccess = true;
1835
- restrictDiag.lastError = null;
1836
- } catch (e) {
1837
- restrictDiag.lastKey = key;
1838
- restrictDiag.lastAttemptAt = (/* @__PURE__ */ new Date()).toISOString();
1839
- restrictDiag.lastSuccess = false;
1840
- restrictDiag.lastError = e.message;
1841
- console.error(`[serenity-hooks] restrict 失败 (key=${key}):`, e.message);
1842
- }
1843
- else if (!on && existing) {
1844
- try {
1845
- existing();
1846
- } catch (e) {
1847
- console.error(`[serenity-hooks] restrict 解除失败 (key=${key}):`, e.message);
1848
- }
1849
- safeModeRestrictions.delete(key);
1850
- }
1851
- writeRestrictDiag(root);
1852
- }
1853
- /** 从 exec 参数中提取常见路径字段(write/edit 工具);宽松读取 */
1854
- function extractPathArg(exec) {
1855
- const args = exec.arguments;
1856
- if (args === null || typeof args !== "object") return void 0;
1857
- const a = args;
1858
- for (const key of [
1859
- "path",
1860
- "file_path",
1861
- "target",
1862
- "dst"
1863
- ]) {
1864
- const v = a[key];
1865
- if (typeof v === "string") return v;
1866
- }
1867
- }
1868
- /**
1869
- * 注册守卫:tools/pre-execute 瀑布 + ctx.tools.guard 终局。
1870
- * 两者都从 CCC 根实时读取配置(无状态、无缓存)。
1871
- */
1872
- function registerGuards(ctx, opts = {}) {
1873
- const configPaths = opts.configPaths;
1874
- const evaluate = (exec) => {
1875
- const root = findSerenityRoot(resolveAgentCwd(exec));
1876
- if (!root) return { kind: "allow" };
1877
- const safeModeOn = isSafeModeOn(root);
1878
- const blacklist = readBlacklist(root, configPaths);
1879
- const pathArg = extractPathArg(exec);
1880
- return decideGuard({
1881
- root,
1882
- toolName: exec.name,
1883
- safeModeOn,
1884
- blacklist,
1885
- pathArg
1886
- });
1887
- };
1888
- ctx.on("tools/pre-execute", async (exec, next) => {
1889
- const d = evaluate(exec);
1890
- if (d.deny) return {
1891
- kind: "deny",
1892
- reason: d.deny
1893
- };
1894
- return next();
1895
- });
1896
- ctx.tools.guard((exec) => {
1897
- return evaluate(exec).deny;
1898
- });
1899
- }
1900
- //#endregion
1901
2009
  //#region src/seams/loop.ts
1902
2010
  /** 解析当前 dsh 会话 scope 的活跃会话 SESSION.md 绝对路径;无标记/越界返回 null */
1903
2011
  function resolveActiveSession(root, scope = DEFAULT_SESSION_SCOPE) {
@@ -2007,21 +2115,6 @@ function registerKeeper(ctx, opts = {}) {
2007
2115
  });
2008
2116
  }
2009
2117
  //#endregion
2010
- //#region src/constants.ts
2011
- /** 常量(纯模块,零 DSH 依赖) */
2012
- /**
2013
- * ACC 版本:自动从 package.json 读取(单一真相源,消除与 CHANGELOG 的漂移)。
2014
- * 发布时只需改 package.json 的 version。
2015
- */
2016
- const ACC_VERSION = (() => {
2017
- try {
2018
- const here = dirname(fileURLToPath(import.meta.url));
2019
- return JSON.parse(readFileSync(join(here, "..", "package.json"), "utf-8")).version ?? "0.0.0";
2020
- } catch {
2021
- return "0.0.0";
2022
- }
2023
- })();
2024
- //#endregion
2025
2118
  //#region src/skills-discovery.ts
2026
2119
  /**
2027
2120
  * skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
@@ -2195,6 +2288,22 @@ function constraintsBlock(root) {
2195
2288
  ""
2196
2289
  ].join("\n");
2197
2290
  }
2291
+ /**
2292
+ * EAP 自检提示块(DSH 扩展,无 osp 对应——osp 无此块)。
2293
+ * 每次输出前的机械自检清单,强化 EAP 表现(E↑ 显式/R↓ 可重建/S↑ 稳定)。
2294
+ * 独立块而非塞进 CCE/Constraints:后两者受 osp-alignment 逐字节断言约束。
2295
+ */
2296
+ function eapBlock() {
2297
+ return [
2298
+ "",
2299
+ "=== Serenity EAP ===",
2300
+ "每次输出前自检(显式抽象原则:思维的价值 = 外部可重建性):",
2301
+ " • E↑ 显式 — 变量/实体明确定义,关系指明方向/基数,边界划定;不用歧义词(\"处理\"\"优化\"→具体化)",
2302
+ " • R↓ 可重建 — 关键决策记录理由与备选,不跳级讨论(先对齐上层再进下层)",
2303
+ " • S↑ 稳定 — 结构可重复生成,避免依赖隐含上下文",
2304
+ ""
2305
+ ].join("\n");
2306
+ }
2198
2307
  /** 4) SKILL.md 全文:该 CCC 顶层入口 skill 原文(对齐 osp:原文直推,无包裹头;仅过滤治理内容) */
2199
2308
  function entrySkillSectionText(root) {
2200
2309
  const skills = findEntrySkills(root);
@@ -2245,12 +2354,13 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
2245
2354
  ""
2246
2355
  ].join("\n");
2247
2356
  }
2248
- /** 完整系统提示词注入文本:ACC + CCE + Constraints + SKILL 全文 + Session(osp 顺序) */
2357
+ /** 完整系统提示词注入文本:ACC + CCE + Constraints + EAP + SKILL 全文 + Session(osp 顺序 + EAP 扩展) */
2249
2358
  function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
2250
2359
  const parts = [
2251
2360
  accBlock(root),
2252
2361
  cceBlock(),
2253
- constraintsBlock(root)
2362
+ constraintsBlock(root),
2363
+ eapBlock()
2254
2364
  ];
2255
2365
  const skill = entrySkillSectionText(root);
2256
2366
  if (skill) parts.push(skill);
@@ -2258,6 +2368,31 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
2258
2368
  if (session) parts.push(session);
2259
2369
  return parts.join("\n\n");
2260
2370
  }
2371
+ /**
2372
+ * Code Mode 适配行:当前 scope 以 Code Mode 呈现工具时(run_code 可见),
2373
+ * ACC 块按 native 语义指引"直接调用工具"会与"只有 run_code 可直接调用"的执行
2374
+ * 塌缩冲突(模型直呼工具名 → UNKNOWN_TOOL,拒绝信息误导)。追加一行说明,
2375
+ * 引导经 run_code 程序内 tools.* 调用。both 模式不塌缩,该行不误导(程序内
2376
+ * 调用同样合法),故按 run_code 可见性(code|both)统一附加。
2377
+ * @param ctx - 插件 ctx(读 tools 注册表)。
2378
+ * @param scope - 装配 scope(agent);无则按全局视图判断。
2379
+ * @returns 适配行(含换行前缀);非 code/both 返回空串。
2380
+ */
2381
+ function codeModeAdaptationLine(ctx, scope) {
2382
+ try {
2383
+ if (!(ctx.tools.get("run_code", scope) !== void 0)) return "";
2384
+ } catch {
2385
+ return "";
2386
+ }
2387
+ return [
2388
+ "",
2389
+ "=== Serenity Code Mode ===",
2390
+ "当前会话以 Code Mode 呈现工具:模型直接调用 ACC 工具名(cc_fs/acc_msm 等)会被拒绝(UNKNOWN_TOOL)。",
2391
+ "请在一个 run_code 程序内经生成的 SDK 绑定调用:`await tools.cc_fs(...)`、`await tools.acc_msm(...)` 等。",
2392
+ "程序只返回你 print/return 的内容——务必 curate 输出。",
2393
+ ""
2394
+ ].join("\n");
2395
+ }
2261
2396
  /** 从 assembly context 解析 agent cwd(subagent/后台 agent 同样带 agent) */
2262
2397
  function agentCwd(context) {
2263
2398
  return (context.agent?.session)?.header?.cwd;
@@ -2281,7 +2416,9 @@ function registerEntrySkillSectionGlobal(ctx) {
2281
2416
  if (!cwd) return "";
2282
2417
  const root = findSerenityRoot(cwd);
2283
2418
  if (!root) return "";
2284
- return serenitySystemPrompt(root, agentScope$1(context));
2419
+ const base = serenitySystemPrompt(root, agentScope$1(context));
2420
+ const codeLine = codeModeAdaptationLine(ctx, context.scope);
2421
+ return codeLine ? `${base}\n${codeLine}` : base;
2285
2422
  }
2286
2423
  });
2287
2424
  console.log("[serenity-hooks] ✓ 全局入口 skill section 已注册(systemPrompt 就绪)");
@@ -2289,6 +2426,39 @@ function registerEntrySkillSectionGlobal(ctx) {
2289
2426
  console.error(`[serenity-hooks] ✗ 全局入口 skill section 注册失败: ${err.message}(检查 inject 是否含 systemPrompt)`);
2290
2427
  }
2291
2428
  }
2429
+ /**
2430
+ * agent 级 scoped 注册(P0-1:抗 preset/动态插件同名 shadow)。
2431
+ *
2432
+ * 为什么 scoped:DSH 的 systemPrompt section 支持 scoped 层同名 shadow 全局
2433
+ * (agent.ctx.systemPrompt.section),且 scope 链最近层胜出。若 ACC 只注册全局
2434
+ * section,preset 或动态 Cordis 插件可在 scoped 层注册同名 `serenity-entry`
2435
+ * 遮蔽 ACC 身份。scoped 注册在 agent 自身层 = 最近层,任何外部组合无法覆盖。
2436
+ * 全局注册保留为 fallback(未走 session-start 的 agent / 冷恢复路径)。
2437
+ *
2438
+ * text 回调闭包持有 root 与 agent:content 固定(该 agent 的 CCC 身份),
2439
+ * code-mode 适配按装配 context.scope 判断(与全局版一致)。
2440
+ */
2441
+ const sectionedAgents = /* @__PURE__ */ new Set();
2442
+ function registerEntrySkillSection(agent, root) {
2443
+ const key = agent.session.id ?? "global";
2444
+ if (sectionedAgents.has(key)) return false;
2445
+ try {
2446
+ const scope = agent.session.id ?? "default";
2447
+ agent.ctx.systemPrompt.section({
2448
+ name: "serenity-entry",
2449
+ order: -50,
2450
+ text: (context) => {
2451
+ const base = serenitySystemPrompt(root, scope);
2452
+ const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
2453
+ return codeLine ? `${base}\n${codeLine}` : base;
2454
+ }
2455
+ });
2456
+ sectionedAgents.add(key);
2457
+ return true;
2458
+ } catch {
2459
+ return false;
2460
+ }
2461
+ }
2292
2462
  //#endregion
2293
2463
  //#region src/seams/context.ts
2294
2464
  const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
@@ -2345,6 +2515,7 @@ function registerContext(ctx, opts = {}) {
2345
2515
  const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
2346
2516
  if (!root) return;
2347
2517
  const key = agentKey(agent);
2518
+ registerEntrySkillSection(agent, root);
2348
2519
  if (injected.has(key)) return;
2349
2520
  injected.add(key);
2350
2521
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars, agentScope(agent)));
@@ -2360,9 +2531,12 @@ function registerContext(ctx, opts = {}) {
2360
2531
  const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
2361
2532
  const key = agentKey(agent);
2362
2533
  const downstream = await next();
2363
- if (root) try {
2364
- syncSafeModeRestriction(agent, root);
2365
- } catch {}
2534
+ if (root) {
2535
+ try {
2536
+ syncSafeModeRestriction(agent, root);
2537
+ } catch {}
2538
+ registerEntrySkillSection(agent, root);
2539
+ }
2366
2540
  if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
2367
2541
  injected.add(key);
2368
2542
  return {
@@ -2398,45 +2572,6 @@ function registerCompactRetention(ctx, opts = {}) {
2398
2572
  });
2399
2573
  }
2400
2574
  //#endregion
2401
- //#region src/status.ts
2402
- /**
2403
- * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
2404
- *
2405
- * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / loop 模型。
2406
- * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
2407
- */
2408
- function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
2409
- const root = findSerenityRoot(cwd);
2410
- const restrict = getRestrictDiagnostics();
2411
- if (!root) return {
2412
- root: null,
2413
- accVersion: ACC_VERSION,
2414
- safeModeOn: false,
2415
- blacklist: [],
2416
- threshold: null,
2417
- loopModel: null,
2418
- restrict
2419
- };
2420
- const cfg = loadSerenityConfig(root, configPaths);
2421
- return {
2422
- root,
2423
- accVersion: ACC_VERSION,
2424
- safeModeOn: isSafeModeOn(root),
2425
- blacklist: readBlacklist(root, configPaths),
2426
- threshold: cfg.sessionKeeper?.threshold ?? null,
2427
- loopModel: cfg.loop?.defaultModel ?? null,
2428
- restrict
2429
- };
2430
- }
2431
- /** 切换安全模式(写/删标记文件);返回实际生效状态 */
2432
- function setSafeMode(root, on) {
2433
- const marker = resolve(root, SAFE_MODE_MARKER);
2434
- if (on) {
2435
- if (!existsSync(marker)) writeFileSync(marker, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2436
- } else rmSync(marker, { force: true });
2437
- return { on: isSafeModeOn(root) };
2438
- }
2439
- //#endregion
2440
2575
  //#region src/api.ts
2441
2576
  const ROUTE_PATH = "/serenity/status";
2442
2577
  function readBody(req) {
@@ -2479,10 +2614,18 @@ function registerStatusApi(ctx, opts = {}) {
2479
2614
  try {
2480
2615
  if (req.method === "GET") {
2481
2616
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
2482
- sendJson(res, 200, getStatus(resolveWorkspace(ctx, {
2617
+ const status = getStatus(resolveWorkspace(ctx, {
2483
2618
  sessionId: url.searchParams.get("sessionId") ?? void 0,
2484
2619
  workspace: url.searchParams.get("workspace") ?? void 0
2485
- }), configPaths));
2620
+ }), configPaths);
2621
+ const runtime = ctx.get("codeRuntime");
2622
+ sendJson(res, 200, {
2623
+ ...status,
2624
+ ...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
2625
+ language: runtime.language,
2626
+ isolation: runtime.isolation
2627
+ } }
2628
+ });
2486
2629
  return;
2487
2630
  }
2488
2631
  if (req.method === "POST") {
@@ -2662,6 +2805,7 @@ const inject = [
2662
2805
  "shellEnv",
2663
2806
  "skills",
2664
2807
  "agentLoop",
2808
+ "agents",
2665
2809
  "systemPrompt"
2666
2810
  ];
2667
2811
  const Config = z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.16.2",
3
+ "version": "1.16.4",
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": {
@@ -50,6 +50,7 @@
50
50
  "peerDependencies": {
51
51
  "@deepseek-ai/dsh-agent": "^0.1.0-rc.5",
52
52
  "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.5",
53
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.5",
53
54
  "@deepseek-ai/dsh-compaction": "^0.1.0-rc.5",
54
55
  "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.5",
55
56
  "@deepseek-ai/dsh-llm": "^0.1.0-rc.5",
@@ -75,6 +76,9 @@
75
76
  "@deepseek-ai/dsh-agent-loop": {
76
77
  "optional": true
77
78
  },
79
+ "@deepseek-ai/dsh-agent-presets": {
80
+ "optional": true
81
+ },
78
82
  "@deepseek-ai/dsh-compaction": {
79
83
  "optional": true
80
84
  }