@shgroup/dsh-serenity-hooks 1.86.0 → 1.87.1

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.86.0",
3
+ "version": "1.87.1",
4
4
  "main": "lib/index.js",
5
5
  "description": "宁静号 ACC harness(Native Cordis 插件):给 DSH 装一个「AI 工作区」——14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
@@ -128,8 +128,29 @@ export declare function __resetImChannelsForTest(): void;
128
128
  /** 已注册通道 id(工具 `channel` 参数枚举) */
129
129
  export declare function imChannelIds(): string[];
130
130
  /**
131
- * 可见性判据:本 CCC 是否启用了**任一** IM 通道。
131
+ * 可见性判据:本 CCC 是否**有任何可用的 IM 账号**。
132
132
  * 未启用 → 工具面对该 agent **不可见**(`tools.restrict` 移除,而非调用时报错)。
133
+ *
134
+ * ## 🔴 2026-10-22 判据修正(owner 令:「把开关升级为有任意账号即可,而不是盯着微信」)
135
+ *
136
+ * **旧判据** = 遍历已注册通道、任一 `channel.isEnabled(root)` 为真即可见。
137
+ * **旧判据的缺陷**(实测于一个只配飞书的 CCC):`feishuChannel.isEnabled()` 首行是
138
+ * `if (settings.enabled !== true) return false` —— 总开关一关,**后面的账号判断根本不执行**
139
+ * ⇒ 账号明明配了、`enabled:true`,却因为一个**渠道无关的总开关**被永久判成"未启用"
140
+ * ⇒ `im-bridge` 在模型清单里彻底消失,而配置面看上去是对的(`accounts[]` 里躺着一条 feishu)。
141
+ * 这正是 D179 的形态:**配置说它有,行为说它没有**,且零报错。
142
+ *
143
+ * **新判据** = **账号即事实**:`readImSettings(root).accounts` 里存在
144
+ * 「`enabled !== false` 的账号」即视为可见。理由三条:
145
+ * ① **可见性问的是"这个 CCC 有没有 IM 能力"**,而能力的载体是**账号**,不是总开关;
146
+ * ② 总开关的语义是"**要不要让桥跑起来**",把它当成可见性上限,会让"配好了但暂时不想让它跑"
147
+ * 这种合法状态变成"工具凭空消失";
148
+ * ③ 判**按账号判**后,微信与飞书不再各有一套"总开关 + 账号"的双重判据 ⇒ 消除同族漂移。
149
+ *
150
+ * ⚠️ **不放宽的部分**:没有账号 ⇒ 仍然不可见(`accounts` 为空即 false)。
151
+ * ⚠️ 单个通道自己的 `isEnabled` 仍是**发送/启停**判据(那是通道自己的事),
152
+ * 只是**不再兼任**工具可见性判据 —— 两件事被这次修正分开了。
153
+ *
133
154
  * @param root 本会话 CCC 根(由调用方解析)
134
155
  */
135
156
  export declare function hasEnabledImChannel(root: string): boolean;
package/lib/index.js CHANGED
@@ -7,7 +7,7 @@ import { _ as resolveConstraintSystemPrompt, a as listBoundSessionIds, d as cons
7
7
  import { A as buildResourceIndex, C as verifyPassword, D as recordToolUsage, E as verifyTotpCode, F as hasTrajectoryPrompt, M as isSafeSkillName, N as truncateContent, O as REFERENCE_DIR, P as TRAJECTORY_PROMPT_FILENAME, S as toWire, T as MECHANISM_PORTS, _ as registerEntrySkillSectionGlobal, a as trajectoryConstraintFor, b as projectKnownWorkspaces, c as trajectoryWorkspaceMdPath, d as CHEAP_WORKER_GUIDE, f as providerAvailabilityError, g as registerEntrySkillSection, h as registerCognitiveTrajectorySkillSection, i as getTrajectoryAgent, j as buildResourceNameSuffix, k as SKILL_ONTOLOGY_FILE, l as unregisterTrajectorySession, m as splitModel, n as createTrajectoryAgent, o as trajectoryConstraintMsmGate, p as requireWhitelistedModel, r as ensureWorkspacePromptSection, s as trajectoryConstraintParticipates, t as askTrajectory, u as workspaceCognitiveTrajectoryLine, v as applyWirePatch, w as FACE_PORTS, x as readAdvancedSettings, y as migrateLegacyLocalstore } from "./session-DZXSLLEx.js";
8
8
  import { n as hasSessionLogById, r as sessionsRootDir } from "./cleanup-C-92qnDs.js";
9
9
  import { n as readSimpleSettings, r as registerSettingsSection } from "./settings-H7TVDayZ.js";
10
- import { a as readFeishuCredential, d as weixinSessionIdForTrajectory, i as matchWeixinRoute, l as sanitizeFileName$1, n as extractWeixinText, o as readWeixinCredential, r as hasVoiceItem, s as readWeixinSettings, t as extractWeixinMedia, u as weixinInboundDir } from "./route-GtYJdR06.js";
10
+ import { a as readFeishuCredential, c as readWeixinSettings, d as weixinInboundDir, f as weixinSessionIdForTrajectory, i as matchWeixinRoute, n as extractWeixinText, o as readImSettings, r as hasVoiceItem, s as readWeixinCredential, t as extractWeixinMedia, u as sanitizeFileName$1 } from "./route-3OzOAjec.js";
11
11
  import { REGISTERED_TOOLS } from "./invariant.js";
12
12
  import { c as sendTextMessage, i as getUpdates, l as sendTyping, n as downloadMedia, o as markdownToPlainText, r as getConfig, s as sendFileMessage, t as TypingStatus, u as sniffImageExt } from "./ilink-BMk2w2KY.js";
13
13
  import { a as registerImAccountImpl, i as normalizeAccountChannel, n as getImAccountImpl, r as imAccountChannels } from "./accounts-BHQbakUh.js";
@@ -11638,15 +11638,39 @@ function imChannelIds() {
11638
11638
  return [...channels.keys()];
11639
11639
  }
11640
11640
  /**
11641
- * 可见性判据:本 CCC 是否启用了**任一** IM 通道。
11641
+ * 可见性判据:本 CCC 是否**有任何可用的 IM 账号**。
11642
11642
  * 未启用 → 工具面对该 agent **不可见**(`tools.restrict` 移除,而非调用时报错)。
11643
+ *
11644
+ * ## 🔴 2026-10-22 判据修正(owner 令:「把开关升级为有任意账号即可,而不是盯着微信」)
11645
+ *
11646
+ * **旧判据** = 遍历已注册通道、任一 `channel.isEnabled(root)` 为真即可见。
11647
+ * **旧判据的缺陷**(实测于一个只配飞书的 CCC):`feishuChannel.isEnabled()` 首行是
11648
+ * `if (settings.enabled !== true) return false` —— 总开关一关,**后面的账号判断根本不执行**
11649
+ * ⇒ 账号明明配了、`enabled:true`,却因为一个**渠道无关的总开关**被永久判成"未启用"
11650
+ * ⇒ `im-bridge` 在模型清单里彻底消失,而配置面看上去是对的(`accounts[]` 里躺着一条 feishu)。
11651
+ * 这正是 D179 的形态:**配置说它有,行为说它没有**,且零报错。
11652
+ *
11653
+ * **新判据** = **账号即事实**:`readImSettings(root).accounts` 里存在
11654
+ * 「`enabled !== false` 的账号」即视为可见。理由三条:
11655
+ * ① **可见性问的是"这个 CCC 有没有 IM 能力"**,而能力的载体是**账号**,不是总开关;
11656
+ * ② 总开关的语义是"**要不要让桥跑起来**",把它当成可见性上限,会让"配好了但暂时不想让它跑"
11657
+ * 这种合法状态变成"工具凭空消失";
11658
+ * ③ 判**按账号判**后,微信与飞书不再各有一套"总开关 + 账号"的双重判据 ⇒ 消除同族漂移。
11659
+ *
11660
+ * ⚠️ **不放宽的部分**:没有账号 ⇒ 仍然不可见(`accounts` 为空即 false)。
11661
+ * ⚠️ 单个通道自己的 `isEnabled` 仍是**发送/启停**判据(那是通道自己的事),
11662
+ * 只是**不再兼任**工具可见性判据 —— 两件事被这次修正分开了。
11663
+ *
11643
11664
  * @param root 本会话 CCC 根(由调用方解析)
11644
11665
  */
11645
11666
  function hasEnabledImChannel(root) {
11646
- for (const channel of channels.values()) try {
11647
- if (channel.isEnabled(root)) return true;
11648
- } catch {}
11649
- return false;
11667
+ let settings;
11668
+ try {
11669
+ settings = readImSettings(root);
11670
+ } catch {
11671
+ return false;
11672
+ }
11673
+ return (settings.accounts ?? []).some((a) => a.enabled !== false);
11650
11674
  }
11651
11675
  /**
11652
11676
  * 错误码 → 可行动提示(模型视角:说清怎么改)。
@@ -11847,6 +11871,125 @@ async function runImBridge(root, req) {
11847
11871
  return fail("ACTION_UNKNOWN", `unknown action "${action}"`);
11848
11872
  }
11849
11873
  //#endregion
11874
+ //#region src/seams/tool-visibility.ts
11875
+ /** ACC 自己注册的工具名集合(退化档只发这些 —— 它们一定在册) */
11876
+ const REGISTERED_TOOLS_SET = new Set(REGISTERED_TOOLS);
11877
+ /** 收集器:把各来源的 deny 合并成一个判决(**纯函数、无副作用**) */
11878
+ function collectVisibilityDecision(sources) {
11879
+ const deny = /* @__PURE__ */ new Set();
11880
+ let hasError = false;
11881
+ for (const s of sources) {
11882
+ if (s.error !== void 0) hasError = true;
11883
+ if (!s.ok) continue;
11884
+ for (const name of s.deny) if (typeof name === "string" && name !== "") deny.add(name);
11885
+ }
11886
+ return {
11887
+ deny: [...deny].sort(),
11888
+ sources,
11889
+ hasError
11890
+ };
11891
+ }
11892
+ /** 判决签名(幂等判据:签名相同 ⇒ 目标态已达到 ⇒ **一次 restrict 都不发**) */
11893
+ function decisionSignature(decision) {
11894
+ return decision.deny.join(",");
11895
+ }
11896
+ /** `${sessionId}` → 已发射状态(**全模块唯一的一份**) */
11897
+ const applied = /* @__PURE__ */ new Map();
11898
+ /**
11899
+ * 发射一次判决(**唯一的写点**)。
11900
+ *
11901
+ * 三条纪律:
11902
+ * ① **幂等**:签名与当前已发射的一致 ⇒ 直接返回 `'unchanged'`,**不碰宿主**
11903
+ * (这正是消除"工具清单闪动"的那一步)。
11904
+ * ② **先撤后发**:宿主的 disposer 才是"撤销上一次",先撤再发才不会叠加旧集合。
11905
+ * (旧集合是新集合的子集时叠不叠都看不出来,但**不是子集时**会残留 —— 必须撤。)
11906
+ * ③ 🔴 **未注册的名字不许炸掉整次发射**:宿主契约"unknown names … fail"。
11907
+ * 合并后集合里只要有一个不在册的名字,整次 restrict 就失败 ⇒ 六个机制**全都不生效**。
11908
+ * ⇒ 策略 = **直接发 → 全量被拒才退到「只发 ACC 自己注册的那些」一档**,退档时
11909
+ * 把被放弃的名字如实记账(进 `dropped`,由调用方决定要不要告警)。
11910
+ * ⚠️ **不做 `tools.get` 预探活** —— 那个视图查不出 agent-plane 贡献的工具
11911
+ * (`bash` / `schedule_*`),会把**在册的名字误判成未注册**(见下方发射段的实测记录)。
11912
+ * ⚠️ 这比旧行为**更保守**:旧行为是"某条 restrict 失败 ⇒ 那一格静默不生效",
11913
+ * 新行为是"全量失败 ⇒ 至少 ACC 那一格照常生效" —— **等价或更好**,且总有日志。
11914
+ *
11915
+ * @returns `'unchanged'` / `'applied'` / `'failed'`(`failed` 时 `deny` 保持原样)
11916
+ */
11917
+ function applyVisibilityDecision(agent, sessionId, decision) {
11918
+ const signature = decisionSignature(decision);
11919
+ const existing = applied.get(sessionId);
11920
+ if (existing !== void 0 && existing.signature === signature) return {
11921
+ status: "unchanged",
11922
+ dropped: []
11923
+ };
11924
+ if (existing !== void 0) {
11925
+ try {
11926
+ existing.dispose();
11927
+ } catch {}
11928
+ applied.delete(sessionId);
11929
+ }
11930
+ const tools = agent.ctx.tools;
11931
+ const dropped = [];
11932
+ const names = decision.deny;
11933
+ if (names.length === 0) {
11934
+ applied.set(sessionId, {
11935
+ signature,
11936
+ deny: [],
11937
+ dispose: () => {}
11938
+ });
11939
+ return {
11940
+ status: "applied",
11941
+ dropped
11942
+ };
11943
+ }
11944
+ try {
11945
+ const dispose = tools.restrict({ deny: [...names] });
11946
+ applied.set(sessionId, {
11947
+ signature,
11948
+ deny: names,
11949
+ dispose
11950
+ });
11951
+ return {
11952
+ status: "applied",
11953
+ dropped
11954
+ };
11955
+ } catch (firstError) {
11956
+ const accOnly = names.filter((n) => REGISTERED_TOOLS_SET.has(n));
11957
+ if (accOnly.length === 0 || accOnly.length === names.length) return {
11958
+ status: "failed",
11959
+ dropped,
11960
+ error: firstError.message
11961
+ };
11962
+ const abandoned = names.filter((n) => !REGISTERED_TOOLS_SET.has(n));
11963
+ try {
11964
+ const dispose = tools.restrict({ deny: accOnly });
11965
+ applied.set(sessionId, {
11966
+ signature,
11967
+ deny: accOnly,
11968
+ dispose
11969
+ });
11970
+ return {
11971
+ status: "applied",
11972
+ dropped: abandoned
11973
+ };
11974
+ } catch (secondError) {
11975
+ return {
11976
+ status: "failed",
11977
+ dropped,
11978
+ error: secondError.message
11979
+ };
11980
+ }
11981
+ }
11982
+ }
11983
+ /** 会话销毁清理(防 per-会话 Map 无界增长;lifecycle 调用) */
11984
+ function forgetToolVisibility(sessionId) {
11985
+ const existing = applied.get(sessionId);
11986
+ if (existing === void 0) return;
11987
+ try {
11988
+ existing.dispose();
11989
+ } catch {}
11990
+ applied.delete(sessionId);
11991
+ }
11992
+ //#endregion
11850
11993
  //#region src/seams/guards.ts
11851
11994
  /**
11852
11995
  * guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
@@ -11983,6 +12126,119 @@ function decideGuard(input) {
11983
12126
  }
11984
12127
  return { kind: "allow" };
11985
12128
  }
12129
+ /**
12130
+ * 🔴 **工具可见性的唯一发射入口**(方案 C 的落地)。
12131
+ *
12132
+ * ## 它取代了什么
12133
+ *
12134
+ * 改之前有六条独立 sync,每条在自己的 per-会话 Map 里放一个 `restrict` disposer,
12135
+ * 且都由 `pre-step`(每步)与 `seed`(冷载入)各自调用 ⇒ 一次交互里可能发出多次
12136
+ * `tools.restrict` ⇒ 宿主每次发 `tools/change` ⇒ 模型侧看到工具清单**先进后出**
12137
+ * (实测症状:`新增 7 个` 紧跟 `移除 7 个`)。
12138
+ *
12139
+ * ## 为什么合并是对的(不是简化)
12140
+ *
12141
+ * 宿主 `ToolRuntime.restrict` 契约逐字:**Restrictions intersect**。
12142
+ * ⇒ N 次 `deny: Dᵢ` 的净效果 = `⋃Dᵢ`。先求并集只发一次,**结果相同**、只触发一次 `tools/change`。
12143
+ * 前提 = **只用 `deny`**(`allow` 的"交集"不满足并集律);本仓 18 处 restrict 全部只发 `deny`。
12144
+ *
12145
+ * ## 正确性怎么保住(这是本设计被质疑的那一点)
12146
+ *
12147
+ * 六源**各自独立**算自己的 deny 并**各自 catch**:某一个来源读配置抛错 ⇒ 它返回
12148
+ * `ok:false`(不贡献 deny),**其余五源照常生效**。⇒ 合并**不会**把一格的失败放大成六格。
12149
+ * 全量计算见 {@link computeVisibilitySources}(纯函数)。
12150
+ *
12151
+ * @param agent 目标会话
12152
+ * @param root CCC 根;`null` ⇒ 不在 CCC 内 ⇒ 隐藏**全部** ACC 工具
12153
+ */
12154
+ function syncToolVisibility(agent, root) {
12155
+ const sessionId = agent.session.id ?? "global";
12156
+ const sources = computeVisibilitySources(sessionId, root);
12157
+ const decision = collectVisibilityDecision(sources);
12158
+ if (root !== null && decision.hasError) {
12159
+ const bad = sources.filter((s) => s.error !== void 0);
12160
+ for (const s of bad) console.error(`[serenity-hooks] 工具可见性来源「${s.id}」判据失败(其余来源照常生效): ${s.error ?? ""}`);
12161
+ }
12162
+ const r = applyVisibilityDecision(agent, sessionId, decision);
12163
+ if (r.status === "failed") console.error(`[serenity-hooks] 工具可见性发射失败 (session=${sessionId}): ${r.error ?? ""}`);
12164
+ else if (r.dropped.length > 0) console.log(`[serenity-hooks] 工具可见性:跳过 ${r.dropped.length} 个未注册名(${r.dropped.join(", ")})`);
12165
+ }
12166
+ /**
12167
+ * 六源 → 各自的 deny 集合(**纯函数:只读配置、不碰宿主**)。
12168
+ *
12169
+ * 每条来源**自己负责自己的失败**:抛错就 catch 在这里,转成 `ok:false + error`。
12170
+ * ⛔ 绝不让一个来源的异常冒泡 —— 那会让另外五源一起失效(正是合并最容易犯的错)。
12171
+ */
12172
+ function computeVisibilitySources(sessionId, root) {
12173
+ const out = [];
12174
+ out.push(sourceOf("nonCcc", () => {
12175
+ if (root !== null) return { deny: [] };
12176
+ return { deny: [...REGISTERED_TOOLS] };
12177
+ }));
12178
+ if (root === null) {
12179
+ for (const id of [
12180
+ "safeMode",
12181
+ "imBridge",
12182
+ "exclusive",
12183
+ "hostSchedule",
12184
+ "trajectoryConstraint"
12185
+ ]) out.push({
12186
+ id,
12187
+ deny: [],
12188
+ ok: false
12189
+ });
12190
+ return out;
12191
+ }
12192
+ out.push(sourceOf("safeMode", () => {
12193
+ if (!isSafeModeOn(root)) return { deny: [] };
12194
+ let declared = [];
12195
+ try {
12196
+ declared = readSafeModeHiddenTools(root);
12197
+ } catch (e) {
12198
+ return {
12199
+ deny: [...SAFE_MODE_DENY_TOOLS],
12200
+ error: e.message
12201
+ };
12202
+ }
12203
+ return { deny: [.../* @__PURE__ */ new Set([...SAFE_MODE_DENY_TOOLS, ...declared])] };
12204
+ }));
12205
+ out.push(sourceOf("imBridge", () => hasEnabledImChannel(root) ? { deny: [] } : { deny: [IM_BRIDGE_TOOL_NAME] }));
12206
+ out.push(sourceOf("exclusive", () => {
12207
+ const declared = readExclusiveTools(root);
12208
+ return { deny: EXCLUSIVE_TOOL_NAMES.filter((t) => !declared.includes(t)) };
12209
+ }));
12210
+ out.push(sourceOf("hostSchedule", () => ({ deny: [...HOST_SCHEDULE_TOOL_NAMES] })));
12211
+ out.push(sourceOf("trajectoryConstraint", () => {
12212
+ const profile = trajectoryConstraintForSession(root, sessionId);
12213
+ if (profile === null) return { deny: [] };
12214
+ const allowed = profileToolWhitelist(profile);
12215
+ if (allowed === null) return { deny: [] };
12216
+ return { deny: REGISTERED_TOOLS.filter((n) => !allowed.has(n)) };
12217
+ }));
12218
+ return out;
12219
+ }
12220
+ /**
12221
+ * 把一个"可能抛错的 denies 计算"包成来源(**统一在这里 catch**)。
12222
+ * 失败 ⇒ `ok:false`:本来源不贡献 deny,**其余来源不受影响**。
12223
+ */
12224
+ function sourceOf(id, fn) {
12225
+ try {
12226
+ const r = fn();
12227
+ return {
12228
+ id,
12229
+ deny: r.deny,
12230
+ ok: true,
12231
+ ...r.error !== void 0 ? { error: r.error } : {}
12232
+ };
12233
+ } catch (e) {
12234
+ return {
12235
+ id,
12236
+ deny: [],
12237
+ ok: false,
12238
+ error: e.message
12239
+ };
12240
+ }
12241
+ }
11986
12242
  /** 从 exec 提取 agent 会话 cwd(CCC 根检测基准);无则回退进程 cwd */
11987
12243
  function resolveAgentCwd(exec) {
11988
12244
  return exec.agent?.session?.header?.cwd ?? process.cwd();
@@ -12008,168 +12264,18 @@ function getRestrictDiagnostics() {
12008
12264
  denyByKey: Object.fromEntries([...safeModeRestrictions].map(([k, v]) => [k, v.deny]))
12009
12265
  };
12010
12266
  }
12011
- /** 诊断落盘:AGENT_SESSIONS/.restrict-diag.json(文件通道,避免 HTTP 自锁) */
12012
- function writeRestrictDiag(root) {
12013
- try {
12014
- const dir = resolve(root, "AGENT_SESSIONS");
12015
- mkdirSync(dir, { recursive: true });
12016
- writeFileSync(resolve(dir, ".restrict-diag.json"), JSON.stringify({
12017
- ...getRestrictDiagnostics(),
12018
- cccRoot: root
12019
- }, null, 2) + "\n", "utf-8");
12020
- } catch {}
12021
- }
12022
- /**
12023
- * 同步安全模式工具隐藏:标记存在 → `agent.ctx.tools.restrict deny` 隐藏
12024
- * **基础表(bash)∪ CCC 声明的 `safeMode.hiddenTools`**;标记消失 → 全部解除。
12025
- * pre-step 每步调用 ⇒ 开/关/**改声明**三种变化都实时生效(v1.51.4 起含"改声明")。
12026
- *
12027
- * 与 `readSafeModeHiddenTools` 的分工(R↓):本函数负责**形状与失败退化**,reader 只管取值。
12028
- * 🔴 退化规则:DSH 契约规定 **未注册的工具名会让整次 restrict 失败**("unknown names … fail")
12029
- * ⇒ 绝不能让 CCC 写错一个名字就丢掉 safe-mode 的核心隐藏(bash)。失败时退化为**只隐藏基础表**,
12030
- * 并在日志响亮报错;退化态**按期望签名记账**,不会每步重试刷屏。
12031
- */
12032
- function syncSafeModeRestriction(agent, root) {
12033
- const key = agent.session.id ?? "global";
12034
- const on = isSafeModeOn(root);
12035
- const existing = safeModeRestrictions.get(key);
12036
- if (!on) {
12037
- if (existing !== void 0) {
12038
- try {
12039
- existing.dispose();
12040
- } catch (e) {
12041
- console.error(`[serenity-hooks] restrict 解除失败 (key=${key}):`, e.message);
12042
- }
12043
- safeModeRestrictions.delete(key);
12044
- }
12045
- writeRestrictDiag(root);
12046
- return;
12047
- }
12048
- let declared = [];
12049
- try {
12050
- declared = readSafeModeHiddenTools(root);
12051
- } catch (e) {
12052
- console.error(`[serenity-hooks] safeMode.hiddenTools 判据读取失败 (key=${key}):`, e.message);
12053
- }
12054
- const deny = [.../* @__PURE__ */ new Set([...SAFE_MODE_DENY_TOOLS, ...declared])];
12055
- const signature = deny.join(",");
12056
- if (existing !== void 0 && existing.signature === signature) {
12057
- writeRestrictDiag(root);
12058
- return;
12059
- }
12060
- if (existing !== void 0) {
12061
- try {
12062
- existing.dispose();
12063
- } catch (e) {
12064
- console.error(`[serenity-hooks] restrict 重算前解除失败 (key=${key}):`, e.message);
12065
- }
12066
- safeModeRestrictions.delete(key);
12067
- }
12068
- let applied = deny;
12069
- let dispose;
12070
- try {
12071
- dispose = agent.ctx.tools.restrict({ deny: [...deny] });
12072
- } catch (e) {
12073
- restrictDiag.lastKey = key;
12074
- restrictDiag.lastAttemptAt = isoLocal();
12075
- restrictDiag.lastSuccess = false;
12076
- restrictDiag.lastError = e.message;
12077
- console.error(`[serenity-hooks] restrict 失败,已退化为基础表(多为 safeMode.hiddenTools 里有本 CCC **未注册**的工具名)(key=${key}):`, e.message);
12078
- applied = [...SAFE_MODE_DENY_TOOLS];
12079
- try {
12080
- dispose = agent.ctx.tools.restrict({ deny: applied });
12081
- } catch (e2) {
12082
- restrictDiag.lastError = `${e.message} / 退化亦失败: ${e2.message}`;
12083
- console.error(`[serenity-hooks] restrict 退化失败(守卫仍兜底拦截)(key=${key}):`, e2.message);
12084
- safeModeRestrictions.set(key, {
12085
- signature,
12086
- deny: [],
12087
- dispose: () => {}
12088
- });
12089
- writeRestrictDiag(root);
12090
- return;
12091
- }
12092
- }
12093
- restrictDiag.lastKey = key;
12094
- restrictDiag.lastAttemptAt = isoLocal();
12095
- restrictDiag.lastSuccess = true;
12096
- restrictDiag.lastError = null;
12097
- safeModeRestrictions.set(key, {
12098
- signature,
12099
- deny: applied,
12100
- dispose
12101
- });
12102
- writeRestrictDiag(root);
12103
- }
12104
12267
  /** 条件可见的 IM 工具名(与 tools/im/channels.ts 的注册名一致) */
12105
12268
  const IM_BRIDGE_TOOL_NAME = "im-bridge";
12106
- /** agent key → restrict disposer(im-bridge 隐藏状态;key 同安全模式 = 会话 id) */
12107
- const imBridgeRestrictions = /* @__PURE__ */ new Map();
12108
- /**
12109
- * 同步 im-bridge 可见性(v1.31.0,S142 用户拍板 Q3「未配置则不可见」)。
12110
- *
12111
- * 判据 = 本会话 CCC 是否启用了**任一** IM 通道(`hasEnabledImChannel`,当前只有 weixin):
12112
- * - 未启用 → `agent.ctx.tools.restrict({ deny: ['im-bridge'] })`:**从 schema 移除**
12113
- * (模型看不到,而不是看得到但调用被拒——用户原话"不配置则不可见")
12114
- * - 启用 → 解除隐藏(CCC 配置热更新即时生效)
12115
- *
12116
- * 与 `syncSafeModeRestriction` 同点调用(pre-step 每步 + 会话就绪),失败不阻断
12117
- * (守卫仍兜底;restrict 不可用时最坏是"看得到但通道未配置 → 返回 CHANNEL_NOT_CONFIGURED")。
12118
- */
12119
- function syncImBridgeVisibility(agent, root) {
12120
- const key = agent.session.id ?? "global";
12121
- let enabled = false;
12122
- try {
12123
- enabled = hasEnabledImChannel(root);
12124
- } catch (e) {
12125
- console.error(`[serenity-hooks] im-bridge 可见性判据失败 (key=${key}):`, e.message);
12126
- return;
12127
- }
12128
- const existing = imBridgeRestrictions.get(key);
12129
- if (!enabled && !existing) try {
12130
- const dispose = agent.ctx.tools.restrict({ deny: [IM_BRIDGE_TOOL_NAME] });
12131
- imBridgeRestrictions.set(key, dispose);
12132
- } catch (e) {
12133
- console.error(`[serenity-hooks] im-bridge 隐藏失败 (key=${key}):`, e.message);
12134
- }
12135
- else if (enabled && existing) {
12136
- try {
12137
- existing();
12138
- } catch (e) {
12139
- console.error(`[serenity-hooks] im-bridge 解除隐藏失败 (key=${key}):`, e.message);
12140
- }
12141
- imBridgeRestrictions.delete(key);
12142
- }
12143
- }
12144
- /** 会话销毁清理(lifecycle 调用;防 per-会话 Map 无界增长——review F-08 同族) */
12145
- function forgetImBridgeVisibility(sessionId) {
12146
- const existing = imBridgeRestrictions.get(sessionId);
12147
- if (!existing) return;
12148
- try {
12149
- existing();
12150
- } catch {}
12151
- imBridgeRestrictions.delete(sessionId);
12152
- }
12153
12269
  /**
12154
- * 宿主 `dsh-tool-schedule` 注册的 4 个模型可见工具名(**逐字**,来自该包 `lib/index.js` 的
12155
- * `defineTool({ name: … })`:`schedule_create` / `schedule_list` / `schedule_update` / `schedule_delete`)。
12270
+ * 宿主 `dsh-tool-schedule` 注册的 4 个模型可见工具名(**逐字**)。
12156
12271
  *
12157
- * 🔴 **为什么要在 CCC 内屏蔽(owner 原话:「这和 CRO 冲突,要求在宁静号激活情况下屏蔽掉」)**:
12158
- * 宿主从 `0.2.1-rc.2` 起自带一套**模型可见的定时器工具**(`ctx.schedule` 服务 + 这 4 个工具)。
12159
- * 而宁静号的唤醒/巡航走的是**另一套**机制:`cognitive_trajectory send-later` → 落 CCC 的
12160
- * `AGENT_SESSIONS/wake-registry.json` → 由**中心调度器 5min tick 统一投递**(D58)。
12161
- * 两套并存会产生**三个具体问题**(不是"重复功能"这种审美问题):
12162
- * ① **两处真相源**:宿主的 schedule 状态在宿主域、我们的在 CCC 文件里,**谁在跑说不清**;
12163
- * ② **CRO 判据失真**:CRO 程序读的是 `wake-registry.json`,宿主定时器触发的唤起**它看不见**
12164
- * ⇒ 巡检/记账全部遗漏(本轨已有同族教训:「CRO 只在 `wake===true` 时算」那条被推翻);
12165
- * ③ **安静失败**:模型可能顺手用 `schedule_create` 排一个唤醒,**它不在任何人的账本里**。
12166
- * ⇒ 故**在 CCC 内隐藏**(从模型清单移除),非 CCC 会话**不受影响**(那是宿主的正常能力)。
12167
- *
12168
- * ⚠️ **一处必须防的坑**(与 `hideAllFor` 同族):宿主 `restrict` 对**未注册的名字**会让**整次调用失败**
12169
- * (`dsh-tools` 契约逐字:"Empty filters, unknown names … fail")。
12170
- * 这 4 个名字是否在册取决于**宿主是否装了该 tool 包**(不同宿主版本/裁剪可能没有)
12171
- * ⇒ 所以**逐个名字单独发 restrict**,某个失败就跳过那个,**绝不整批发**
12172
- * (整批发一旦撞到缺名的宿主 ⇒ 一个都不隐藏 + 只有一行 log ⇒ 缺陷静默)。
12272
+ * 🔴 **为什么要在 CCC 内屏蔽**(owner 原话:「这和 CRO 冲突,要求在宁静号激活情况下屏蔽掉」):
12273
+ * 宁静号的唤醒/巡航走 `cognitive_trajectory send-later` → `AGENT_SESSIONS/wake-registry.json`
12274
+ * → 中心调度器 5min tick(D58)。宿主这套定时器是**另一个**真相源 ⇒ 两处记账、
12275
+ * CRO 判据失真、且模型可能顺手排一个"不在任何账本里"的唤醒。
12276
+ * ⚠️ 本清单**合并发射时不做探活**。⚠️ 现状缺口(v1.87.1 如实登记):宿主**没装**
12277
+ * `dsh-tool-schedule` 时,这 4 个名字会把整次 restrict 拒掉,而退档只保留 ACC 自己的名字
12278
+ * ⇒ 这一格的屏蔽**不生效**(旧实现同样不生效,**未回归**;但"顺带修好了"这句是假的,已删)。
12173
12279
  */
12174
12280
  const HOST_SCHEDULE_TOOL_NAMES = [
12175
12281
  "schedule_create",
@@ -12177,42 +12283,6 @@ const HOST_SCHEDULE_TOOL_NAMES = [
12177
12283
  "schedule_update",
12178
12284
  "schedule_delete"
12179
12285
  ];
12180
- /** `${sessionId}::${toolName}` → restrict disposer(逐工具,便于某个名字不成立时单独跳过) */
12181
- const hostScheduleRestrictions = /* @__PURE__ */ new Map();
12182
- /**
12183
- * 在 **CCC 内**屏蔽宿主自带的定时器工具(v1.59.5)。
12184
- *
12185
- * 判据 = **会话是否落在某个 CCC 内**(`root !== null`)——与 `syncCccToolVisibility` 互补:
12186
- * 那个管"不在 CCC ⇒ 隐藏**我们自己的**全部工具",本函数管"**在** CCC ⇒ 隐藏**宿主的**定时器工具"。
12187
- *
12188
- * 幂等;失败不阻断 step(最坏 = 那一个工具仍可见,但那只是"宿主的正常能力没被藏起来",
12189
- * 不会让会话坏掉)。逐个名字独立发 restrict ⇒ 任何一个名字不在册都不会连累其余三个。
12190
- *
12191
- * @param agent 目标会话
12192
- * @param root 该会话的 CCC 根;`null` ⇒ 非 CCC ⇒ **不动**(宿主的定时器在别处是正常能力)
12193
- */
12194
- function syncHostScheduleToolHiding(agent, root) {
12195
- if (root === null) return;
12196
- const sessionId = agent.session.id ?? "global";
12197
- for (const tool of HOST_SCHEDULE_TOOL_NAMES) {
12198
- const key = `${sessionId}::${tool}`;
12199
- if (hostScheduleRestrictions.has(key)) continue;
12200
- try {
12201
- hostScheduleRestrictions.set(key, agent.ctx.tools.restrict({ deny: [tool] }));
12202
- } catch {}
12203
- }
12204
- }
12205
- /** 会话销毁清理(lifecycle 调用;按 `${sessionId}::` 前缀回收) */
12206
- function forgetHostScheduleToolHiding(sessionId) {
12207
- const prefix = `${sessionId}::`;
12208
- for (const [key, dispose] of [...hostScheduleRestrictions]) {
12209
- if (!key.startsWith(prefix)) continue;
12210
- try {
12211
- dispose();
12212
- } catch {}
12213
- hostScheduleRestrictions.delete(key);
12214
- }
12215
- }
12216
12286
  /**
12217
12287
  * ACC 注册但**默认对所有 CCC 隐藏**的专属工具清单。
12218
12288
  *
@@ -12222,156 +12292,6 @@ function forgetHostScheduleToolHiding(sessionId) {
12222
12292
  * 本族是"被点名才可见"(能力归属 ACC 负责人)。
12223
12293
  */
12224
12294
  const EXCLUSIVE_TOOL_NAMES = ["acc-diag"];
12225
- /** `${sessionId}::${toolName}` → restrict disposer */
12226
- const exclusiveRestrictions = /* @__PURE__ */ new Map();
12227
- /**
12228
- * `sessionId` → { signature, dispose }(角色工具面 restrict 状态)
12229
- *
12230
- * `signature` = 本轮实际发出的 deny 清单(排序后 join)。用它做**幂等判据**:
12231
- * 每次 pre-step 都重算白名单,只有**算出来的结果变了**才重新发 restrict
12232
- * —— 配置不变就不重复发(restrict 会重建 schema,没必要每步都做)。
12233
- */
12234
- const trajectoryConstraintRestrictions = /* @__PURE__ */ new Map();
12235
- /**
12236
- * 同步 **轨迹约束会话的工具面**:把白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
12237
- *
12238
- * ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
12239
- *
12240
- * 在它之前,轨迹约束白名单**只由运行时守卫拦截**(`decideGuard` 的 轨迹约束 分支):
12241
- * 工具全部出现在模型 schema 里,调用时才被打回。⇒ 两个真实后果:
12242
- * ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
12243
- * 看不出"是不在清单里"还是"临时性拒绝");
12244
- * ② **配置变动在 schema 上永远不生效** —— 改 `trajectory-constraint.json` 的 `tools` 只影响"调用是否放行"。
12245
- *
12246
- * ## 判据与降级
12247
- *
12248
- * - deny 集合 = **`REGISTERED_TOOLS` − 白名单**(只对**我们自己注册的**工具算,
12249
- * 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
12250
- * - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
12251
- * 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
12252
- * - 读不到约束面(不是 轨迹约束 / 配置删了)⇒ **撤销既有隐藏**(回到"无约束"):
12253
- * ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是 轨迹约束的会话上
12254
- * (这正是 owner 说的"删掉 `trajectory-constraint.json` 就该立刻恢复")。
12255
- * - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
12256
- *
12257
- * @param agent 目标会话
12258
- * @param root CCC 根
12259
- */
12260
- function syncTrajectoryConstraintToolVisibility(agent, root) {
12261
- const sessionId = agent.session.id ?? "global";
12262
- const profile = trajectoryConstraintForSession(root, sessionId);
12263
- const existing = trajectoryConstraintRestrictions.get(sessionId);
12264
- if (profile === null) {
12265
- if (existing !== void 0) {
12266
- try {
12267
- existing.dispose();
12268
- } catch {}
12269
- trajectoryConstraintRestrictions.delete(sessionId);
12270
- }
12271
- return;
12272
- }
12273
- const allowed = profileToolWhitelist(profile);
12274
- if (allowed === null) {
12275
- if (existing !== void 0) {
12276
- try {
12277
- existing.dispose();
12278
- } catch {}
12279
- trajectoryConstraintRestrictions.delete(sessionId);
12280
- }
12281
- return;
12282
- }
12283
- const resident = REGISTERED_TOOLS.slice(0, REGISTERED_TOOLS.length - 3);
12284
- const tiers = [REGISTERED_TOOLS, resident];
12285
- for (const names of tiers) {
12286
- const deny = names.filter((n) => !allowed.has(n));
12287
- const signature = [...deny].sort().join(",");
12288
- if (existing !== void 0 && existing.signature === signature) return;
12289
- if (deny.length === 0) {
12290
- if (existing !== void 0) {
12291
- try {
12292
- existing.dispose();
12293
- } catch {}
12294
- trajectoryConstraintRestrictions.delete(sessionId);
12295
- }
12296
- return;
12297
- }
12298
- try {
12299
- if (existing !== void 0) {
12300
- try {
12301
- existing.dispose();
12302
- } catch {}
12303
- trajectoryConstraintRestrictions.delete(sessionId);
12304
- }
12305
- const dispose = agent.ctx.tools.restrict({ deny: [...deny] });
12306
- trajectoryConstraintRestrictions.set(sessionId, {
12307
- signature,
12308
- deny,
12309
- dispose
12310
- });
12311
- return;
12312
- } catch {
12313
- continue;
12314
- }
12315
- }
12316
- }
12317
- /** 会话销毁清理(lifecycle 调用) */
12318
- function forgetTrajectoryConstraintToolVisibility(sessionId) {
12319
- const existing = trajectoryConstraintRestrictions.get(sessionId);
12320
- if (!existing) return;
12321
- try {
12322
- existing.dispose();
12323
- } catch {}
12324
- trajectoryConstraintRestrictions.delete(sessionId);
12325
- }
12326
- /**
12327
- * 同步专属工具可见性(v1.33)。
12328
- *
12329
- * 判据 = CCC 配置 `exclusiveTools`(`readExclusiveTools`):
12330
- * - **未声明** → `agent.ctx.tools.restrict({ deny: [tool] })`:从 schema 移除(模型看不到)
12331
- * - **声明** → 解除隐藏(CCC 配置热更新即时生效)
12332
- *
12333
- * 判据失败按"未声明"处理(fail-closed:专属工具默认隐藏),原因见 `readExclusiveTools` 注释。
12334
- * 与 `syncImBridgeVisibility` 同点调用(pre-step 每步 + 会话就绪),失败不阻断一步——
12335
- * 最坏情况是"看得到但被拒",而不是让 step 出错。
12336
- */
12337
- function syncExclusiveToolsVisibility(agent, root) {
12338
- const sessionId = agent.session.id ?? "global";
12339
- let declared = [];
12340
- try {
12341
- declared = readExclusiveTools(root);
12342
- } catch (e) {
12343
- console.error(`[serenity-hooks] 专属工具可见性判据失败 (session=${sessionId}):`, e.message);
12344
- }
12345
- for (const tool of EXCLUSIVE_TOOL_NAMES) {
12346
- const key = `${sessionId}::${tool}`;
12347
- const existing = exclusiveRestrictions.get(key);
12348
- const visible = declared.includes(tool);
12349
- if (!visible && !existing) try {
12350
- exclusiveRestrictions.set(key, agent.ctx.tools.restrict({ deny: [tool] }));
12351
- } catch (e) {
12352
- console.error(`[serenity-hooks] 专属工具隐藏失败 (${key}):`, e.message);
12353
- }
12354
- else if (visible && existing) {
12355
- try {
12356
- existing();
12357
- } catch (e) {
12358
- console.error(`[serenity-hooks] 专属工具解除隐藏失败 (${key}):`, e.message);
12359
- }
12360
- exclusiveRestrictions.delete(key);
12361
- }
12362
- }
12363
- }
12364
- /** 会话销毁清理(lifecycle 调用;本函数按 `${sessionId}::` 前缀回收全部专属工具项) */
12365
- function forgetExclusiveToolsVisibility(sessionId) {
12366
- const prefix = `${sessionId}::`;
12367
- for (const [key, dispose] of [...exclusiveRestrictions]) {
12368
- if (!key.startsWith(prefix)) continue;
12369
- try {
12370
- dispose();
12371
- } catch {}
12372
- exclusiveRestrictions.delete(key);
12373
- }
12374
- }
12375
12295
  /**
12376
12296
  * 非 CCC 会话的工具隐藏状态(sessionId → restrict disposer)。
12377
12297
  *
@@ -12395,74 +12315,6 @@ function forgetExclusiveToolsVisibility(sessionId) {
12395
12315
  * **静默退回"工具照样可见"**=缺陷没修好。故此处走**两档尝试**(见 `hideAllFor`)。
12396
12316
  */
12397
12317
  const nonCccRestrictions = /* @__PURE__ */ new Map();
12398
- /**
12399
- * 在非 CCC 会话上隐藏全部 ACC 工具(两档尝试)。
12400
- *
12401
- * 🔴 **为什么要两档(D119:不许照源码"意图"断言宿主行为)**:取消注册的工具名会让整次
12402
- * `restrict` 失败,而 `diagram` 缺省不在册。若只发一次全表 deny,"deny 里有个不在册的名字"
12403
- * 就会让**整次调用**失败 ⇒ 一个都不隐藏(且只有一行 console.error)。
12404
- *
12405
- * 档位(**先全覆盖、失败再退到"恒在册"那一段**):
12406
- * ① 全表 `REGISTERED_TOOLS`(14:含条件工具)—— 首选,覆盖最全
12407
- * ② 常驻段(前 `length - CONDITIONAL_TOOL_COUNT` 个)—— 退路:只保证**恒注册**的那些被隐藏
12408
- *
12409
- * 返回**实际生效的注销器**;两档都失败 → `{ failed: [...] }`(原因一并带回,由调用方决定怎么响)。
12410
- *
12411
- * ⚠️ **档位失败不在这里逐条告警**:档 ① 在 `diagram` 缺省关时**按设计**就会失败(它不在册),
12412
- * 那是**常态**、不是故障 ⇒ 逐条 error 会在每个非 CCC 会话上刷**假警报**(看着像坏了,其实档 ② 已成功)。
12413
- * 真正的失败只由调用方在"两档全失败"时**响亮报一次**。
12414
- */
12415
- function hideAllFor(agent) {
12416
- const resident = REGISTERED_TOOLS.slice(0, REGISTERED_TOOLS.length - 3);
12417
- const tiers = [{
12418
- label: "全表(含条件工具)",
12419
- names: REGISTERED_TOOLS
12420
- }, {
12421
- label: "常驻段(跳过条件工具)",
12422
- names: resident
12423
- }];
12424
- const failed = [];
12425
- for (const tier of tiers) try {
12426
- return {
12427
- dispose: agent.ctx.tools.restrict({ deny: [...tier.names] }),
12428
- names: tier.names
12429
- };
12430
- } catch (e) {
12431
- failed.push(`${tier.label}: ${e.message}`);
12432
- }
12433
- return { failed };
12434
- }
12435
- /**
12436
- * 同步「本会话在不在 CCC 内」的工具可见性(v1.56.6)。
12437
- *
12438
- * @param agent 目标会话的 agent
12439
- * @param root 该会话的 CCC 根;`null` ⇒ **不在任何 CCC 内** ⇒ 隐藏全部 ACC 工具
12440
- *
12441
- * 幂等:同一会话重复调用不会重复 restrict(已是目标态即直接返回)。
12442
- * 失败不抛(最坏情况:工具可见,但**调用时**仍被各工具自己的根解析拒绝——既有兜底不变)。
12443
- */
12444
- function syncCccToolVisibility(agent, root) {
12445
- const sessionId = agent.session.id ?? "global";
12446
- const existing = nonCccRestrictions.get(sessionId);
12447
- const shouldHide = root === null;
12448
- if (shouldHide && !existing) {
12449
- const hidden = hideAllFor(agent);
12450
- if ("failed" in hidden) {
12451
- console.error(`[serenity-hooks] 非 CCC 工具隐藏**两档全失败**(session=${sessionId}) —— 工具仍可见(但有各工具自身的 !root 兜底):${hidden.failed.join(" / ")}`);
12452
- return;
12453
- }
12454
- nonCccRestrictions.set(sessionId, hidden.dispose);
12455
- return;
12456
- }
12457
- if (!shouldHide && existing) {
12458
- try {
12459
- existing();
12460
- } catch (e) {
12461
- console.error(`[serenity-hooks] 非 CCC 工具恢复失败 (session=${sessionId}):`, e.message);
12462
- }
12463
- nonCccRestrictions.delete(sessionId);
12464
- }
12465
- }
12466
12318
  /** 会话销毁清理(lifecycle 调用;防 per-会话 Map 无界增长) */
12467
12319
  function forgetNonCccToolVisibility(sessionId) {
12468
12320
  const existing = nonCccRestrictions.get(sessionId);
@@ -13764,7 +13616,7 @@ function registerContext(ctx, opts = {}) {
13764
13616
  const cwd = agent.session?.header?.cwd ?? process.cwd();
13765
13617
  const root = cccRootForCwd(cwd);
13766
13618
  try {
13767
- syncCccToolVisibility(agent, root);
13619
+ syncToolVisibility(agent, root);
13768
13620
  } catch {}
13769
13621
  if (!root) return;
13770
13622
  const key = agentKey(agent);
@@ -13832,15 +13684,8 @@ function registerContext(ctx, opts = {}) {
13832
13684
  }
13833
13685
  injected.add(key);
13834
13686
  agent.inject(accMessage(root, configPaths, entrySkillMaxChars));
13835
- syncSafeModeRestriction(agent, root);
13836
- try {
13837
- syncImBridgeVisibility(agent, root);
13838
- } catch {}
13839
- try {
13840
- syncExclusiveToolsVisibility(agent, root);
13841
- } catch {}
13842
13687
  try {
13843
- syncHostScheduleToolHiding(agent, root);
13688
+ syncToolVisibility(agent, root);
13844
13689
  } catch {}
13845
13690
  try {
13846
13691
  syncTrajectoryModelForSession(agent, root);
@@ -13857,31 +13702,14 @@ function registerContext(ctx, opts = {}) {
13857
13702
  const root = cccRootForCwd(cwd);
13858
13703
  const key = agentKey(agent);
13859
13704
  const downstream = await next();
13860
- if (root) {
13861
- try {
13862
- syncTrajectoryConstraintToolVisibility(agent, root);
13863
- } catch {}
13864
- try {
13865
- syncTrajectoryModelForSession(agent, root);
13866
- } catch {}
13867
- }
13868
- if (root && hasTrajectoryOwnCharter(root, key)) return downstream;
13869
13705
  try {
13870
- syncCccToolVisibility(agent, root);
13706
+ syncToolVisibility(agent, root);
13707
+ } catch {}
13708
+ if (root) try {
13709
+ syncTrajectoryModelForSession(agent, root);
13871
13710
  } catch {}
13711
+ if (root && hasTrajectoryOwnCharter(root, key)) return downstream;
13872
13712
  if (root) {
13873
- try {
13874
- syncSafeModeRestriction(agent, root);
13875
- } catch {}
13876
- try {
13877
- syncImBridgeVisibility(agent, root);
13878
- } catch {}
13879
- try {
13880
- syncExclusiveToolsVisibility(agent, root);
13881
- } catch {}
13882
- try {
13883
- syncHostScheduleToolHiding(agent, root);
13884
- } catch {}
13885
13713
  registerEntrySkillSection(agent, root);
13886
13714
  registerCognitiveTrajectorySkillSection(agent, root);
13887
13715
  }
@@ -14478,11 +14306,11 @@ function registerStatusApi(ctx, opts = {}) {
14478
14306
  sendJson$2(res, 400, { error: chk.error });
14479
14307
  return;
14480
14308
  }
14481
- const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
14309
+ const { readWeixinSettings } = await import("./route-3OzOAjec.js").then((n) => n.l);
14482
14310
  const { weixinBridgeStatus } = await Promise.resolve().then(() => bridge_exports);
14483
14311
  const settings = readWeixinSettings(chk.root);
14484
14312
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
14485
- const { imCredentialBound } = await import("./route-GtYJdR06.js").then((n) => n.c);
14313
+ const { imCredentialBound } = await import("./route-3OzOAjec.js").then((n) => n.l);
14486
14314
  const { normalizeAccountChannel } = await import("./accounts-BHQbakUh.js").then((n) => n.t);
14487
14315
  const accounts = (settings.accounts ?? []).map((a) => {
14488
14316
  const channel = normalizeAccountChannel(a.channel);
@@ -14519,7 +14347,7 @@ function registerStatusApi(ctx, opts = {}) {
14519
14347
  const { fetchQRCode } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
14520
14348
  const { randomUUID } = await import("node:crypto");
14521
14349
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
14522
- const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
14350
+ const { readWeixinSettings } = await import("./route-3OzOAjec.js").then((n) => n.l);
14523
14351
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
14524
14352
  const loginKey = randomUUID();
14525
14353
  weixinLogins.set(loginKey, {
@@ -14540,7 +14368,7 @@ function registerStatusApi(ctx, opts = {}) {
14540
14368
  sendJson$2(res, 400, { error: "missing accountId" });
14541
14369
  return;
14542
14370
  }
14543
- const { removeWeixinAccount } = await import("./route-GtYJdR06.js").then((n) => n.c);
14371
+ const { removeWeixinAccount } = await import("./route-3OzOAjec.js").then((n) => n.l);
14544
14372
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14545
14373
  removeWeixinAccount(root, body.accountId);
14546
14374
  syncCccBridge(ctx, root);
@@ -14566,7 +14394,7 @@ function registerStatusApi(ctx, opts = {}) {
14566
14394
  trajectory: code
14567
14395
  });
14568
14396
  }
14569
- const { saveWeixinRoutes } = await import("./route-GtYJdR06.js").then((n) => n.c);
14397
+ const { saveWeixinRoutes } = await import("./route-3OzOAjec.js").then((n) => n.l);
14570
14398
  const { findDirNameByCode } = await import("./profile-vHheCJiU.js").then((n) => n.a);
14571
14399
  for (const r of routes) if (findDirNameByCode(root, r.trajectory) === null) {
14572
14400
  sendJson$2(res, 400, { error: `unknown trajectory: ${r.trajectory} (no such trajectory code in ${root})` });
@@ -14578,7 +14406,7 @@ function registerStatusApi(ctx, opts = {}) {
14578
14406
  }
14579
14407
  if (body.action === "set-enabled") {
14580
14408
  const enabled = body.enabled === true;
14581
- const { readWeixinSettings, setWeixinEnabled } = await import("./route-GtYJdR06.js").then((n) => n.c);
14409
+ const { readWeixinSettings, setWeixinEnabled } = await import("./route-3OzOAjec.js").then((n) => n.l);
14582
14410
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14583
14411
  const settings = readWeixinSettings(root);
14584
14412
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -14601,7 +14429,7 @@ function registerStatusApi(ctx, opts = {}) {
14601
14429
  sendJson$2(res, 400, { error: "appId 与 appSecret 都必须填(只填一个等于没绑)" });
14602
14430
  return;
14603
14431
  }
14604
- const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
14432
+ const { readWeixinSettings, upsertWeixinAccount, writeFeishuCredential, nextFeishuAccountId } = await import("./route-3OzOAjec.js").then((n) => n.l);
14605
14433
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14606
14434
  const before = readWeixinSettings(root);
14607
14435
  const accountId = (body.accountId ?? "").trim() !== "" ? body.accountId.trim() : nextFeishuAccountId(before);
@@ -14651,7 +14479,7 @@ function registerStatusApi(ctx, opts = {}) {
14651
14479
  return;
14652
14480
  }
14653
14481
  const { pollQRStatus } = await import("./ilink-BMk2w2KY.js").then((n) => n.a);
14654
- const { readWeixinSettings } = await import("./route-GtYJdR06.js").then((n) => n.c);
14482
+ const { readWeixinSettings } = await import("./route-3OzOAjec.js").then((n) => n.l);
14655
14483
  const settings = readWeixinSettings(login.root);
14656
14484
  const status = await pollQRStatus({
14657
14485
  baseUrl: void 0,
@@ -14666,7 +14494,7 @@ function registerStatusApi(ctx, opts = {}) {
14666
14494
  });
14667
14495
  return;
14668
14496
  }
14669
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-GtYJdR06.js").then((n) => n.c);
14497
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./route-3OzOAjec.js").then((n) => n.l);
14670
14498
  const { syncCccBridge } = await Promise.resolve().then(() => bridge_exports);
14671
14499
  const accountId = nextWeixinAccountId(settings);
14672
14500
  upsertWeixinAccount(login.root, {
@@ -21152,20 +20980,11 @@ function cleanupSessionState(sessionId) {
21152
20980
  forgetManualOutputSession(sessionId);
21153
20981
  } catch {}
21154
20982
  try {
21155
- forgetImBridgeVisibility(sessionId);
21156
- } catch {}
21157
- try {
21158
- forgetExclusiveToolsVisibility(sessionId);
20983
+ forgetToolVisibility(sessionId);
21159
20984
  } catch {}
21160
20985
  try {
21161
20986
  forgetNonCccToolVisibility(sessionId);
21162
20987
  } catch {}
21163
- try {
21164
- forgetHostScheduleToolHiding(sessionId);
21165
- } catch {}
21166
- try {
21167
- forgetTrajectoryConstraintToolVisibility(sessionId);
21168
- } catch {}
21169
20988
  try {
21170
20989
  forgetLogbookCompactionState(sessionId);
21171
20990
  } catch {}
@@ -423,4 +423,4 @@ function setWeixinEnabled(root, enabled) {
423
423
  return next;
424
424
  }
425
425
  //#endregion
426
- export { readFeishuCredential as a, route_exports as c, weixinSessionIdForTrajectory as d, matchWeixinRoute as i, sanitizeFileName as l, extractWeixinText as n, readWeixinCredential as o, hasVoiceItem as r, readWeixinSettings as s, extractWeixinMedia as t, weixinInboundDir as u };
426
+ export { readFeishuCredential as a, readWeixinSettings as c, weixinInboundDir as d, weixinSessionIdForTrajectory as f, matchWeixinRoute as i, route_exports as l, extractWeixinText as n, readImSettings as o, hasVoiceItem as r, readWeixinCredential as s, extractWeixinMedia as t, sanitizeFileName as u };
@@ -10,6 +10,7 @@ import type { Context } from 'cordis';
10
10
  import type { Agent } from '@deepseek-ai/dsh-agent';
11
11
  import { type BlacklistRule } from '../kernel/ccc.js';
12
12
  import { type TrajectoryConstraint } from '../trajectory/policy/profile.js';
13
+ import { type VisibilitySource } from './tool-visibility.js';
13
14
  export interface GuardInput {
14
15
  root: string;
15
16
  toolName: string;
@@ -39,6 +40,39 @@ interface GuardDecisionResult {
39
40
  * 优先级:safe-mode bash > 路径越界 > 黑名单命中。
40
41
  */
41
42
  export declare function decideGuard(input: GuardInput): GuardDecisionResult;
43
+ /**
44
+ * 🔴 **工具可见性的唯一发射入口**(方案 C 的落地)。
45
+ *
46
+ * ## 它取代了什么
47
+ *
48
+ * 改之前有六条独立 sync,每条在自己的 per-会话 Map 里放一个 `restrict` disposer,
49
+ * 且都由 `pre-step`(每步)与 `seed`(冷载入)各自调用 ⇒ 一次交互里可能发出多次
50
+ * `tools.restrict` ⇒ 宿主每次发 `tools/change` ⇒ 模型侧看到工具清单**先进后出**
51
+ * (实测症状:`新增 7 个` 紧跟 `移除 7 个`)。
52
+ *
53
+ * ## 为什么合并是对的(不是简化)
54
+ *
55
+ * 宿主 `ToolRuntime.restrict` 契约逐字:**Restrictions intersect**。
56
+ * ⇒ N 次 `deny: Dᵢ` 的净效果 = `⋃Dᵢ`。先求并集只发一次,**结果相同**、只触发一次 `tools/change`。
57
+ * 前提 = **只用 `deny`**(`allow` 的"交集"不满足并集律);本仓 18 处 restrict 全部只发 `deny`。
58
+ *
59
+ * ## 正确性怎么保住(这是本设计被质疑的那一点)
60
+ *
61
+ * 六源**各自独立**算自己的 deny 并**各自 catch**:某一个来源读配置抛错 ⇒ 它返回
62
+ * `ok:false`(不贡献 deny),**其余五源照常生效**。⇒ 合并**不会**把一格的失败放大成六格。
63
+ * 全量计算见 {@link computeVisibilitySources}(纯函数)。
64
+ *
65
+ * @param agent 目标会话
66
+ * @param root CCC 根;`null` ⇒ 不在 CCC 内 ⇒ 隐藏**全部** ACC 工具
67
+ */
68
+ export declare function syncToolVisibility(agent: Agent, root: string | null): void;
69
+ /**
70
+ * 六源 → 各自的 deny 集合(**纯函数:只读配置、不碰宿主**)。
71
+ *
72
+ * 每条来源**自己负责自己的失败**:抛错就 catch 在这里,转成 `ok:false + error`。
73
+ * ⛔ 绝不让一个来源的异常冒泡 —— 那会让另外五源一起失效(正是合并最容易犯的错)。
74
+ */
75
+ export declare function computeVisibilitySources(sessionId: string, root: string | null): VisibilitySource[];
42
76
  /** restrict 诊断状态(status API 暴露;排查 restrict 未生效问题) */
43
77
  interface RestrictDiagnostics {
44
78
  lastKey: string | null;
@@ -51,53 +85,28 @@ interface RestrictDiagnostics {
51
85
  }
52
86
  export declare function getRestrictDiagnostics(): RestrictDiagnostics;
53
87
  /**
54
- * 同步安全模式工具隐藏:标记存在 → `agent.ctx.tools.restrict deny` 隐藏
55
- * **基础表(bash)∪ CCC 声明的 `safeMode.hiddenTools`**;标记消失 → 全部解除。
56
- * pre-step 每步调用 ⇒ 开/关/**改声明**三种变化都实时生效(v1.51.4 起含"改声明")。
88
+ * im-bridge 隐藏诊断(排查"工具为什么不在清单里")。
57
89
  *
58
- * 与 `readSafeModeHiddenTools` 的分工(R↓):本函数负责**形状与失败退化**,reader 只管取值。
59
- * 🔴 退化规则:DSH 契约规定 **未注册的工具名会让整次 restrict 失败**("unknown names … fail")
60
- * ⇒ 绝不能让 CCC 写错一个名字就丢掉 safe-mode 的核心隐藏(bash)。失败时退化为**只隐藏基础表**,
61
- * 并在日志响亮报错;退化态**按期望签名记账**,不会每步重试刷屏。
90
+ * 🔴 方案 C 之后**唯一真相源**是 `seams/tool-visibility.ts` 的全局表。
91
+ * 本函数只**读**它并回答"该会话当前是否隐藏了 im-bridge" ——
92
+ * 不再自己维护第二份状态(那会让诊断与实现各说一套,D164 同族)。
62
93
  */
63
- export declare function syncSafeModeRestriction(agent: Agent, root: string): void;
64
- /** im-bridge 隐藏诊断(排查"工具为什么不在清单里") */
65
94
  export declare function getImBridgeVisibilityDiagnostics(): {
66
95
  hiddenKeys: string[];
67
96
  };
68
97
  /**
69
- * 同步 im-bridge 可见性(v1.31.0,S142 用户拍板 Q3「未配置则不可见」)。
70
- *
71
- * 判据 = 本会话 CCC 是否启用了**任一** IM 通道(`hasEnabledImChannel`,当前只有 weixin):
72
- * - 未启用 → `agent.ctx.tools.restrict({ deny: ['im-bridge'] })`:**从 schema 移除**
73
- * (模型看不到,而不是看得到但调用被拒——用户原话"不配置则不可见")
74
- * - 启用 → 解除隐藏(CCC 配置热更新即时生效)
98
+ * 会话销毁清理(lifecycle 调用)。
75
99
  *
76
- * 与 `syncSafeModeRestriction` 同点调用(pre-step 每步 + 会话就绪),失败不阻断
77
- * (守卫仍兜底;restrict 不可用时最坏是"看得到但通道未配置 → 返回 CHANNEL_NOT_CONFIGURED")。
100
+ * ⚠️ 方案 C 后撤销动作**只在** `forgetToolVisibility` 里做一次(那一次合并 restrict)。
101
+ * 本函数保留为语义占位:重复撤销同一条 restrict 会抛错,故这里刻意不做动作。
78
102
  */
79
- export declare function syncImBridgeVisibility(agent: Agent, root: string): void;
80
- /** 会话销毁清理(lifecycle 调用;防 per-会话 Map 无界增长——review F-08 同族) */
81
- export declare function forgetImBridgeVisibility(sessionId: string): void;
103
+ export declare function forgetImBridgeVisibility(_sessionId: string): void;
82
104
  /** 诊断面:本会话已屏蔽的宿主定时器工具(排查"工具为什么不见了") */
83
105
  export declare function getHostScheduleHiddenDiagnostics(): {
84
106
  hidden: string[];
85
107
  };
86
- /**
87
- * 在 **CCC 内**屏蔽宿主自带的定时器工具(v1.59.5)。
88
- *
89
- * 判据 = **会话是否落在某个 CCC 内**(`root !== null`)——与 `syncCccToolVisibility` 互补:
90
- * 那个管"不在 CCC ⇒ 隐藏**我们自己的**全部工具",本函数管"**在** CCC ⇒ 隐藏**宿主的**定时器工具"。
91
- *
92
- * 幂等;失败不阻断 step(最坏 = 那一个工具仍可见,但那只是"宿主的正常能力没被藏起来",
93
- * 不会让会话坏掉)。逐个名字独立发 restrict ⇒ 任何一个名字不在册都不会连累其余三个。
94
- *
95
- * @param agent 目标会话
96
- * @param root 该会话的 CCC 根;`null` ⇒ 非 CCC ⇒ **不动**(宿主的定时器在别处是正常能力)
97
- */
98
- export declare function syncHostScheduleToolHiding(agent: Agent, root: string | null): void;
99
- /** 会话销毁清理(lifecycle 调用;按 `${sessionId}::` 前缀回收) */
100
- export declare function forgetHostScheduleToolHiding(sessionId: string): void;
108
+ /** 会话销毁清理(lifecycle 调用)——方案 C 后由统一清理撤销,这里保留语义占位。 */
109
+ export declare function forgetHostScheduleToolHiding(_sessionId: string): void;
101
110
  /** 专属工具隐藏诊断(排查"工具为什么不在清单里") */
102
111
  export declare function getExclusiveToolsDiagnostics(): {
103
112
  hidden: string[];
@@ -107,62 +116,17 @@ export declare function getTrajectoryConstraintVisibilityDiagnostics(): {
107
116
  keys: string[];
108
117
  denyByKey: Record<string, string[]>;
109
118
  };
110
- /**
111
- * 同步 **轨迹约束会话的工具面**:把白名单之外的工具**从 schema 移除**(不是"看得到但被拒")。
112
- *
113
- * ## 为什么必须有这条(owner 2026-10-19:「工具清单也要热加载」)
114
- *
115
- * 在它之前,轨迹约束白名单**只由运行时守卫拦截**(`decideGuard` 的 轨迹约束 分支):
116
- * 工具全部出现在模型 schema 里,调用时才被打回。⇒ 两个真实后果:
117
- * ① **模型看得见它不该有的工具** —— 它会去试,试了才撞墙(一轮白烧,且错误文案泛化,
118
- * 看不出"是不在清单里"还是"临时性拒绝");
119
- * ② **配置变动在 schema 上永远不生效** —— 改 `trajectory-constraint.json` 的 `tools` 只影响"调用是否放行"。
120
- *
121
- * ## 判据与降级
122
- *
123
- * - deny 集合 = **`REGISTERED_TOOLS` − 白名单**(只对**我们自己注册的**工具算,
124
- * 不去动宿主的工具 —— 宿主的工具由各自的条件同步管)。
125
- * - ⚠️ **未注册的名字会让整次 restrict 失败**(宿主契约逐字 "unknown names … fail"),
126
- * 而 `diagram` 是按开关注册的 ⇒ 与 `hideAllFor` 同款**两档尝试**(先全覆盖,失败退常驻段)。
127
- * - 读不到约束面(不是 轨迹约束 / 配置删了)⇒ **撤销既有隐藏**(回到"无约束"):
128
- * ⛔ 不能只是 `return` —— 那会把上一步的隐藏**永久留在**一个已不再是 轨迹约束的会话上
129
- * (这正是 owner 说的"删掉 `trajectory-constraint.json` 就该立刻恢复")。
130
- * - 幂等:算出来的 deny 签名不变就跳过(不每步重建 schema)。
131
- *
132
- * @param agent 目标会话
133
- * @param root CCC 根
134
- */
135
- export declare function syncTrajectoryConstraintToolVisibility(agent: Agent, root: string): void;
136
- /** 会话销毁清理(lifecycle 调用) */
137
- export declare function forgetTrajectoryConstraintToolVisibility(sessionId: string): void;
138
- /**
139
- * 同步专属工具可见性(v1.33)。
140
- *
141
- * 判据 = CCC 配置 `exclusiveTools`(`readExclusiveTools`):
142
- * - **未声明** → `agent.ctx.tools.restrict({ deny: [tool] })`:从 schema 移除(模型看不到)
143
- * - **声明** → 解除隐藏(CCC 配置热更新即时生效)
144
- *
145
- * 判据失败按"未声明"处理(fail-closed:专属工具默认隐藏),原因见 `readExclusiveTools` 注释。
146
- * 与 `syncImBridgeVisibility` 同点调用(pre-step 每步 + 会话就绪),失败不阻断一步——
147
- * 最坏情况是"看得到但被拒",而不是让 step 出错。
148
- */
149
- export declare function syncExclusiveToolsVisibility(agent: Agent, root: string): void;
150
- /** 会话销毁清理(lifecycle 调用;本函数按 `${sessionId}::` 前缀回收全部专属工具项) */
151
- export declare function forgetExclusiveToolsVisibility(sessionId: string): void;
119
+ /** 会话销毁清理(lifecycle 调用)。
120
+ * ⚠️ 方案 C 后撤销只在 `forgetToolVisibility` 做一次;本函数保留语义占位。
121
+ * 留着它的价值:lifecycle 的调用点表达"这条会话有轨迹约束可见性状态要清",
122
+ * 删掉调用点会让那件事**从代码里消失**(将来有人重加机制时找不到插点)。 */
123
+ export declare function forgetTrajectoryConstraintToolVisibility(_sessionId: string): void;
124
+ /** 会话销毁清理(lifecycle 调用)——同上,语义占位。 */
125
+ export declare function forgetExclusiveToolsVisibility(_sessionId: string): void;
152
126
  /** 非 CCC 隐藏诊断(排查"工具为什么不见了") */
153
127
  export declare function getNonCccHiddenDiagnostics(): {
154
128
  hidden: string[];
155
129
  };
156
- /**
157
- * 同步「本会话在不在 CCC 内」的工具可见性(v1.56.6)。
158
- *
159
- * @param agent 目标会话的 agent
160
- * @param root 该会话的 CCC 根;`null` ⇒ **不在任何 CCC 内** ⇒ 隐藏全部 ACC 工具
161
- *
162
- * 幂等:同一会话重复调用不会重复 restrict(已是目标态即直接返回)。
163
- * 失败不抛(最坏情况:工具可见,但**调用时**仍被各工具自己的根解析拒绝——既有兜底不变)。
164
- */
165
- export declare function syncCccToolVisibility(agent: Agent, root: string | null): void;
166
130
  /** 会话销毁清理(lifecycle 调用;防 per-会话 Map 无界增长) */
167
131
  export declare function forgetNonCccToolVisibility(sessionId: string): void;
168
132
  interface GuardRegistration {
@@ -0,0 +1,77 @@
1
+ /** 一次 restrict 的 deny 集合来源(每个来源**独立成败**,互不连累) */
2
+ export interface VisibilitySource {
3
+ /** 稳定标识(诊断与去重用;例:`safeMode` / `im-bridge` / `hostSchedule`) */
4
+ readonly id: string;
5
+ /** 本来源要隐藏的工具名(已去重前的数组;由收集器统一去重) */
6
+ readonly deny: readonly string[];
7
+ /**
8
+ * 本来源的判据是否成立。
9
+ * - `false` ⇒ 本来源**不贡献** deny(等价于它"没意见"),**不是**错误。
10
+ * - 读取配置抛错等情况由来源自己决定返回 `false` 还是抛(收集器会捕获并降级)。
11
+ */
12
+ readonly ok: boolean;
13
+ /** `ok=false` 且确实出了错(不是"判据不成立")时的一句话,用于诊断 */
14
+ readonly error?: string;
15
+ }
16
+ /** 一次可见性判决(纯数据,可 JSON 化,便于诊断与测试断言) */
17
+ export interface VisibilityDecision {
18
+ /** 合并后的 deny(已去重、已排序 —— 排序让签名稳定、可比较) */
19
+ readonly deny: string[];
20
+ /** 参与判决的来源(含失败的),用于诊断"谁把工具藏了" */
21
+ readonly sources: readonly VisibilitySource[];
22
+ /** 是否有任一来源报错(决定要不要打日志;**不影响** deny 的计算) */
23
+ readonly hasError: boolean;
24
+ }
25
+ /**
26
+ * 🔴 本模块的**准入契约**:可见性判决只允许用 `deny`。
27
+ *
28
+ * 宿主 `restrict` 同时支持 `allow` 与 `deny`,而**两者不可混用合并**:
29
+ * `allow: A₁` 与 `allow: A₂` 的交集是 `A₁ ∩ A₂`,**不是并集**。
30
+ * ⇒ 若将来某个来源想"只保留某几个工具",它**不能**塞进本模块,
31
+ * 必须自己单独发一条 restrict(那条会与本次的 deny 正常求交)。
32
+ * 该常量是这条契约的**机器可读声明**(测试据此钉住)。
33
+ */
34
+ export declare const TOOL_VISIBILITY_DENY_ONLY = true;
35
+ /** 收集器:把各来源的 deny 合并成一个判决(**纯函数、无副作用**) */
36
+ export declare function collectVisibilityDecision(sources: readonly VisibilitySource[]): VisibilityDecision;
37
+ /** 判决签名(幂等判据:签名相同 ⇒ 目标态已达到 ⇒ **一次 restrict 都不发**) */
38
+ export declare function decisionSignature(decision: VisibilityDecision): string;
39
+ /** 诊断:当前每个会话的可见性状态(排查"某工具为何不可见") */
40
+ export declare function getToolVisibilityDiagnostics(): {
41
+ keys: string[];
42
+ denyByKey: Record<string, string[]>;
43
+ };
44
+ /**
45
+ * 发射一次判决(**唯一的写点**)。
46
+ *
47
+ * 三条纪律:
48
+ * ① **幂等**:签名与当前已发射的一致 ⇒ 直接返回 `'unchanged'`,**不碰宿主**
49
+ * (这正是消除"工具清单闪动"的那一步)。
50
+ * ② **先撤后发**:宿主的 disposer 才是"撤销上一次",先撤再发才不会叠加旧集合。
51
+ * (旧集合是新集合的子集时叠不叠都看不出来,但**不是子集时**会残留 —— 必须撤。)
52
+ * ③ 🔴 **未注册的名字不许炸掉整次发射**:宿主契约"unknown names … fail"。
53
+ * 合并后集合里只要有一个不在册的名字,整次 restrict 就失败 ⇒ 六个机制**全都不生效**。
54
+ * ⇒ 策略 = **直接发 → 全量被拒才退到「只发 ACC 自己注册的那些」一档**,退档时
55
+ * 把被放弃的名字如实记账(进 `dropped`,由调用方决定要不要告警)。
56
+ * ⚠️ **不做 `tools.get` 预探活** —— 那个视图查不出 agent-plane 贡献的工具
57
+ * (`bash` / `schedule_*`),会把**在册的名字误判成未注册**(见下方发射段的实测记录)。
58
+ * ⚠️ 这比旧行为**更保守**:旧行为是"某条 restrict 失败 ⇒ 那一格静默不生效",
59
+ * 新行为是"全量失败 ⇒ 至少 ACC 那一格照常生效" —— **等价或更好**,且总有日志。
60
+ *
61
+ * @returns `'unchanged'` / `'applied'` / `'failed'`(`failed` 时 `deny` 保持原样)
62
+ */
63
+ export declare function applyVisibilityDecision(agent: {
64
+ ctx: {
65
+ tools: {
66
+ restrict(f: {
67
+ deny: string[];
68
+ }): () => void;
69
+ };
70
+ };
71
+ }, sessionId: string, decision: VisibilityDecision): {
72
+ status: 'unchanged' | 'applied' | 'failed';
73
+ dropped: string[];
74
+ error?: string;
75
+ };
76
+ /** 会话销毁清理(防 per-会话 Map 无界增长;lifecycle 调用) */
77
+ export declare function forgetToolVisibility(sessionId: string): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.86.0",
3
+ "version": "1.87.1",
4
4
  "description": "宁静号 ACC harness(Native Cordis 插件)——给 DeepSeek Harness 装一个「AI 工作区」:14 个工具(container_fs/cognitive_trajectory/dashboard/container_git/msm/praxis/ontology/serenity-skill-creator/cheap-worker/localstore/container_admin;另三个 im-bridge/acc-diag/diagram 按配置条件出现)+ 机械约束(安全模式/工作区围墙/密钥守卫/对外输出守卫)+ 认知轨迹日志与原地重建 + 网页登录入口/IM 桥/认知轨迹唤醒注册表。轨迹声明 = 轨迹目录下的 trajectory.json(约束面 + 旧标识 uid + CRO 机器绑定 cro.host;旧名 trajectory-constraint.json / skiff.json 仍在读取族里);轨迹提示词 = 轨迹目录下的 PROMPT.md(一条轨迹「是谁、按什么章程工作」,所有轨迹通用,追加在容器层与质量层之后,不替换它们)。适配 DSH 0.1.7-rc.1 / 0.2.0-rc.1 / 0.2.1-rc.1(三档)。",
5
5
  "license": "MIT",
6
6
  "repository": {