@shgroup/dsh-serenity-hooks 1.16.3 → 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 +1 -1
- package/lib/index.js +322 -216
- package/package.json +1 -1
package/dsh.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-serenity-hooks",
|
|
3
|
-
"version": "1.16.
|
|
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();
|
|
@@ -1785,157 +2006,6 @@ function createLoopTool(ctx) {
|
|
|
1785
2006
|
});
|
|
1786
2007
|
}
|
|
1787
2008
|
//#endregion
|
|
1788
|
-
//#region src/seams/guards.ts
|
|
1789
|
-
/**
|
|
1790
|
-
* guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
|
|
1791
|
-
*
|
|
1792
|
-
* 纯决策逻辑(decideGuard)与 DSH 注册(registerGuards)分离:
|
|
1793
|
-
* 前者零 DSH 依赖可单测,后者把决策接到 tools/pre-execute 瀑布 + ctx.tools.guard 终局。
|
|
1794
|
-
*
|
|
1795
|
-
* 对应 opencode-serenity-plugin 的 tool.execute.before 路径守卫 + bash 开关 + 黑名单。
|
|
1796
|
-
*/
|
|
1797
|
-
/**
|
|
1798
|
-
* 安全模式 + 黑名单 + P3 路径守卫的纯决策。
|
|
1799
|
-
* 对齐 opencode-serenity-plugin 标准:安全模式 = **bash 禁用** + 写入黑名单;
|
|
1800
|
-
* write/edit 等工具仅受路径逃逸与黑名单约束(不整体禁用)。
|
|
1801
|
-
* 优先级:safe-mode bash > 路径越界 > 黑名单命中。
|
|
1802
|
-
*/
|
|
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
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
return { kind: "allow" };
|
|
1826
|
-
}
|
|
1827
|
-
/** 从 exec 提取 agent 会话 cwd(CCC 根检测基准);无则回退进程 cwd */
|
|
1828
|
-
function resolveAgentCwd(exec) {
|
|
1829
|
-
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1830
|
-
}
|
|
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() {
|
|
1843
|
-
return {
|
|
1844
|
-
...restrictDiag,
|
|
1845
|
-
activeKeys: [...safeModeRestrictions.keys()]
|
|
1846
|
-
};
|
|
1847
|
-
}
|
|
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 {}
|
|
1858
|
-
}
|
|
1859
|
-
/**
|
|
1860
|
-
* 同步安全模式工具隐藏:标记存在 → agent.ctx.tools.restrict deny 隐藏写工具;
|
|
1861
|
-
* 标记消失 → 解除。pre-step 每步调用 → 切换实时生效。
|
|
1862
|
-
*/
|
|
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);
|
|
1890
|
-
}
|
|
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;
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
/**
|
|
1907
|
-
* 注册守卫:tools/pre-execute 瀑布 + ctx.tools.guard 终局。
|
|
1908
|
-
* 两者都从 CCC 根实时读取配置(无状态、无缓存)。
|
|
1909
|
-
*/
|
|
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
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
//#endregion
|
|
1939
2009
|
//#region src/seams/loop.ts
|
|
1940
2010
|
/** 解析当前 dsh 会话 scope 的活跃会话 SESSION.md 绝对路径;无标记/越界返回 null */
|
|
1941
2011
|
function resolveActiveSession(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
@@ -2045,21 +2115,6 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
2045
2115
|
});
|
|
2046
2116
|
}
|
|
2047
2117
|
//#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
2118
|
//#region src/skills-discovery.ts
|
|
2064
2119
|
/**
|
|
2065
2120
|
* skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
|
|
@@ -2233,6 +2288,22 @@ function constraintsBlock(root) {
|
|
|
2233
2288
|
""
|
|
2234
2289
|
].join("\n");
|
|
2235
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
|
+
}
|
|
2236
2307
|
/** 4) SKILL.md 全文:该 CCC 顶层入口 skill 原文(对齐 osp:原文直推,无包裹头;仅过滤治理内容) */
|
|
2237
2308
|
function entrySkillSectionText(root) {
|
|
2238
2309
|
const skills = findEntrySkills(root);
|
|
@@ -2283,12 +2354,13 @@ function sessionBlock(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
2283
2354
|
""
|
|
2284
2355
|
].join("\n");
|
|
2285
2356
|
}
|
|
2286
|
-
/** 完整系统提示词注入文本:ACC + CCE + Constraints + SKILL 全文 + Session(osp
|
|
2357
|
+
/** 完整系统提示词注入文本:ACC + CCE + Constraints + EAP + SKILL 全文 + Session(osp 顺序 + EAP 扩展) */
|
|
2287
2358
|
function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
2288
2359
|
const parts = [
|
|
2289
2360
|
accBlock(root),
|
|
2290
2361
|
cceBlock(),
|
|
2291
|
-
constraintsBlock(root)
|
|
2362
|
+
constraintsBlock(root),
|
|
2363
|
+
eapBlock()
|
|
2292
2364
|
];
|
|
2293
2365
|
const skill = entrySkillSectionText(root);
|
|
2294
2366
|
if (skill) parts.push(skill);
|
|
@@ -2296,6 +2368,31 @@ function serenitySystemPrompt(root, scope = DEFAULT_SESSION_SCOPE) {
|
|
|
2296
2368
|
if (session) parts.push(session);
|
|
2297
2369
|
return parts.join("\n\n");
|
|
2298
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
|
+
}
|
|
2299
2396
|
/** 从 assembly context 解析 agent cwd(subagent/后台 agent 同样带 agent) */
|
|
2300
2397
|
function agentCwd(context) {
|
|
2301
2398
|
return (context.agent?.session)?.header?.cwd;
|
|
@@ -2319,7 +2416,9 @@ function registerEntrySkillSectionGlobal(ctx) {
|
|
|
2319
2416
|
if (!cwd) return "";
|
|
2320
2417
|
const root = findSerenityRoot(cwd);
|
|
2321
2418
|
if (!root) return "";
|
|
2322
|
-
|
|
2419
|
+
const base = serenitySystemPrompt(root, agentScope$1(context));
|
|
2420
|
+
const codeLine = codeModeAdaptationLine(ctx, context.scope);
|
|
2421
|
+
return codeLine ? `${base}\n${codeLine}` : base;
|
|
2323
2422
|
}
|
|
2324
2423
|
});
|
|
2325
2424
|
console.log("[serenity-hooks] ✓ 全局入口 skill section 已注册(systemPrompt 就绪)");
|
|
@@ -2327,6 +2426,39 @@ function registerEntrySkillSectionGlobal(ctx) {
|
|
|
2327
2426
|
console.error(`[serenity-hooks] ✗ 全局入口 skill section 注册失败: ${err.message}(检查 inject 是否含 systemPrompt)`);
|
|
2328
2427
|
}
|
|
2329
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
|
+
}
|
|
2330
2462
|
//#endregion
|
|
2331
2463
|
//#region src/seams/context.ts
|
|
2332
2464
|
const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
|
|
@@ -2383,6 +2515,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
2383
2515
|
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
2384
2516
|
if (!root) return;
|
|
2385
2517
|
const key = agentKey(agent);
|
|
2518
|
+
registerEntrySkillSection(agent, root);
|
|
2386
2519
|
if (injected.has(key)) return;
|
|
2387
2520
|
injected.add(key);
|
|
2388
2521
|
agent.inject(accMessage(root, configPaths, entrySkillMaxChars, agentScope(agent)));
|
|
@@ -2398,9 +2531,12 @@ function registerContext(ctx, opts = {}) {
|
|
|
2398
2531
|
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
2399
2532
|
const key = agentKey(agent);
|
|
2400
2533
|
const downstream = await next();
|
|
2401
|
-
if (root)
|
|
2402
|
-
|
|
2403
|
-
|
|
2534
|
+
if (root) {
|
|
2535
|
+
try {
|
|
2536
|
+
syncSafeModeRestriction(agent, root);
|
|
2537
|
+
} catch {}
|
|
2538
|
+
registerEntrySkillSection(agent, root);
|
|
2539
|
+
}
|
|
2404
2540
|
if (!root || injected.has(key) || downstream.kind !== "enter") return downstream;
|
|
2405
2541
|
injected.add(key);
|
|
2406
2542
|
return {
|
|
@@ -2436,45 +2572,6 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
2436
2572
|
});
|
|
2437
2573
|
}
|
|
2438
2574
|
//#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
2575
|
//#region src/api.ts
|
|
2479
2576
|
const ROUTE_PATH = "/serenity/status";
|
|
2480
2577
|
function readBody(req) {
|
|
@@ -2517,10 +2614,18 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
2517
2614
|
try {
|
|
2518
2615
|
if (req.method === "GET") {
|
|
2519
2616
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
2520
|
-
|
|
2617
|
+
const status = getStatus(resolveWorkspace(ctx, {
|
|
2521
2618
|
sessionId: url.searchParams.get("sessionId") ?? void 0,
|
|
2522
2619
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
2523
|
-
}), 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
|
+
});
|
|
2524
2629
|
return;
|
|
2525
2630
|
}
|
|
2526
2631
|
if (req.method === "POST") {
|
|
@@ -2700,6 +2805,7 @@ const inject = [
|
|
|
2700
2805
|
"shellEnv",
|
|
2701
2806
|
"skills",
|
|
2702
2807
|
"agentLoop",
|
|
2808
|
+
"agents",
|
|
2703
2809
|
"systemPrompt"
|
|
2704
2810
|
];
|
|
2705
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.
|
|
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": {
|