@sema-agent/server 7.15.0 → 7.16.0-rc.2

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.
Files changed (61) hide show
  1. package/USAGE.md +36 -2
  2. package/dist/approval-card.d.ts +65 -0
  3. package/dist/approval-card.js +54 -6
  4. package/dist/approval-reconciler.d.ts +17 -1
  5. package/dist/boot/resolve-spec.js +31 -4
  6. package/dist/boot/runner-deps.d.ts +41 -2
  7. package/dist/boot/runner-deps.js +43 -0
  8. package/dist/boot/session-faces.js +26 -4
  9. package/dist/boot/shutdown.js +17 -0
  10. package/dist/boot/stores.js +20 -0
  11. package/dist/config-center/apply-effective.d.ts +14 -0
  12. package/dist/config-center/apply-effective.js +81 -1
  13. package/dist/config-types.d.ts +28 -2
  14. package/dist/config.js +71 -37
  15. package/dist/fleet/fleet-terminal-window.d.ts +12 -0
  16. package/dist/fleet/fleet-terminal-window.js +27 -4
  17. package/dist/http/active-run-conflict.d.ts +41 -1
  18. package/dist/http/active-run-conflict.js +24 -10
  19. package/dist/http/routes/approvals-assistant.d.ts +11 -3
  20. package/dist/http/routes/approvals-assistant.js +97 -20
  21. package/dist/http/routes/capabilities.js +8 -1
  22. package/dist/http/routes/diagnostics.d.ts +18 -0
  23. package/dist/http/routes/diagnostics.js +26 -0
  24. package/dist/http/routes/runs.js +63 -17
  25. package/dist/http/routes/side-query.js +15 -1
  26. package/dist/http/routes/tasks.js +32 -3
  27. package/dist/http/server.js +67 -13
  28. package/dist/leader/fanout.d.ts +18 -0
  29. package/dist/leader/fanout.js +34 -1
  30. package/dist/leader/leader.js +9 -5
  31. package/dist/leader/wire.js +16 -5
  32. package/dist/main.js +1 -0
  33. package/dist/model-select.d.ts +43 -1
  34. package/dist/model-select.js +70 -2
  35. package/dist/observability/fail-open.d.ts +8 -0
  36. package/dist/observability/fail-open.js +8 -0
  37. package/dist/observability/metrics.js +3 -0
  38. package/dist/parent-watch.d.ts +57 -0
  39. package/dist/parent-watch.js +108 -0
  40. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  41. package/dist/plugins/checkpoint-store-sql.js +133 -7
  42. package/dist/plugins/local-checkpoint-store.js +11 -1
  43. package/dist/plugins/memory-embedder-fingerprint.d.ts +119 -0
  44. package/dist/plugins/memory-embedder-fingerprint.js +280 -0
  45. package/dist/plugins/permission-rule-store-sql.d.ts +0 -3
  46. package/dist/plugins/permission-rule-store-sql.js +1 -7
  47. package/dist/plugins/pg-pool.js +3 -0
  48. package/dist/plugins/store-backend.d.ts +5 -6
  49. package/dist/plugins/store-backend.js +4 -1
  50. package/dist/plugins/store-contracts.d.ts +17 -0
  51. package/dist/plugins/store-contracts.js +33 -0
  52. package/dist/plugins/tidb-pool.js +7 -0
  53. package/dist/plugins/tool-result-store-sql.d.ts +18 -13
  54. package/dist/plugins/tool-result-store-sql.js +50 -29
  55. package/dist/run-local.js +1 -0
  56. package/dist/tool-approval.d.ts +9 -0
  57. package/dist/tool-approval.js +74 -3
  58. package/dist/trace/project.js +8 -0
  59. package/dist/trace/redact.d.ts +14 -1
  60. package/dist/trace/redact.js +14 -2
  61. package/package.json +3 -3
@@ -0,0 +1,108 @@
1
+ /**
2
+ * #219(黑板 [3617]):引擎 **parent 监视**。
3
+ *
4
+ * ## 防的是什么
5
+ *
6
+ * 壳(cli/TUI/桌面)自 spawn 引擎的**同机形**里,壳被 SIGKILL(崩溃、`kill -9`、OOM killer)之后没有
7
+ * 任何清理钩子跑得起来:引擎被 reparent 到 init,**继续活着** —— 占端口、占库连接池、占模型配额,而且
8
+ * 再没有人会给它发 SIGTERM(T11 的"骑进程组"腿只覆盖信号送得到的形;SIGKILL 那一路本来就无钩)。
9
+ * 于是引擎给自己装一只**自查**腿:定期问一句"当年生我的那个 pid 还在吗",不在就**自己**走正常停机。
10
+ *
11
+ * ## 机制(逐条都是契约)
12
+ *
13
+ * 1. **opt-in**:`SEMA_PARENT_PID` 缺席 ⇒ 整件不装配(`boot/shutdown.ts` 连 `createParentWatch` 都不调)。
14
+ * 存量部署逐字零变化。坏值拒启在 `config.ts` 的 `parentPidEnv()`。
15
+ * 2. **探活 = `process.kill(pid, 0)`**:0 号信号不投递任何信号,只做"这个 pid 存在吗 + 我能不能给它
16
+ * 发信号"的检查。两种失败方向**语义相反**,写反了后果不对称:
17
+ * · `ESRCH` = 没有这个进程 ⇒ 父**不在**(这是我们要抓的);
18
+ * · `EPERM` = 进程**在**,只是本进程无权给它发信号(父跑在另一个 uid 下 —— systemd/su/容器里
19
+ * 很常见)⇒ **算活着**。把 EPERM 当"不在",每台这种机器上的引擎会在开机后两拍内自杀。
20
+ * 3. **连续两拍才判死**:单拍抖动(探活恰好撞上一次瞬时错误)不误杀。任何一拍看到"活着"就清零。
21
+ * 4. **判死后走 `drain`**,即 `boot/shutdown.ts` 里 SIGTERM 用的那一条 `drainThenShutdown` —— in-flight
22
+ * run 的结算 / park 语义与人工停机**逐字一致**。本模块自己**从不** `process.exit`。
23
+ * 5. **interval 必须 unref**:监视腿不得成为"进程本该退出却退不掉"的理由(与 drain tick 刻意不 unref
24
+ * 的取向相反 —— 那一只 tick 就是停机驱动,这一只只是观察者)。
25
+ *
26
+ * ## 已知残余(成文,现版不收窄)
27
+ *
28
+ * · **pid 复用**:两拍窗口(≤30s)内父 pid 被系统复用,探活看到的是新进程,监视会判"父还活着"而不自退。
29
+ * · **僵尸父进程**(codex 复审 H,本机 perl fork 实测证实):`kill(pid, 0)` 只证明 **pid 表项**在,不证明
30
+ * 进程还在跑 —— 父退出但**没被它自己的父进程 wait 回收**时留下 zombie(`ps STAT=Z`),表项还在,于是
31
+ * 每一拍都判"活着",监视永不触发。触发条件是壳的**上级**不回收子进程(容器里的朴素 PID 1 是典型),
32
+ * 终端 / launchd / systemd 起的壳不在此列。
33
+ *
34
+ * 两条都源于同一个根:`kill(pid, 0)` 是**弱身份**判据。收窄需要一个真身份锚(`process.ppid` 变化、
35
+ * starttime、pidfd、或壳侧持有型管道),那是**机制换代**而不是参数微调,归设计属主裁。本件在定稿里
36
+ * 就是**尽力自愈**、不是强一致的父子生命周期绑定 —— 要强一致,该由壳侧开一条持有型管道并在断开时收尸。
37
+ */
38
+ /** 探活拍频。**固定值,不开 env 旋钮**(设计定稿:有真需求再议,防旋钮增殖)。 */
39
+ export const PARENT_WATCH_INTERVAL_MS = 15_000;
40
+ /** 装配并**立刻**起监视(装配即 `parent_watch_armed`)。返回停机链用的 `stop()`。 */
41
+ export function createParentWatch(deps) {
42
+ const probe = deps.probe ?? ((pid) => void process.kill(pid, 0));
43
+ let firstMissAtMs;
44
+ let stopped = false;
45
+ let probeErrorReported = false;
46
+ deps.log("parent_watch_armed", {
47
+ pid: deps.pid,
48
+ intervalMs: PARENT_WATCH_INTERVAL_MS,
49
+ note: "engine watches this pid; TWO consecutive ESRCH probes trigger the same graceful drain as SIGTERM (EPERM = alive). Unset SEMA_PARENT_PID to disable.",
50
+ });
51
+ const timer = setInterval(() => {
52
+ if (stopped)
53
+ return;
54
+ if (deps.isStopped()) {
55
+ stop(); // 另一个停机属主已接管 —— 观察者退场,别把它的 drain 升级成硬停
56
+ return;
57
+ }
58
+ let alive = true;
59
+ try {
60
+ probe(deps.pid);
61
+ }
62
+ catch (err) {
63
+ // ⚠️ 方向:只有 ESRCH 才是"父不在"。EPERM(无权发信号)与任何其它 errno 都算**活着** ——
64
+ // 这一支的保守方向是"继续服务",因为误判的代价是把一台**正常**引擎连同它的 in-flight run 关掉。
65
+ const code = err?.code;
66
+ alive = code !== "ESRCH";
67
+ // 🔴 不静默(codex 复审 M):`kill(pid, 0)` 正常只给 ESRCH/EPERM 两个 errno,出现第三种意味着
68
+ // **探活这条腿自己坏了**(例:pid 越出 int32 ⇒ `ERR_INVALID_ARG_TYPE`),而它的落点恰是"判活",
69
+ // 于是监视会 armed 之后永久失效、零信号。判死方向不变(保守),但必须留下一条带 errno 的线索;
70
+ // 一次性 —— 每 15s 刷一行会把真信号淹掉。
71
+ if (code !== "ESRCH" && code !== "EPERM" && !probeErrorReported) {
72
+ probeErrorReported = true;
73
+ deps.log("parent_watch_probe_error", {
74
+ pid: deps.pid,
75
+ code: code ?? "unknown",
76
+ err: String(err),
77
+ note: "liveness probe failed with an errno that is neither ESRCH nor EPERM — treated as ALIVE (conservative), so this watch will NOT self-terminate while this persists",
78
+ });
79
+ }
80
+ }
81
+ if (alive) {
82
+ firstMissAtMs = undefined; // 活着的证据把窗口清零(连续性判据,不是累计计数)
83
+ return;
84
+ }
85
+ if (firstMissAtMs === undefined) {
86
+ firstMissAtMs = Date.now();
87
+ return;
88
+ }
89
+ stop();
90
+ deps.log("parent_watch_exit", {
91
+ pid: deps.pid,
92
+ firstMissAtMs,
93
+ secondMissAtMs: Date.now(),
94
+ note: "parent pid gone on two consecutive probes — entering the SIGTERM graceful drain path",
95
+ });
96
+ deps.drain();
97
+ }, PARENT_WATCH_INTERVAL_MS);
98
+ // 观察者绝不是进程存活的理由(与 drain tick 刻意不 unref 的取向相反)。
99
+ timer.unref?.();
100
+ function stop() {
101
+ if (stopped)
102
+ return;
103
+ stopped = true;
104
+ clearInterval(timer);
105
+ }
106
+ return { stop };
107
+ }
108
+ //# sourceMappingURL=parent-watch.js.map
@@ -62,7 +62,74 @@ export interface PendingCheckpoint {
62
62
  * summary, touchedPaths}) for human/irreversible_ask gates. listPending returns the queue severity-sorted
63
63
  * (DESC) so the supervisor inbox triages high-risk asks first. `null` for gates with no descriptor. */
64
64
  riskDescriptor: RiskDescriptor | null;
65
+ /**
66
+ * [3683]-2 / [3684]②(**ADDITIVE**):这条 park 的**规则候选** —— core 在 mint 这道门时挂到
67
+ * `pendingAction.ruleSuggestions` 上的那一份(core `checkpoint-store.d.ts` design/179 §4;它明写与
68
+ * 同步腿的 `AskRequest.ruleSuggestions` 是**同一条契约**:顺序即展示序,core 的 `exact` 形在场时排在
69
+ * 最前,core 契约基数 ≤2 —— server 侧执法帽是 `MAX_RULE_SUGGESTIONS`=4 的容忍余量,两个数的分工见
70
+ * 该常量顶注;**索引不是身份**:形不合的候选逐条丢,判形读 `match`)。
71
+ *
72
+ * 🔴 **无供给 ≠ 丢失**(本键存在的全部理由):同步腿的卡带这一格(`card_json` 里逐字在),而耐久腿
73
+ * (park 行 → `GET /v1/approvals`)此前**根本没投**,于是同一张卡走 durable 路时「不再询问」那一格
74
+ * 恒空 —— 消费端只能读成「本部署不供候选」。本键把 core 真给的那份带上 wire,缺席从此是**真的**没有
75
+ * 候选(规则车道没武装 / 命令说不出规则 / 这只 ask 规则清不掉),而不是投影层漏了。
76
+ *
77
+ * 🔴 **只在为真时在场**:core 不给 ⇒ 键**缺席**(不铸 `null`、不铸 `[]`)——与 `boundCallId`/`gateKind`
78
+ * 同一条 wire 纪律([1995]②「absent nullable field is OMITTED, not null」)。存量行(本列存在之前
79
+ * park 的)读回同样是缺席,与「这只 ask 没有候选」在消费端读法一致:本卡无此选项。
80
+ *
81
+ * 🔴 **展示用,永不参与 resume**:core 侧同款定性(「never read by the resume path, and never a rule by
82
+ * itself」)。本仓耐久腿今天也没有兑付口(`/v1/approvals/:id/decide` 的体不带规则位),所以它是**分诊
83
+ * 材料**不是承诺 —— 消费端渲这一格时要按「同副本活卡腿才兑得动」读(同 `ApprovalCardSchema.ruleSuggestions`
84
+ * 顶注那条射程说明)。
85
+ */
86
+ ruleSuggestions?: ReadonlyArray<{
87
+ rule: string;
88
+ match: "exact" | "prefix";
89
+ command: string;
90
+ }>;
65
91
  }
92
+ /**
93
+ * `pendingAction.ruleSuggestions` 的**铸行侧**投影(mint 面,与 {@link boundedToolInput} 同位同待遇)。
94
+ *
95
+ * 三件事,一件都不能少:
96
+ * 1. **结构窄读**(宪法 [2704] 边界必 schema / 禁裸 as-cast):blob 里的这一格是 `unknown`(旧 core /
97
+ * 坏行 / 未来加字段都可能)。形不合的**整条丢弃**(不是整只丢弃)——一条坏候选不该让另一条真候选消失。
98
+ * 判据只看 `rule`/`match`/`command` 三键合形(`RuleSuggestionRawSchema` 是**默认 strip 形** `.strip()`,
99
+ * 多余键**剥掉**、且不拒收 —— 重扫二轮更正:旧注写 `.loose()`,而 zod 4 的 loose = passthrough,
100
+ * 未知键会**留在** `parsed.data` 里,与「剥掉」相反)。上游 additive 加一键是 core 的常规做法,拿 strict 去窄读会让那一次加字段把整只
101
+ * 候选静默清零 —— 「有供给」当场变「无供给」,与本键的存在理由正相反(顶注第三段)。
102
+ * 2. **脱敏**:候选文本是从**命令原文**铸出来的(`Bash(curl -H "Authorization: Bearer …")` 这种 exact
103
+ * 形拼得出秘密),而 `GET /v1/approvals` 是**跨租户可见**的运维队列。落列前过 `redactSecrets` ——
104
+ * 与同行 `tool_input` 在 mint 就 `redactDeep` 是**同一条**纪律(#209 件4 把 `shadowedRule` 修在读面
105
+ * 是因为那条路的行早已落库、改铸点救不了存量;本键是**新**列,没有存量行要救,修在铸点更彻底:
106
+ * 秘密从此连**落库**都不落,不是落了库再在两条读面各洗一遍)。
107
+ * 3. **脱敏之后**才截长(codex 对抗复审 [medium],验真后修):`redactSecrets` 会**变长**,先按限长校验
108
+ * 原文再脱敏会产出超限文本,而 SQL 腿有**回读**(列 → 本函数再跑一遍)、LOCAL 腿没有 ⇒ 同一份素材
109
+ * 两个后端给出不同卡面。顺序反过来之后本函数对自己的输出**幂等**,两条腿同形。完整论证见
110
+ * `RuleSuggestionRawSchema` 顶注。
111
+ * 4. **限基数**:core 契约今天 ≤2,这里按 {@link MAX_RULE_SUGGESTIONS}(=4,server 侧容忍帽,两个数的
112
+ * 分工逐字见该常量顶注)截 —— 一条被改坏/未来放宽的上游不该把运维队列的一行撑爆。
113
+ * 🔴 **扣预算在合形之后**(合并码重扫):旧序先 `slice` 再判形,于是 ≥MAX 条坏候选能把唯一的真候选
114
+ * 挤出预算,读数是 `undefined` —— 与上面第 1 条自证的不变量(坏的丢、好的照投)正相反。扫描窗
115
+ * {@link MAX_RULE_SUGGESTION_SCAN} 仍是有界的:一条被改坏成上万条的行不该让每 3s 一拍的读路径去逐条判形。
116
+ *
117
+ * 🔴 `opts.countRedactions`:**两条腿都照常脱敏,只有回读腿不计数**(合并码重扫;codex 复审第一轮
118
+ * 指出「回读不脱敏」会把纵深防御一起关掉,验真后按这条收)。`redactSecrets` 对自己的输出是**值幂等、
119
+ * 计数不幂等**(URL-userinfo / JSON 秘密字段两条 pattern 会命中自己的输出并再计一次),而回读腿正是
120
+ * `/v1/approvals/stream` 每 3s 一拍的热路径 ⇒ 每条订阅每行每拍给 `redactions_applied_total` +1,把那个
121
+ * 用来识别「过度脱敏 / pattern 从不触发」的指标灌成不可读。关掉的是**计数**:重扫仍在,手改过的行 /
122
+ * 将来某条写路径漏了脱敏,读面照样兜住(运维队列跨租户可见,少脱一次的代价永远不能省)。
123
+ *
124
+ * 返回 `undefined` = **无供给**(键不落列):core 没给、给了空数组、或整只形不合。
125
+ */
126
+ export declare function boundedRuleSuggestions(raw: unknown, opts?: {
127
+ countRedactions?: boolean;
128
+ }): Array<{
129
+ rule: string;
130
+ match: "exact" | "prefix";
131
+ command: string;
132
+ }> | undefined;
66
133
  /** Redact + size-bound a pending tool call's args for persistence/serving. JSON-roundtrips so the stored shape
67
134
  * matches what the operator queue serves; oversized payloads collapse to a marker rather than bloating the row.
68
135
  * Exported for the LOCAL twin (local-checkpoint-store.ts) so the operator-queue projection can't drift. */
@@ -28,9 +28,11 @@
28
28
  */
29
29
  import { createHash } from "node:crypto";
30
30
  import { APPROVAL_GATE_KINDS_SQL_IN } from "../tool-approval.js"; // A-002.1 单一属主(SQL IN 片段从闭集数组派生)
31
- import { CheckpointError, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
32
- import { redactDeep } from "../trace/redact.js";
31
+ import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
32
+ import { redactDeep, redactSecrets } from "../trace/redact.js";
33
+ import { MAX_RULE_SUGGESTIONS, RuleSuggestionRawSchema } from "../approval-card.js";
33
34
  import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
35
+ import { recordFailOpen } from "../observability/fail-open.js";
34
36
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
35
37
  /** Cap the persisted/served pending-approval args so a pathological tool payload can't bloat the checkpoint row
36
38
  * or the operator-queue response (the BFF renders this in an approval card; a few KB is plenty). */
@@ -63,6 +65,115 @@ export const TERMINAL_GRACE_MS = 3_600_000; // exported for the LOCAL twin's rea
63
65
  export function tokenFingerprint(token) {
64
66
  return "sha256:" + createHash("sha256").update(token).digest("hex").slice(0, 12);
65
67
  }
68
+ /** 窄读的**扫描窗**:最多看这么多条原料再判形(基数帽 {@link MAX_RULE_SUGGESTIONS} 扣在合形之后)。
69
+ * 存在的理由是热读路径的有界性,不是基数语义 —— 所以它比基数帽宽得多。 */
70
+ const MAX_RULE_SUGGESTION_SCAN = 32;
71
+ /**
72
+ * `pendingAction.ruleSuggestions` 的**铸行侧**投影(mint 面,与 {@link boundedToolInput} 同位同待遇)。
73
+ *
74
+ * 三件事,一件都不能少:
75
+ * 1. **结构窄读**(宪法 [2704] 边界必 schema / 禁裸 as-cast):blob 里的这一格是 `unknown`(旧 core /
76
+ * 坏行 / 未来加字段都可能)。形不合的**整条丢弃**(不是整只丢弃)——一条坏候选不该让另一条真候选消失。
77
+ * 判据只看 `rule`/`match`/`command` 三键合形(`RuleSuggestionRawSchema` 是**默认 strip 形** `.strip()`,
78
+ * 多余键**剥掉**、且不拒收 —— 重扫二轮更正:旧注写 `.loose()`,而 zod 4 的 loose = passthrough,
79
+ * 未知键会**留在** `parsed.data` 里,与「剥掉」相反)。上游 additive 加一键是 core 的常规做法,拿 strict 去窄读会让那一次加字段把整只
80
+ * 候选静默清零 —— 「有供给」当场变「无供给」,与本键的存在理由正相反(顶注第三段)。
81
+ * 2. **脱敏**:候选文本是从**命令原文**铸出来的(`Bash(curl -H "Authorization: Bearer …")` 这种 exact
82
+ * 形拼得出秘密),而 `GET /v1/approvals` 是**跨租户可见**的运维队列。落列前过 `redactSecrets` ——
83
+ * 与同行 `tool_input` 在 mint 就 `redactDeep` 是**同一条**纪律(#209 件4 把 `shadowedRule` 修在读面
84
+ * 是因为那条路的行早已落库、改铸点救不了存量;本键是**新**列,没有存量行要救,修在铸点更彻底:
85
+ * 秘密从此连**落库**都不落,不是落了库再在两条读面各洗一遍)。
86
+ * 3. **脱敏之后**才截长(codex 对抗复审 [medium],验真后修):`redactSecrets` 会**变长**,先按限长校验
87
+ * 原文再脱敏会产出超限文本,而 SQL 腿有**回读**(列 → 本函数再跑一遍)、LOCAL 腿没有 ⇒ 同一份素材
88
+ * 两个后端给出不同卡面。顺序反过来之后本函数对自己的输出**幂等**,两条腿同形。完整论证见
89
+ * `RuleSuggestionRawSchema` 顶注。
90
+ * 4. **限基数**:core 契约今天 ≤2,这里按 {@link MAX_RULE_SUGGESTIONS}(=4,server 侧容忍帽,两个数的
91
+ * 分工逐字见该常量顶注)截 —— 一条被改坏/未来放宽的上游不该把运维队列的一行撑爆。
92
+ * 🔴 **扣预算在合形之后**(合并码重扫):旧序先 `slice` 再判形,于是 ≥MAX 条坏候选能把唯一的真候选
93
+ * 挤出预算,读数是 `undefined` —— 与上面第 1 条自证的不变量(坏的丢、好的照投)正相反。扫描窗
94
+ * {@link MAX_RULE_SUGGESTION_SCAN} 仍是有界的:一条被改坏成上万条的行不该让每 3s 一拍的读路径去逐条判形。
95
+ *
96
+ * 🔴 `opts.countRedactions`:**两条腿都照常脱敏,只有回读腿不计数**(合并码重扫;codex 复审第一轮
97
+ * 指出「回读不脱敏」会把纵深防御一起关掉,验真后按这条收)。`redactSecrets` 对自己的输出是**值幂等、
98
+ * 计数不幂等**(URL-userinfo / JSON 秘密字段两条 pattern 会命中自己的输出并再计一次),而回读腿正是
99
+ * `/v1/approvals/stream` 每 3s 一拍的热路径 ⇒ 每条订阅每行每拍给 `redactions_applied_total` +1,把那个
100
+ * 用来识别「过度脱敏 / pattern 从不触发」的指标灌成不可读。关掉的是**计数**:重扫仍在,手改过的行 /
101
+ * 将来某条写路径漏了脱敏,读面照样兜住(运维队列跨租户可见,少脱一次的代价永远不能省)。
102
+ *
103
+ * 返回 `undefined` = **无供给**(键不落列):core 没给、给了空数组、或整只形不合。
104
+ */
105
+ export function boundedRuleSuggestions(raw, opts = {}) {
106
+ if (!Array.isArray(raw))
107
+ return undefined;
108
+ const observe = opts.countRedactions ?? true;
109
+ const out = [];
110
+ for (const item of raw.slice(0, MAX_RULE_SUGGESTION_SCAN)) {
111
+ if (out.length >= MAX_RULE_SUGGESTIONS)
112
+ break; // 基数帽扣在**合形之后**
113
+ const parsed = RuleSuggestionRawSchema.safeParse(item);
114
+ if (!parsed.success)
115
+ continue; // 坏的那条丢掉,好的那条照投
116
+ // 先脱敏、再截 —— 截完必然满足 `RuleSuggestionSchema` 的 `.max()`,于是回读再跑一遍是恒等操作。
117
+ out.push({
118
+ rule: redactSecrets(parsed.data.rule, { observe }).slice(0, MAX_RULE_TEXT_CHARS),
119
+ match: parsed.data.match,
120
+ command: redactSecrets(parsed.data.command, { observe }).slice(0, MAX_RULE_TEXT_CHARS),
121
+ });
122
+ }
123
+ return out.length > 0 ? out : undefined;
124
+ }
125
+ /**
126
+ * `checkpoint.rule_suggestions` 这一格的**回读口**(合并码重扫)。
127
+ *
128
+ * 🔴 为什么不是直接 `parseJson`:本文件的 `parseJson` 是 **strict** flavor(畸形 JSON 文本直接抛),而
129
+ * `listPending` 是 `rows.map(...)` —— 一处抛就中止整只映射 ⇒ **一行**手改坏的 cell 打掉整个租户的运维
130
+ * 队列(`GET /v1/approvals` 500;`/v1/approvals/stream` 落进它的 catch,每 3s 一次静默重试,心跳照常而
131
+ * 队列永远空)。而 {@link boundedRuleSuggestions} 的顶注承诺的是「手改过的行在这里当场落成**无供给**」——
132
+ * 承诺只对「合法 JSON 但错形」成立,对「JSON 文本本身坏了」不成立。这里把两者补齐:坏 cell ⇒ 这一行
133
+ * 这一格缺席,其余行照常读出来。
134
+ *
135
+ * 🔴 **响亮**:F 类兜底(丢的是一格展示/分诊材料,不是判据),按仓内纪律走 `recordFailOpen` —— 登记 tag
136
+ * + 逐次计数 + 逐次探针 + 每进程一次 warn。静默 `catch` 在这条路上正是本仓要消灭的形。
137
+ */
138
+ function readRuleSuggestionsCell(cell) {
139
+ let parsed;
140
+ try {
141
+ parsed = parseJson(cell);
142
+ }
143
+ catch {
144
+ recordFailOpen("server.approvals.rule-suggestions-cell-unreadable");
145
+ return undefined;
146
+ }
147
+ return boundedRuleSuggestions(parsed, { countRedactions: false });
148
+ }
149
+ /**
150
+ * `checkpoint.risk_descriptor` 这一格的**回读口**(重扫二轮)——与 {@link readRuleSuggestionsCell} 同族同待遇。
151
+ *
152
+ * 🔴 病(红先复现):上一格补了容错,而**同一个 `rows.map`** 里这一格还走 strict `parseJson`。两个后端里
153
+ * 这两列是同一种存储形(`tidb-pool.ts` / `pg-pool.ts` 双方言都是 TEXT),坏法完全同族(运维手改 / 回滚
154
+ * 残留 / 列语义漂)⇒ 坏的若是这一格,`listPending` 照样整只 reject,上一格顶注与 FAIL-OPEN-CENSUS 宣称的
155
+ * 「一行坏 cell 不再打掉整只队列」在「手改过的行」这个真实场景上并不成立。
156
+ *
157
+ * 🔴 **失败方向自证**(能力语义一致性,#157 纪律):这一格在本读面上只有三个**展示/分诊**消费者 ——
158
+ * ① severity 分诊排序(缺席折 0,排在有 descriptor 的行之后)② `shadowedRule` 的读面脱敏 ③ 读面派生的
159
+ * `governanceForced` 徽章。三者**都不是执法判据**:门早已 park,队列只负责把它显示给人;真按
160
+ * `riskDescriptor.shellGateDoctrine` 判的 `active-run-conflict.ts` 读的是 checkpoint blob 的 `get()`,
161
+ * 不经本读面。而 `governanceForced` 的成文语义本就是「缺席 = 没有治理来源的**证据**」(恒不写 `false`),
162
+ * 缺席是保守读法。对照另一支:整只队列 500 时这三样连同**其余所有行**一起消失。⇒ 行级容忍严格优于整只
163
+ * 毒化,不是把保护型判据静默放行。
164
+ *
165
+ * tag **单列不并计**:两格的缺席后果不同(候选=「不再询问」那一格;本格=分诊排序与出身徽章),并计会让
166
+ * 「哪一格在坏」在遥测里读不出来。
167
+ */
168
+ function readRiskDescriptorCell(cell) {
169
+ try {
170
+ return parseJson(cell);
171
+ }
172
+ catch {
173
+ recordFailOpen("server.approvals.risk-descriptor-cell-unreadable");
174
+ return null;
175
+ }
176
+ }
66
177
  /** Redact + size-bound a pending tool call's args for persistence/serving. JSON-roundtrips so the stored shape
67
178
  * matches what the operator queue serves; oversized payloads collapse to a marker rather than bloating the row.
68
179
  * Exported for the LOCAL twin (local-checkpoint-store.ts) so the operator-queue projection can't drift. */
@@ -313,10 +424,15 @@ export class SqlCheckpointStore {
313
424
  // Persist the (redacted, bounded) tool args alongside so the operator queue can serve the approval-card
314
425
  // payload without an N+1 trace.turns fetch. Stored as a stringified JSON column value.
315
426
  const toolInput = boundedToolInput(pa?.args);
427
+ // [3684]②:归因键之一——core 挂在 pendingAction 上的规则候选,**在 mint 这一刻**落成自己的列。
428
+ // 为什么是列而不是 listPending 里现拆 blob:`checkpoint` 列是整只 suspend 快照(会话状态全在里面),
429
+ // 运维队列每行拖一份那个 blob 是数量级错误的读放大;同行的 `tool_input`/`bound_input_hash`/`gate_kind`/
430
+ // `risk_descriptor` 四列都是同一条「铸行时反范式一格,读口零 blob」的先例。
431
+ const ruleSuggestions = boundedRuleSuggestions(pa?.ruleSuggestions);
316
432
  try {
317
- await this.db.query(this.q("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at_ms, terminal_at_ms, gate_kind, bound_input_hash, risk_descriptor) " +
318
- "VALUES (?,?,?,?,'pending',?,?,?,?,?,?,?,?,?,?)", "INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at_ms, terminal_at_ms, gate_kind, bound_input_hash, risk_descriptor) " +
319
- "VALUES ($1,$2,$3,$4,'pending',$5,$6,$7::jsonb,$8::jsonb,$9,$10,$11,$12,$13,$14)"), [
433
+ await this.db.query(this.q("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at_ms, terminal_at_ms, gate_kind, bound_input_hash, risk_descriptor, rule_suggestions) " +
434
+ "VALUES (?,?,?,?,'pending',?,?,?,?,?,?,?,?,?,?,?)", "INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at_ms, terminal_at_ms, gate_kind, bound_input_hash, risk_descriptor, rule_suggestions) " +
435
+ "VALUES ($1,$2,$3,$4,'pending',$5,$6,$7::jsonb,$8::jsonb,$9,$10,$11,$12,$13,$14,$15)"), [
320
436
  token,
321
437
  cp.scope,
322
438
  cp.sessionId,
@@ -331,6 +447,7 @@ export class SqlCheckpointStore {
331
447
  cp.gate?.kind ?? null, // D-D SLA split: human/irreversible_ask deadline → resolve-deny; others → expire
332
448
  pa?.boundInputHash ?? null, // D-1: the opaque hash the portal must echo on /decide (surfaced via listPending so the TOCTOU binding is reachable)
333
449
  ((g) => (g?.riskDescriptor ? this.json(g.riskDescriptor, "risk descriptor") : null))(cp.gate), // riskDescriptor inbox: stamp core's INERT descriptor for triage-sort
450
+ ruleSuggestions === undefined ? null : this.json(ruleSuggestions, "rule suggestions"), // [3684]②:无供给 ⇒ 列 NULL(读口据它省键,绝不铸 [])
334
451
  ]);
335
452
  }
336
453
  catch (e) {
@@ -571,7 +688,7 @@ export class SqlCheckpointStore {
571
688
  * joined from task_active (the JOIN KEY to the run/trace — a suspended run KEEPS its
572
689
  * session claim, so the join is live for every pending row; null only in pathological windows). */
573
690
  async listPending(scope) {
574
- const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at_ms, c.deadline, c.gate_kind, ta.task_id " +
691
+ const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.rule_suggestions, c.created_at_ms, c.deadline, c.gate_kind, ta.task_id " +
575
692
  "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
576
693
  const { rows } = scope
577
694
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
@@ -580,6 +697,11 @@ export class SqlCheckpointStore {
580
697
  const toolCallId = r.tool_call_id ?? null;
581
698
  const boundInputHash = r.bound_input_hash ?? null;
582
699
  const gateKind = r.gate_kind ?? null;
700
+ // [3684]②:回读也走**同一个** `boundedRuleSuggestions`(不是裸 parse 直投)—— 形的属主只有一个,
701
+ // 手改过的行/未来列语义漂了都在这里当场落成「无供给」而不是把垃圾投上运维队列。列已在 mint 面脱敏,
702
+ // 这一遍照常脱敏(纵深防御:手改过的行也不许把凭证投上跨租户队列),只是**不计数**
703
+ // (值幂等但计数不幂等,见该函数 `opts.countRedactions` 顶注)。
704
+ const ruleSuggestions = readRuleSuggestionsCell(r.rule_suggestions);
583
705
  return {
584
706
  sessionId: String(r.session_id),
585
707
  scope: String(r.scope),
@@ -600,7 +722,11 @@ export class SqlCheckpointStore {
600
722
  input: r.tool_input ?? null,
601
723
  createdAt: Number(r.created_at_ms),
602
724
  deadline: r.deadline == null ? null : Number(r.deadline),
603
- riskDescriptor: parseJson(r.risk_descriptor),
725
+ // 重扫二轮:与下一格同族的**容错回读**(strict 形会把一行手改坏的 cell 变成整只队列 500,
726
+ // 判据与失败方向逐字见 {@link readRiskDescriptorCell} 顶注)。
727
+ riskDescriptor: readRiskDescriptorCell(r.risk_descriptor),
728
+ // [3684]②:无供给 ⇒ **省键**(同 boundCallId/gateKind 的 [1995]② 纪律,绝不 null/[])。
729
+ ...(ruleSuggestions !== undefined ? { ruleSuggestions } : {}),
604
730
  };
605
731
  });
606
732
  // riskDescriptor inbox: triage-sort by severity DESC (highest-risk asks first), then oldest-first within a
@@ -25,7 +25,7 @@ import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, mkdirSyn
25
25
  import { join } from "node:path";
26
26
  import { isApprovalGateKind } from "../tool-approval.js"; // A-002.1 单一属主
27
27
  import { FileCheckpointStore, atomicWriteFile, sanitizePathComponent, } from "@sema-agent/core";
28
- import { boundedToolInput, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
28
+ import { boundedRuleSuggestions, boundedToolInput, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS } from "./checkpoint-store-sql.js";
29
29
  /** design/80 D-D read-time twin of the TiDB put-time `terminal_at_ms` column (same formula — anti-drift). */
30
30
  function terminalAtOf(cp) {
31
31
  return Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS);
@@ -237,6 +237,16 @@ export class LocalCheckpointStore {
237
237
  createdAt: cp.createdAt,
238
238
  deadline: cp.deadline ?? null,
239
239
  riskDescriptor: gate?.riskDescriptor ?? null,
240
+ // [3684]②:与 SQL 孪生**同一个** `boundedRuleSuggestions`(窄读+脱敏+截基数)。本地形没有列,
241
+ // 直接从 blob 上的 `pendingAction` 取——投影结果必须与两条 SQL 腿逐字同形,否则 local 档的运维
242
+ // 队列会给出一份别人没有的卡面。无供给 ⇒ 省键(绝不 null/[])。
243
+ // 🔴 这里**连计数一起**保留(不同于 SQL 腿的回读形 `{ countRedactions: false }` —— 那条腿脱敏
244
+ // **照跑**,关掉的只是计数):本地形每次读的是**未脱敏的原始 blob**,这一遍是它唯一的一次脱敏,
245
+ // 不是重扫已脱敏文本 ⇒ `redactions_applied_total` 记的是真命中,该计。
246
+ ...(() => {
247
+ const rs = boundedRuleSuggestions(pa?.ruleSuggestions);
248
+ return rs !== undefined ? { ruleSuggestions: rs } : {};
249
+ })(),
240
250
  });
241
251
  }
242
252
  // Same triage order as the SQL twins: severity DESC, then oldest-first within a tier.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * design/234 —— embedder **指纹门**(清空重建形,clay 终裁 [3647]⑤ 方向;稿=sema-internal
3
+ * `server/design/design-234-embedder-fingerprint-20260812.md` v1+v2)。
4
+ *
5
+ * ## 病灶
6
+ *
7
+ * 向量行落在 `agent_memory_engine_entry.embedding`,**行上没有任何「产自哪个 embedder」的记账**。既有
8
+ * 防线只有 embedder 的维度门(长度≠`MEMORY_EMBEDDER_DIM` ⇒ 抛),它只拦「换模型且维度恰好不同」;
9
+ * **同维度换模型**(768 家族之间、1024 家族之间迁移=最常见形)完全静默——旧空间与新空间的向量在同一
10
+ * 列里做 cosine,检索排序变噪音,没有任何一层会说出来。
11
+ *
12
+ * ## 形
13
+ *
14
+ * 指纹 = identity 两元组 `{ model, dimensions }`,**明文**存(不 hash:诊断可读性 > 紧凑,日志要能直接
15
+ * 念出「从 X 换到了 Y」)。**endpoint 不进指纹**——换供应商域名/代理/端口不换语义空间,进了会造成大量
16
+ * 误清;残余成文:同名模型在不同供应商若是不同实现(自托管 finetune 撞名)指纹辨不出,operator 明知
17
+ * 异实现时改 `MEMORY_EMBEDDER_MODEL` 名或手动清列(USAGE 有一行 SQL)。
18
+ *
19
+ * 存储位 = **新单行元表**(additive,零删库重建窗)。不给主表加列:主表改形=触发本仓「无 ALTER、删库
20
+ * 重建」BREAKING 窗;行级记账在「清空重建」语义下也无增益(全量清 ⇒ 不存在混合态)。
21
+ *
22
+ * ## 属主边界
23
+ *
24
+ * `memory-engine-pg.ts` 是 core 移入件(minimal-diff move-in 纪律)⇒ 元表 DDL + 比对清空腿全在**本
25
+ * 文件**(server 侧),移入文件零改动;表名常量本文件自有(`PG_MEMORY_ENGINE_TABLES` 不动)。
26
+ * 列名 `meta_key`/`meta_value`:`key` 是 MySQL 保留字,将来若补 TiDB 腿零反引号坑(K2)。
27
+ *
28
+ * ## 六态(v2 总表)
29
+ *
30
+ * | 态 | 判定 | 动作 |
31
+ * |---|---|---|
32
+ * | A | embedder 配置缺席 | **门不装**(连 ensure 都不调),元表不动——装配点的事,不在本文件 |
33
+ * | B | 元表有行、identity 逐键相等 | 照常,**零写** |
34
+ * | C | 元表有行、不等 | 清列 → upsert 新 identity(`reason:"changed"`) |
35
+ * | D | 元表无行、库内**有**非 NULL 向量 | 同 C 清(存量库无法证明同源=保守清,`reason:"unattributed"`) |
36
+ * | E | 元表无行、零向量行 | 只写 identity,不清(`memory_embedder_identity_armed`) |
37
+ * | F | 元表有行但 value 解析失败 | 走清空臂(`reason:"unreadable"`)——**绝不当成匹配**(边界必 schema) |
38
+ *
39
+ * (F 的判据是 `model`/`dimensions` 缺失或类型不对;**多出来的未知键不算坏**,见 schema 处的注。)
40
+ *
41
+ * ## 三条不可动的判据
42
+ *
43
+ * 1. **次序:先清列、后写元表**。反序若中间崩溃 = 新 identity 已记而旧向量还在 = 正是被修的病(此后
44
+ * 每次 boot 恒判 B,残留态永不复检);正序中间崩溃 = 下次 boot 无元表行、向量已 NULL ⇒ E 态补记,
45
+ * 幂等安全。
46
+ * 2. **CAS 循环**(v2 C1-2):元表写后**重读**,读回≠自写 ⇒ 另一副本并发写了**不同** identity(部署
47
+ * 配置漂移)⇒ 响亮 `memory_embedder_identity_conflict`(两值并示)+ 重跑比对,上限
48
+ * {@link CAS_MAX_ROUNDS} 轮,超限抛(拒启)。运行期旧副本混写非 CAS 可拦 ⇒ 归停机纪律(USAGE)。
49
+ * 3. **门自身失败 = 抛(拒启)**。吞掉 = 元表停留旧值 = 下次 boot 再清一次,把一次性代价变成每次 boot
50
+ * 的代价;且「门在场但没跑成」静默 = fail-open。
51
+ *
52
+ * 计数取法:`PgQueryResult` 刻意无 `rowCount` ⇒ 用 CTE `WITH c AS (UPDATE … RETURNING 1) SELECT
53
+ * count(*)::int`(禁 `RETURNING id` 全表拉回)。计数**按整次调用累计**、原因锚在第一次真清的那一轮 ——
54
+ * 「清过就必响亮」对终局判 B 的那一支同样成立(codex R2 finding 2)。
55
+ *
56
+ * ## 射程外(稿 §出界)
57
+ *
58
+ * 主动回填件(清空后不回填,行被 patch 时自然重嵌)/ 行级 embedder 记账列(结构性根治滚动窗)/
59
+ * pgvector 接线件的列 dim 校验 / TiDB 向量面(v1 lexical only)。
60
+ */
61
+ import { z } from "zod";
62
+ import type { Logger } from "../observability/logger.js";
63
+ import type { PgQueryFn } from "./pg-query.js";
64
+ /** 元表名(本文件自有 const —— core 移入文件的 `PG_MEMORY_ENGINE_TABLES` 零改动)。 */
65
+ export declare const PG_MEMORY_ENGINE_META_TABLE = "agent_memory_engine_meta";
66
+ /** 单行元表里 embedder 指纹那一行的键。 */
67
+ export declare const EMBEDDER_IDENTITY_META_KEY = "embedder_identity";
68
+ /** CAS 循环上限(v2 C1-2)。超限 = 并发副本带着**不同** identity 在互相覆盖 ⇒ 拒启。 */
69
+ export declare const CAS_MAX_ROUNDS = 3;
70
+ /**
71
+ * 指纹 schema —— 读回的 `meta_value` 必须过它(手改坏值/回滚残留一律走 F 态,禁 `JSON.parse as` 裸断言)。
72
+ *
73
+ * ⚠️ **未知键刻意放过**(zod 对象默认 strip,不加 `.strict()`):将来某个版本若往这行里多写一个键,
74
+ * 老副本读到它应当照常按 `model`/`dimensions` 比对,而不是判 F 态把整列清掉 —— 「新副本加了个字段」
75
+ * 与「这行坏了」不是一回事,后者才是 F 态要抓的。载荷只有这两个键,多出来的不参与判定。
76
+ */
77
+ declare const EmbedderIdentitySchema: z.ZodObject<{
78
+ model: z.ZodString;
79
+ dimensions: z.ZodNumber;
80
+ }, z.core.$strip>;
81
+ /** `{ model, dimensions }` —— 明文指纹(endpoint 刻意不进,见头注)。 */
82
+ export type EmbedderIdentity = z.infer<typeof EmbedderIdentitySchema>;
83
+ /** 比对结果的态(六态里除 A 之外的五个 —— A 在装配点,门根本不装)。 */
84
+ export type EmbedderFingerprintState = "match" | "changed" | "unattributed" | "unreadable" | "armed";
85
+ /** 一次比对的结果(装配点只需要它成功返回;字段供测试与将来观测面读)。 */
86
+ export interface EmbedderFingerprintReport {
87
+ /** 命中的态。 */
88
+ state: EmbedderFingerprintState;
89
+ /** 本次调用**累计**清空的向量行数(跨 CAS 轮累加;从未清过 = 0)。 */
90
+ cleared: number;
91
+ /** 清/写之前元表里的 identity;无行或不可解析 ⇒ null。 */
92
+ previous: EmbedderIdentity | null;
93
+ /** 本次写入(或已匹配)的 identity。 */
94
+ identity: EmbedderIdentity;
95
+ /** 收敛用了几轮(1 = 一次过;>1 = 中途撞到并发不同 identity)。 */
96
+ rounds: number;
97
+ }
98
+ /** 比对腿的部署腿注入(日志 + 时钟)。日志缺席 = 纯静默跑(测试/脚本形),装配点必传。 */
99
+ export interface ReconcileEmbedderFingerprintOptions {
100
+ /** boot logger(`info` 报换模型/首装,`error` 报并发 identity 冲突)。 */
101
+ log?: Pick<Logger, "info" | "error">;
102
+ /** 耗时读数的时钟(默认 `Date.now`)——**整次调用**的耗时进日志(含 CAS 重试轮),
103
+ * 兼作锁窗量纲的实测积累(v2 V3)。 */
104
+ now?: () => number;
105
+ }
106
+ /**
107
+ * 元表 DDL —— **只发 CREATE**(K1:混入 SELECT/UPDATE 会让 `scripts/dump-schema.ts` 的录制门当场抛)。
108
+ * 幂等,与其余 `ensurePg*Schema` 同姿势;新腿已注册 `PG_LEGS` + `docs/schema/baseline-pg.sql`。
109
+ */
110
+ export declare function ensurePgMemoryEmbedderMetaSchema(query: PgQueryFn): Promise<void>;
111
+ /**
112
+ * 指纹比对腿 —— boot 一次(env 族是 boot 快照,运行中不热换)。**门自身失败一律抛**(拒启)。
113
+ *
114
+ * ⚠️ 调用姿势:`ensurePgMemoryEmbedderMetaSchema` 之后、memory backend 构造之前;embedder 配置**在场**
115
+ * 才调(A 态连 ensure 都不调)。
116
+ */
117
+ export declare function reconcileEmbedderFingerprint(query: PgQueryFn, identity: EmbedderIdentity, opts?: ReconcileEmbedderFingerprintOptions): Promise<EmbedderFingerprintReport>;
118
+ export {};
119
+ //# sourceMappingURL=memory-embedder-fingerprint.d.ts.map