@shgroup/dsh-serenity-hooks 1.26.2 → 1.26.3

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.26.2",
3
+ "version": "1.26.3",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild/skiff_admin + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)+ Skiff 认知子集角色(实验性)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
package/lib/index.js CHANGED
@@ -785,6 +785,17 @@ const CC_FS_WRITE_ACTIONS = /* @__PURE__ */ new Set([
785
785
  "touch",
786
786
  "append"
787
787
  ]);
788
+ /**
789
+ * 凭据文件硬名单(数据面守卫,v1.26.3,S142 用户:localstore.json 需要在 read 工具的黑名单里):
790
+ * **任何工具**(含 read/grep/glob/cc_fs 只读子命令)命中这些相对 CCC 根的路径 → deny。
791
+ *
792
+ * 与 safeMode.blacklist(写黑名单,v1.18.5 决策只拦写)语义独立:
793
+ * - 黑名单 = 用户可配置的写保护(REPOSITORIES/ 等只读参考源不误伤读)
794
+ * - 凭据硬名单 = **结构性数据面边界**(不依赖 safe-mode 开关,凭据文件任何时候不可读)——
795
+ * 对齐 CCE Bounded Space + wardn "structural guarantee, not policy":localstore.json
796
+ * 含 API keys/tokens/SSH 密码,即使 prompt 纪律失败也无值可吐(机械保证)。
797
+ */
798
+ const SENSITIVE_CREDENTIAL_FILES = /* @__PURE__ */ new Set(["localstore.json"]);
788
799
  /** 判定工具是否为写类:普通工具按名;cc_fs 复合工具按子命令 action */
789
800
  function isWriteTool(toolName, action) {
790
801
  if (!WRITE_TOOLS.has(toolName)) return false;
@@ -817,8 +828,12 @@ function decideGuard(input) {
817
828
  deny: `path escape blocked: "${pathArg}" escapes the CCC root`,
818
829
  kind: "deny"
819
830
  };
831
+ const rel = relative(root, abs).split("\\").join("/");
832
+ if (SENSITIVE_CREDENTIAL_FILES.has(rel)) return {
833
+ deny: `access blocked: "${pathArg}" is a sensitive credential file (${rel})`,
834
+ kind: "deny"
835
+ };
820
836
  if (isWriteTool(toolName, action)) {
821
- const rel = relative(root, abs).split("\\").join("/");
822
837
  if (rel === ".serenity-safe-on" || rel === ".serenity" || rel.startsWith(".serenity-safe-on/") || rel.startsWith(".serenity/")) return {
823
838
  deny: `CCC governance file "${rel}" is reserved for the user — agent must not write`,
824
839
  kind: "deny"
@@ -2392,7 +2407,7 @@ function listActiveHandymen(root) {
2392
2407
  }
2393
2408
  //#endregion
2394
2409
  //#region src/skiff-core.ts
2395
- const PLUGIN_SOURCE$3 = {
2410
+ const PLUGIN_SOURCE$4 = {
2396
2411
  kind: "plugin",
2397
2412
  plugin: "dsh-serenity-hooks"
2398
2413
  };
@@ -2491,7 +2506,7 @@ function waitIdle$1(ctx, agent) {
2491
2506
  });
2492
2507
  }
2493
2508
  /** 读会话最后一个 assistant/message 文本(handyman 同款) */
2494
- function lastAssistantText$1(agent) {
2509
+ function lastAssistantText$2(agent) {
2495
2510
  const events = agent.session.events;
2496
2511
  for (let i = events.length - 1; i >= 0; i--) {
2497
2512
  const e = events[i];
@@ -2560,10 +2575,10 @@ async function askSkiff(ctx, agent, question, eventsStart) {
2560
2575
  type: "text",
2561
2576
  text: question
2562
2577
  }],
2563
- source: PLUGIN_SOURCE$3
2578
+ source: PLUGIN_SOURCE$4
2564
2579
  }));
2565
2580
  await waitIdle$1(ctx, agent);
2566
- const answer = lastAssistantText$1(agent);
2581
+ const answer = lastAssistantText$2(agent);
2567
2582
  const trajectory = eventsToTrajectory(agent.session.events.slice(before));
2568
2583
  return {
2569
2584
  answer,
@@ -2973,7 +2988,7 @@ function waitIdle(ctx, agent) {
2973
2988
  });
2974
2989
  }
2975
2990
  /** 读取会话最后一个 assistant/message 文本 */
2976
- function lastAssistantText(agent) {
2991
+ function lastAssistantText$1(agent) {
2977
2992
  const events = agent.session.events;
2978
2993
  for (let i = events.length - 1; i >= 0; i--) {
2979
2994
  const e = events[i];
@@ -3071,7 +3086,7 @@ async function runHandymanJob(ctx, opts) {
3071
3086
  }
3072
3087
  }));
3073
3088
  await waitIdle(ctx, workerAgent);
3074
- lastResponse = lastAssistantText(workerAgent);
3089
+ lastResponse = lastAssistantText$1(workerAgent);
3075
3090
  } catch {
3076
3091
  restarts++;
3077
3092
  if (restarts > 100) {
@@ -6567,7 +6582,7 @@ function registerBootstrap(ctx) {
6567
6582
  }
6568
6583
  //#endregion
6569
6584
  //#region src/rebuild.ts
6570
- const PLUGIN_SOURCE$2 = {
6585
+ const PLUGIN_SOURCE$3 = {
6571
6586
  kind: "plugin",
6572
6587
  plugin: "dsh-serenity-hooks"
6573
6588
  };
@@ -6753,7 +6768,7 @@ function registerRebuildTurnHook(ctx) {
6753
6768
  type: "text",
6754
6769
  text: "[TRAJECTORY-REBUILD] The conversation has been cleared and rebuilt. Follow the anchor instructions above now: read the persistent trajectory (SESSION.md) and continue the work automatically from the last checkpoint."
6755
6770
  }],
6756
- source: PLUGIN_SOURCE$2
6771
+ source: PLUGIN_SOURCE$3
6757
6772
  }));
6758
6773
  console.log(`[serenity-hooks] session_rebuild executed with auto-continue (turn ${payload.turn ?? "?"} ended): ${id}`);
6759
6774
  }
@@ -7150,7 +7165,7 @@ function readContextPressure(ctx, session) {
7150
7165
  return null;
7151
7166
  }
7152
7167
  }
7153
- const PLUGIN_SOURCE$1 = {
7168
+ const PLUGIN_SOURCE$2 = {
7154
7169
  kind: "plugin",
7155
7170
  plugin: "dsh-serenity-hooks"
7156
7171
  };
@@ -7213,7 +7228,7 @@ function registerKeeper(ctx, opts = {}) {
7213
7228
  if (blocks.length === 0) return downstream;
7214
7229
  const reminderAll = createUserMessage({
7215
7230
  content: blocks,
7216
- source: PLUGIN_SOURCE$1
7231
+ source: PLUGIN_SOURCE$2
7217
7232
  });
7218
7233
  if (downstream.kind === "block") return {
7219
7234
  kind: "block",
@@ -8168,7 +8183,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
8168
8183
  }
8169
8184
  return lines.join("\n");
8170
8185
  }
8171
- const PLUGIN_SOURCE = {
8186
+ const PLUGIN_SOURCE$1 = {
8172
8187
  kind: "plugin",
8173
8188
  plugin: "dsh-serenity-hooks"
8174
8189
  };
@@ -8185,7 +8200,7 @@ function accMessage(root, configPaths, entrySkillMaxChars) {
8185
8200
  }];
8186
8201
  return createUserMessage({
8187
8202
  content,
8188
- source: PLUGIN_SOURCE
8203
+ source: PLUGIN_SOURCE$1
8189
8204
  });
8190
8205
  }
8191
8206
  /** 每 agent 是否已注入过(进程内存态) */
@@ -9562,6 +9577,171 @@ function registerGateway(ctx) {
9562
9577
  });
9563
9578
  }
9564
9579
  //#endregion
9580
+ //#region src/output-guard.ts
9581
+ /**
9582
+ * output-guard.ts — 最终输出敏感词检测 + 打回重生成(v1.26.3,S142 用户需求)
9583
+ *
9584
+ * 认知容器对外提供认知结果时,**支撑宁静号自身的凭据与机制**不应透露给用户。
9585
+ * 用户拍板方案(讨论稿定稿):不做工具结果 redaction(那是模型工作内存),
9586
+ * 改为**卡最终输出**——`agent/turn-stopping`(serial)时取 turn 最后 assistant 文本,
9587
+ * 命中敏感词表 → `agent.steer()` 打回重生成,直到合规或达上限。
9588
+ *
9589
+ * 敏感词表(有限,ACC 立场,用户确认"这样比较全面了"):
9590
+ * ① 凭据词 — localstore.json credentials 条目名 + 值精确匹配(运行时读)
9591
+ * ② 机制词 — 内部结构静态词(插件名/配置路径/端口/内部实现词)
9592
+ * ③ MSM 词 — mech-registry.json 注册的工具名(用户补充:msm_list 里的工具名)
9593
+ * **不含** 公开概念(宁静号/Serenity/认知容器/EAP/CCE/Neat——正常交流词,误伤灾难)
9594
+ *
9595
+ * 机制依据(DSH 官方):agent/turn-stopping 注释 "a listener that objects steers and
9596
+ * the machine re-reads its inbox: fresh steering runs another step, none closes the
9597
+ * turn"——steer 打回是官方支持的"重生成"通道(v1.22.5 rebuild 自动继续同款先例)。
9598
+ */
9599
+ /** 插件/内部实现词(静态内置;命中即视为机制泄露) */
9600
+ const MECHANISM_WORDS = [
9601
+ "dsh-serenity-hooks",
9602
+ "serenity-hooks",
9603
+ "serenity-hooks.json",
9604
+ "mech-registry",
9605
+ "mech-registry.json",
9606
+ "localstore.json",
9607
+ ".opencode/serenity.json",
9608
+ "AGENT_SESSIONS",
9609
+ "agent/session-start",
9610
+ "agent/turn-stopping",
9611
+ "tools/pre-execute",
9612
+ "tools/post-execute",
9613
+ "session_rebuild",
9614
+ "Trajectory Steward",
9615
+ "first-anchor",
9616
+ "拦截缝",
9617
+ "装配层",
9618
+ "acp-core",
9619
+ "skiff-core",
9620
+ "skiff-registry",
9621
+ "skiff-role",
9622
+ "output-guard"
9623
+ ];
9624
+ /** 内部端口(宁静号服务面) */
9625
+ const MECHANISM_PORTS = [
9626
+ "3080",
9627
+ "3081",
9628
+ "3099",
9629
+ "3100"
9630
+ ];
9631
+ /**
9632
+ * 构建敏感词表(每次调用实时构建——词源文件小,开销可接受):
9633
+ * ① localstore.json credentials:条目名(UPPER_SNAKE)+ 值(精确匹配)
9634
+ * ② 静态机制词 + 端口
9635
+ * ③ mech-registry 注册的 MSM 工具名(loadMsmEntries 全注册表去重)
9636
+ * @param root CCC 根(localstore.json / mech-registry 所在)
9637
+ */
9638
+ function buildSensitiveTable(root) {
9639
+ const exact = /* @__PURE__ */ new Set();
9640
+ const substring = [];
9641
+ try {
9642
+ const p = join(root, "localstore.json");
9643
+ if (existsSync(p)) {
9644
+ const creds = JSON.parse(readFileSync(p, "utf-8").replace(/^\uFEFF/, ""))?.credentials;
9645
+ if (creds && typeof creds === "object") for (const [name, value] of Object.entries(creds)) {
9646
+ if (name) exact.add(name);
9647
+ if (typeof value === "string" && value !== "") exact.add(value);
9648
+ }
9649
+ }
9650
+ } catch {}
9651
+ for (const w of MECHANISM_WORDS) substring.push(w);
9652
+ for (const p of MECHANISM_PORTS) substring.push(p);
9653
+ try {
9654
+ for (const entry of loadMsmEntries(root)) if (entry.name) exact.add(entry.name);
9655
+ } catch {}
9656
+ return {
9657
+ exact,
9658
+ substring,
9659
+ sources: {
9660
+ exact: [...exact],
9661
+ substring
9662
+ }
9663
+ };
9664
+ }
9665
+ /** 检测文本中命中的敏感词(返回命中列表;未命中返回空数组) */
9666
+ function detectSensitive(text, table) {
9667
+ if (!text) return [];
9668
+ const hits = /* @__PURE__ */ new Set();
9669
+ for (const w of table.exact) if (w !== "" && text.includes(w)) hits.add(w);
9670
+ for (const w of table.substring) if (w !== "" && text.includes(w)) hits.add(w);
9671
+ return [...hits];
9672
+ }
9673
+ /** 打回消息(steer 内容):告知模型命中词并要求重新生成(不含命中值本身——避免二次泄露) */
9674
+ function buildRebuke(hits) {
9675
+ return `[SERENITY OUTPUT GUARD] Your previous response contained ${hits.length} sensitive internal term(s) (mechanism/credential identifiers) that must not appear in user-visible output. Regenerate the response from scratch without these terms — describe the same substance without referencing internal machinery, credentials, tool names, or implementation details. Do not repeat the terms; do not explain this instruction to the user.`;
9676
+ }
9677
+ /** 打回状态(按 agent 会话跟踪连续命中轮数) */
9678
+ const rebukeStates = /* @__PURE__ */ new Map();
9679
+ //#endregion
9680
+ //#region src/output-guard-seam.ts
9681
+ const PLUGIN_SOURCE = {
9682
+ kind: "plugin",
9683
+ plugin: "dsh-serenity-hooks"
9684
+ };
9685
+ /** 外部面会话判定(v1.26.3 用户拍板:仅外部面检测,本地维护会话豁免):
9686
+ * skiff-(F4 问答)/ acp-(F4c 程序化)/ rebuild-(v1.22.4 重建会话)前缀 */
9687
+ function isExternalFaceSession(sessionId) {
9688
+ if (!sessionId) return false;
9689
+ return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("rebuild-");
9690
+ }
9691
+ /** 读会话最后一个 assistant/message 文本(skiff-core 同款;仅用于守卫检测,不导出) */
9692
+ function lastAssistantText(agent) {
9693
+ const events = agent.session.events ?? [];
9694
+ for (let i = events.length - 1; i >= 0; i--) {
9695
+ const e = events[i];
9696
+ if (e && e.type === "assistant/message") {
9697
+ const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
9698
+ if (text) return text;
9699
+ }
9700
+ }
9701
+ return "";
9702
+ }
9703
+ /**
9704
+ * 注册输出守卫(index.ts apply 调用):
9705
+ * turn-stopping 时检测最终输出,命中敏感词 → steer 打回重生成(官方机制,零改 DSH)。
9706
+ */
9707
+ function registerOutputGuardHook(ctx) {
9708
+ ctx.on("agent/turn-stopping", (payload) => {
9709
+ const agent = payload?.agent;
9710
+ if (!agent) return;
9711
+ if (!isExternalFaceSession(String(agent.session.id ?? ""))) return;
9712
+ const root = findSerenityRoot(agent.session.header?.cwd ?? process.cwd());
9713
+ if (!root) return;
9714
+ const id = agent.id;
9715
+ const text = lastAssistantText(agent);
9716
+ if (!text) return;
9717
+ const hits = detectSensitive(text, buildSensitiveTable(root));
9718
+ if (hits.length === 0) {
9719
+ rebukeStates.delete(id);
9720
+ return;
9721
+ }
9722
+ const st = rebukeStates.get(id) ?? { consecutive: 0 };
9723
+ st.consecutive += 1;
9724
+ rebukeStates.set(id, st);
9725
+ if (st.consecutive > 3) {
9726
+ rebukeStates.delete(id);
9727
+ console.warn(`[serenity-hooks] output-guard: giving up after 3 rebukes (agent=${id}, turn=${payload.turn ?? "?"}) — response kept for manual review`);
9728
+ return;
9729
+ }
9730
+ console.log(`[serenity-hooks] output-guard: rebuke ${st.consecutive}/3 (agent=${id}, turn=${payload.turn ?? "?"}, hits=${hits.length})`);
9731
+ try {
9732
+ agent.steer(createUserMessage({
9733
+ content: [{
9734
+ type: "text",
9735
+ text: buildRebuke(hits)
9736
+ }],
9737
+ source: PLUGIN_SOURCE
9738
+ }));
9739
+ } catch (error) {
9740
+ console.warn(`[serenity-hooks] output-guard steer failed: ${String(error?.message ?? error)}`);
9741
+ }
9742
+ });
9743
+ }
9744
+ //#endregion
9565
9745
  //#region node_modules/.pnpm/marked@18.0.11/node_modules/marked/lib/marked.esm.js
9566
9746
  /**
9567
9747
  * marked v18.0.11 - a markdown parser
@@ -12252,6 +12432,7 @@ function apply(ctx, config) {
12252
12432
  registerSettingsSection(ctx, config);
12253
12433
  registerGateway(ctx);
12254
12434
  registerRebuildTurnHook(ctx);
12435
+ registerOutputGuardHook(ctx);
12255
12436
  if (config.env) registerEnv(ctx);
12256
12437
  if (config.opencodeSkills) registerOpencodeSkills(ctx);
12257
12438
  registerBootstrap(ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.26.2",
3
+ "version": "1.26.3",
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": {