@modusensus/dsh-mneme 0.8.9 → 0.8.11

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.
@@ -1,10 +1,17 @@
1
- // #254 写入准入(第一阶段:只计量,不拦截)。
2
- //
3
- // 为什么先只计量:会话写入预算的 N 与同话题冷却的 X 在没有真实分布时拍任何数字
4
- // 都是误杀面(维护者 2026-09-23 拍板「先记录不拦,由遥测定」)。所以这一阶段只把
5
- // 两个闸门的原始测量点写进 llm_audit_logs(status='skipped'、metadata.gate='g1'
6
- // 且同话题重复时附 metadata.g2),不做判定、不改任何写入行为;第二阶段把拦截打开
7
- // 时才引入阈值与二次确认(memory_save({confirm:true}))。
1
+ // #254 写入准入。两个阶段:
2
+ // 阶段一(已合入):只计量、不拦截。会话写入预算的 N 与同话题冷却的 X 在没有真实
3
+ // 分布时拍任何数字都是误杀面(维护者 2026-09-23 拍板「先记录不拦,由遥测定」)。
4
+ // 所以那两个闸门只把原始测量点写进 llm_audit_logs(status='skipped'、
5
+ // metadata.gate='g1' 且同话题重复时附 metadata.g2),不做判定、不改任何写入行为;
6
+ // 阈值与二次确认(memory_save({confirm:true}))等遥测有分布再单独提。
7
+ // 阶段一之后(本批):第 1 级确定性拒绝。按 2026-09-21/09-22 两轮收窄的口径,
8
+ // 第 1 级只剩**密钥/PII 与空白噪声**两类判据走硬拒;去重键命中判 write-update 放行
9
+ // (不把同步去重升级成拒绝门——2606.24535 记的那个失效模式:同步近重复门抢在异步
10
+ // 矛盾检测之前,本该被裁决的矛盾写入被门直接拒掉)。两类判据都零 LLM、纯确定性。
11
+ // 两个开关都默认关:enabled 打开判定,enforce 打开拦截(关 = 仅告警 + 留审计)。
12
+ // 默认关时 evaluate 的返回与阶段一逐字段一致(验收第 1 条「默认路径零行为变化」)。
13
+ // 密钥/PII 那一类的判据实现不在本模块:按验收第 4 条它是 #164 A2 的判据来源,由
14
+ // createWriteAdmission 的 sensitiveScan 注入,本模块只定义它怎么被消费。
8
15
  //
9
16
  // 为什么复用 llm_audit_logs:它是仓库既有的「后台动作回执」表(bookkeeping,不
10
17
  // 触发写钩子,见 store.js 的表注释)。第二阶段落地时「第一次 skipped/gate →
@@ -36,7 +43,7 @@
36
43
  // deleteOldLlmAudits),所以关掉时本模块一行都不写,否则就是在无保留期的表里做
37
44
  // 按写入频次增长。
38
45
  import { PINNED_MEMORY_TYPES } from "./service.js";
39
- import { contentHashOf } from "./content-hash.js";
46
+ import { contentHashOf, normalizeForHash } from "./content-hash.js";
40
47
 
41
48
  // 审计口径(llm_audit_logs 的既有列):trigger_source = 组件名,operation_type =
42
49
  // 动作名,status='skipped' = 本行没有产生任何 LLM 花费(与 summarize 的间隔门、
@@ -45,6 +52,16 @@ export const ADMISSION_TRIGGER_SOURCE = "writeAdmission";
45
52
  export const ADMISSION_OPERATION_TYPE = "write_admission";
46
53
  export const ADMISSION_GATE_SESSION_BUDGET = "g1";
47
54
 
55
+ // 决策两值。阶段一恒 allow;第 1 级判定命中且 enforce 打开时才 deny。
56
+ export const ADMISSION_DECISION_ALLOW = "allow";
57
+ export const ADMISSION_DECISION_DENY = "deny";
58
+
59
+ // 第 1 级的拒绝原因(#254 拍板:硬拒绝只留密钥/PII 与空白噪声;去重键命中归
60
+ // write-update 放行,G1/G2 走二次确认)。reason 是审计行里的稳定键,别当展示文案用。
61
+ export const ADMISSION_DENY_BLANK = "blank";
62
+ export const ADMISSION_DENY_NOISE = "noise";
63
+ export const ADMISSION_DENY_SENSITIVE = "sensitive";
64
+
48
65
  // 首次见到某会话时的回填深度:话题表由审计行重建,只回填最近这么多行。更早的
49
66
  // 话题会漏(测量口径可接受),换来的是一次查询与有界的常驻内存。
50
67
  export const TOPIC_LOOKBACK_ROWS = 200;
@@ -76,19 +93,58 @@ export function extractTopicKeys(memory) {
76
93
  return [...keys].sort();
77
94
  }
78
95
 
96
+ /**
97
+ * 第 1 级「空白 / 纯噪声」判据(#254,零 LLM、纯函数、无状态)。
98
+ *
99
+ * 为什么这两类够格进硬拒:它们的命中面**没有解释空间**——一条归一化后什么都不剩的
100
+ * 写入,进库之后既检索不到也注入不了,写它只有成本没有收益,不存在「误杀有价值内容」
101
+ * 的可能。这正是硬拒与二次确认的分界:有解释空间的(去重命中、预算/冷却)走二次确认,
102
+ * 没有解释空间的才硬拒。
103
+ *
104
+ * 为什么复用 content-hash 的归一化而不另写一套:判据要的是「有没有信息」,而
105
+ * normalizeForHash 的口径(NFKC → 小写 → 去标点 → 空白折叠)恰好就是「把格式折掉之后
106
+ * 还剩什么」。另写一套只会多出一个会漂移的口径(同 content-hash.js 文件头的存量重算
107
+ * 理由)。两个 reason 分开报是为了可解释:`blank` 是真的什么都没写(提交一个空表单),
108
+ * `noise` 是写了但只有标点/空白(`...`、`---`)——两者的修法不同。
109
+ *
110
+ * 为什么不用 quality-filter 的 `repetitive`(dedupRatio < 0.3)一起判:那是**写入后**
111
+ * 的评分扣分项,扣分可以错(只影响排序与归档),硬拒不能错。一条字符多样性低的正常
112
+ * 记忆(同一种分隔符排出的长清单)会被它判成 repetitive,本判据不背这个误杀面。
113
+ *
114
+ * @param {{title?: string, content?: string}|null|undefined} memory
115
+ * @returns {"blank"|"noise"|null} 命中即返回 reason,否则 null
116
+ */
117
+ export function informationlessReason(memory) {
118
+ const title = String(memory?.title ?? "").trim();
119
+ const content = String(memory?.content ?? "").trim();
120
+ if (!title && !content) return ADMISSION_DENY_BLANK;
121
+ if (!normalizeForHash(title) && !normalizeForHash(content)) return ADMISSION_DENY_NOISE;
122
+ return null;
123
+ }
124
+
79
125
  /**
80
126
  * 写入准入(#254)。返回的两个方法就是三个消费方共用的那份接口:
81
- * evaluate 给出决策形状(阶段一恒 allow),record 把测量点写成审计行。
82
- * 阶段二在 evaluate 里引入阈值与 confirm 分支;#249 的注入提示与 #275 的水位计数
83
- * 都读同一份 decision/审计行,而不是各写一套判定。
127
+ * evaluate 给出决策形状,record 把测量点/拒绝面写成审计行。
128
+ * #249 的注入提示与 #275 的水位计数都读同一份 decision/审计行,而不是各写一套判定。
129
+ *
130
+ * 两个开关(config.writeAdmission,都默认关——验收第 1 条「默认路径零行为变化」):
131
+ * enabled — 打开第 1 级判定(空白/噪声 + 注入进来的密钥/PII 判据)。
132
+ * 关时 evaluate 的返回与阶段一逐字段一致,只算三个测量点。
133
+ * enforce — 命中时真的拒绝。关时判定照跑、照留审计,但决策回落 allow(仅告警)。
134
+ * 密钥/PII 那一类判据不在这里实现:按 #254 验收第 4 条,它是 #164 A2 的判据来源,
135
+ * 由 `sensitiveScan` 注入(见下),本模块只定义它怎么被消费。
84
136
  *
85
137
  * @param {object} deps
86
138
  * @param {object} deps.store 存储层(listLlmAudits / saveLlmAudit)
87
- * @param {object} [deps.config] 已解析配置(读 llmAudit.enabled)
139
+ * @param {object} [deps.config] 已解析配置(读 llmAudit.enabled、writeAdmission.*)
88
140
  * @param {object} [deps.logger]
89
141
  * @param {() => number} [deps.now] 时钟注入(测试用)
142
+ * @param {(memory: object) => ({reason: string, kind?: string, label?: string}|null)} [deps.sensitiveScan]
143
+ * 第 1 级的密钥/PII 判据(#164 A2 的落点)。约定:命中返回 `{kind, label}`,未命中
144
+ * 返回 null,**不抛**(抛出按未命中处理并 warn,判据故障不能让写入变成不可用)。
145
+ * 缺省时这一类判据整个不参与——本模块在 A2 落地前只跑空白/噪声。
90
146
  */
91
- export function createWriteAdmission({ store, config, logger, now = Date.now } = {}) {
147
+ export function createWriteAdmission({ store, config, logger, now = Date.now, sensitiveScan = null } = {}) {
92
148
  // sessionKey → Map<话题锚, 最近一次新建行时刻(ms)>。真相源是审计行,这里只是它的
93
149
  // 增量视图:进程重启后按需回填。FIFO 上限防长驻进程内存无界(同 store.js 的
94
150
  // embedding 解析缓存口径)。
@@ -115,6 +171,11 @@ export function createWriteAdmission({ store, config, logger, now = Date.now } =
115
171
  // 穿透行(pinned)整个不在闸门里,也就不该进基准表——只挡内存路径不挡回填,
116
172
  // 重启后口径就变了(同一个话题会因为一条穿透行而被判成重复)。
117
173
  if (metadata.exempt) continue;
174
+ // 被第 1 级拦下的行同理:它根本没进库。内存路径已经跳过了(见 record 的
175
+ // enforced 判断),回填路径必须同口径——否则同一条时间线会随进程重启而变:
176
+ // 重启前不重复、重启后变重复。仅告警档(deny 非空但 decision=allow)要留,
177
+ // 那条行确实落库了。
178
+ if (metadata.deny?.enforced === true) continue;
118
179
  const at = Date.parse(row.timestamp);
119
180
  // 坏时间戳跳过:落到 0 会让 gap 变成几十年,一个离群值就能带偏按分位定的 X。
120
181
  if (!Number.isFinite(at)) continue;
@@ -165,25 +226,82 @@ export function createWriteAdmission({ store, config, logger, now = Date.now } =
165
226
  }
166
227
 
167
228
  /**
168
- * 阶段一决策:恒 allow(本模块里没有阈值),只算出三个闸门的测量点。返回形状一次
169
- * 定死,阶段二只往里加分支、不改字段:
170
- * decision — "allow" | "confirm"(阶段一只可能 allow)
229
+ * 第 1 级判据的汇总口(#254):空白 / 噪声 + 注入进来的密钥 / PII。
230
+ * 顺序按成本排:字符串判据在前,注入的扫描器在后;扫描器抛异常按「未命中」处理
231
+ * (warn)——判据故障不能让所有写入变成不可用,这是与计量同一条不反噬原则。
232
+ * @returns {{reason: string, kind?: string, label?: string}|null}
233
+ */
234
+ function firstLevelHit(memory) {
235
+ const info = informationlessReason(memory);
236
+ if (info) return { reason: info };
237
+ if (typeof sensitiveScan !== "function") return null;
238
+ try {
239
+ const hit = sensitiveScan(memory);
240
+ if (!hit) return null;
241
+ const deny = { reason: ADMISSION_DENY_SENSITIVE };
242
+ // kind / label 只在判据真给了才落盘:给 undefined 会让审计行里出现一个
243
+ // 存在但无值的键,读的人分不清「没报」和「报了空串」。
244
+ if (hit.kind !== undefined) deny.kind = hit.kind;
245
+ if (hit.label !== undefined) deny.label = hit.label;
246
+ return deny;
247
+ } catch (e) {
248
+ warn(`[dsh-mneme] write admission sensitive scan failed: ${String(e)}`);
249
+ return null;
250
+ }
251
+ }
252
+
253
+ /**
254
+ * 决策形状一次定死,后续只加分支、不改字段:
255
+ * decision — "allow" | "deny"
171
256
  * gate — "g1" | null(null = 这次写入不进预算,不记行)
172
257
  * topics — 本次写入的确定性话题锚
173
258
  * repeat — {topic, gapMs} | null(g2 的命中面)
174
259
  * dup — {memory_id, archived, forgotten} | null(内容哈希的命中面)
175
260
  * exempt — null | "pinned"
261
+ * deny — null | {reason, kind?, label?}(第 1 级命中面)
262
+ *
263
+ * deny 非空但 decision 仍是 allow = 仅告警档(config.writeAdmission.enforce 关):
264
+ * 判定照跑、审计照留,写入不拦。读审计时「deny 非空且 decision=allow」就是这一档的
265
+ * 指纹,不需要再读配置才能解释一行。
266
+ *
267
+ * 第 1 级判定放在 pinned 豁免之前:pinned 豁免的是**预算与冷却**(constraint /
268
+ * preference 是 #249 第一批立的逐字保真池,不该被会话预算拦),不是「这条能不能落库」。
269
+ * 一条写着私钥的 constraint 仍然是私钥。
176
270
  * @param {{memory: object, sessionKey?: string|null}} input
177
271
  */
178
272
  function evaluate({ memory, sessionKey } = {}) {
179
- const verdict = { decision: "allow", gate: null, topics: [], repeat: null, dup: null, exempt: null };
273
+ const verdict = {
274
+ decision: ADMISSION_DECISION_ALLOW,
275
+ gate: null,
276
+ topics: [],
277
+ repeat: null,
278
+ dup: null,
279
+ exempt: null,
280
+ deny: null
281
+ };
180
282
  // 无会话身份 = 系统写入(dream / summarize / import / organize),不进预算。
283
+ // 第 1 级判定同样在这里之外:#164 A2 定的判据面含 autoSummarize / dream 输出,
284
+ // 那三处的接线是 A2 的落地范围(本模块跑的是会话内的 memory_save 路径)。
181
285
  if (!sessionKey) return verdict;
182
286
  // 审计关掉时不记行、也不推进话题表(见文件头:那个开关连启动期清理一起关掉)。
183
287
  if (config?.llmAudit?.enabled === false) return verdict;
288
+
289
+ const hit = config?.writeAdmission?.enabled === true ? firstLevelHit(memory) : null;
290
+ const judged = hit ? { ...verdict, deny: hit } : verdict;
291
+
292
+ // 命中且 enforce:决策已定,不再发候选集查询、不再比较话题(纯白花成本)。
293
+ if (hit && config?.writeAdmission?.enforce === true) {
294
+ return {
295
+ ...judged,
296
+ decision: ADMISSION_DECISION_DENY,
297
+ gate: ADMISSION_GATE_SESSION_BUDGET,
298
+ topics: extractTopicKeys(memory)
299
+ };
300
+ }
301
+
184
302
  const topics = extractTopicKeys(memory);
185
303
  const pinned = PINNED_MEMORY_TYPES.has(String(memory?.type ?? ""));
186
- if (pinned) return { ...verdict, gate: ADMISSION_GATE_SESSION_BUDGET, topics, exempt: "pinned" };
304
+ if (pinned) return { ...judged, gate: ADMISSION_GATE_SESSION_BUDGET, topics, exempt: "pinned" };
187
305
  const dup = lookupContentDup(memory);
188
306
  const table = topicTable(sessionKey);
189
307
  const at = now();
@@ -194,11 +312,11 @@ export function createWriteAdmission({ store, config, logger, now = Date.now } =
194
312
  // 同一行可能命中多个锚:取最近的那次(间隔最短=最有价值的那次重复)。
195
313
  if (!repeat || gapMs < repeat.gapMs) repeat = { topic, gapMs };
196
314
  }
197
- return { ...verdict, gate: ADMISSION_GATE_SESSION_BUDGET, topics, repeat, dup };
315
+ return { ...judged, gate: ADMISSION_GATE_SESSION_BUDGET, topics, repeat, dup };
198
316
  }
199
317
 
200
318
  /**
201
- * 把测量点落成审计行(一行一个新建行)。三个信号的读法:
319
+ * 把测量点与拒绝面落成审计行(一行一个新建行 / 一次被拒的写入)。四个信号的读法:
202
320
  * g1 会话写入预算:`GROUP BY session_key` 计数即得「会话内新建条数」分布;要剔
203
321
  * 掉穿透行(pinned 不进预算)就加 `json_extract(metadata,'$.exempt') IS NULL`。
204
322
  * g2 同话题冷却:`json_extract(metadata,'$.g2.gap_ms')` 即得「同话题新建行间隔」
@@ -207,6 +325,10 @@ export function createWriteAdmission({ store, config, logger, now = Date.now } =
207
325
  * dup 内容哈希:`json_extract(metadata,'$.dup.memory_id')` 非空即「这条新行的内容
208
326
  * 与某条既有行归一化后逐字节相同」;`$.dup.archived` 区分命中那条在活区还是
209
327
  * 归档区——归档区命中就是去重候选集漏掉的那一类穿透。
328
+ * deny 第 1 级命中:#254 验收第 2 条要的「被拒的写入可解释、不静默丢弃」就落在
329
+ * 这一项。三个键分工——`$.deny.reason` 是稳定判据键(blank / noise / sensitive),
330
+ * `$.deny.kind` 是密钥/PII 的子类,`$.deny.enforced` 区分「真拦下了」与「仅告警」。
331
+ * enforced=false 时这一行同时也是一个正常的 g1 行(写入确实发生了)。
210
332
  * 计量绝不影响写入:任何异常只 warn。
211
333
  * @returns {object[]} 落下的审计行(无测量点或失败时为空数组)
212
334
  */
@@ -214,6 +336,7 @@ export function createWriteAdmission({ store, config, logger, now = Date.now } =
214
336
  if (!sessionKey || !verdict || verdict.gate == null) return [];
215
337
  const at = now();
216
338
  const topics = Array.isArray(verdict.topics) ? verdict.topics : [];
339
+ const enforced = verdict.decision === ADMISSION_DECISION_DENY;
217
340
  const rows = [];
218
341
  try {
219
342
  rows.push(store.saveLlmAudit({
@@ -239,6 +362,24 @@ export function createWriteAdmission({ store, config, logger, now = Date.now } =
239
362
  archived: verdict.dup.archived === true,
240
363
  forgotten: verdict.dup.forgotten === true
241
364
  } }
365
+ : {}),
366
+ // enforced 由 decision 推出而不是另存一个入参:两者若各存一份,将来就会出现
367
+ // 「decision=deny 但 enforced=false」这种自相矛盾的行。
368
+ //
369
+ // decision 只在真有拒绝面时落盘:两个开关都关时,这条 metadata 必须与只计量
370
+ // 那一阶段逐字节一致(#254 验收第 1 条)。没有 deny 的行本来就默认是放行,
371
+ // 写一个恒为 "allow" 的键只是把「默认路径零行为变化」变成需要解释的事。
372
+ // 读法:有 deny 才有 decision,deny 非空且 decision=allow 就是仅告警档。
373
+ ...(verdict.deny
374
+ ? {
375
+ decision: verdict.decision,
376
+ deny: {
377
+ reason: verdict.deny.reason,
378
+ ...(verdict.deny.kind !== undefined ? { kind: verdict.deny.kind } : {}),
379
+ ...(verdict.deny.label !== undefined ? { label: verdict.deny.label } : {}),
380
+ enforced
381
+ }
382
+ }
242
383
  : {})
243
384
  }
244
385
  }));
@@ -247,8 +388,10 @@ export function createWriteAdmission({ store, config, logger, now = Date.now } =
247
388
  warn(`[dsh-mneme] write admission audit failed: ${String(e)}`);
248
389
  }
249
390
  // 只有真正落库、且不是穿透口的行才推进话题表:审计写失败时内存视图不能跑在审计
250
- // 前面(重启回填会得到另一条时间线),pinned 写入整个不在闸门里。
251
- if (rows.length > 0 && !verdict.exempt) {
391
+ // 前面(重启回填会得到另一条时间线),pinned 写入整个不在闸门里。被拦下的写入
392
+ // 同样不推进——它没进库,成为下一次 g2 的基准就会造出一条不存在的时间线。
393
+ // 仅告警档(deny 非空但 decision=allow)**要**推进:那条行确实落库了。
394
+ if (rows.length > 0 && !verdict.exempt && !enforced) {
252
395
  const table = topicTable(sessionKey);
253
396
  for (const topic of topics) table.set(topic, at);
254
397
  }
package/test/api.test.js CHANGED
@@ -667,8 +667,10 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
667
667
  // v0.8.5 新增 disableMemorySearch/disableMemoryArchive,
668
668
  // 本地嵌入池化新增 localEmbedPooling,issue #315 新增 summarizeReasoningEffort,
669
669
  // issue #239 第 4 项镜像到巩固新增 dreamPeakHours/dreamPeakMaxDeferMinutes,
670
- // issue #292 新增 autoDreamFailureBackoff)
671
- assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1);
670
+ // issue #292 新增 autoDreamFailureBackoff、
671
+ // issue #254 新增 writeAdmission.enabled/writeAdmission.enforce,
672
+ // issue #34 恢复(#333)新增 injectTimePrefix)
673
+ assert.equal(Object.keys(data.effective).length, 59 + 3 + 2 + 1 + 2 + 2 + 2 + 1 + 1 + 1 + 2 + 1 + 2 + 1);
672
674
  assert.equal(data.effective.dreamSkipInvalid, true);
673
675
  assert.equal(data.effective.allowCrossTypeMerge, false);
674
676
  assert.equal(data.effective.dreamMinIntervalMinutes, 0);
@@ -687,6 +689,9 @@ test("GET /api/dsh-mneme/features returns empty overrides and effective config d
687
689
  assert.equal(data.effective.reflectionFailureTracking, true);
688
690
  assert.equal(data.effective["memoryQualityFilter.enabled"], true);
689
691
  assert.equal(data.effective["llmAudit.enabled"], true);
692
+ // #254:写入准入两个键都默认关(默认路径零行为变化),面板可逐项启停。
693
+ assert.equal(data.effective["writeAdmission.enabled"], false);
694
+ assert.equal(data.effective["writeAdmission.enforce"], false);
690
695
  // 新增字符串 / URL / 枚举键
691
696
  assert.equal(data.effective.localEmbedModel, "Xenova/bge-small-zh-v1.5");
692
697
  assert.equal(data.effective.ollamaBaseUrl, "http://localhost:11434");
@@ -735,6 +740,12 @@ test("PUT /api/dsh-mneme/features round-trips nested, string, url and enum keys"
735
740
  const patch = {
736
741
  "memoryQualityFilter.enabled": false,
737
742
  "llmAudit.enabled": false,
743
+ // #254:新点号键必须同时进 settings 白名单**和** api.js 的 NESTED_FLAG_PATHS。
744
+ // 只加白名单时 configFlagValue("writeAdmission.enabled") 取的是 cfg 顶层的
745
+ // 同名字符串(不存在)→ effective 静默少键、PUT 存了也读不回来,且面板无感。
746
+ // 这里往返一次就是钉这件事的(计数锁只钉数量,钉不住值)。
747
+ "writeAdmission.enabled": true,
748
+ "writeAdmission.enforce": true,
738
749
  embedProvider: "local",
739
750
  ollamaBaseUrl: "http://127.0.0.1:11434",
740
751
  dreamProvider: " siliconflow ",
@@ -747,6 +758,8 @@ test("PUT /api/dsh-mneme/features round-trips nested, string, url and enum keys"
747
758
  assert.deepEqual(data.overrides, {
748
759
  "memoryQualityFilter.enabled": false,
749
760
  "llmAudit.enabled": false,
761
+ "writeAdmission.enabled": true,
762
+ "writeAdmission.enforce": true,
750
763
  embedProvider: "local",
751
764
  ollamaBaseUrl: "http://127.0.0.1:11434",
752
765
  dreamProvider: "siliconflow",
@@ -755,6 +768,8 @@ test("PUT /api/dsh-mneme/features round-trips nested, string, url and enum keys"
755
768
  // 嵌套键的覆盖值压过 bundle 配置的对象子字段
756
769
  assert.equal(data.effective["memoryQualityFilter.enabled"], false);
757
770
  assert.equal(data.effective["llmAudit.enabled"], false);
771
+ assert.equal(data.effective["writeAdmission.enabled"], true);
772
+ assert.equal(data.effective["writeAdmission.enforce"], true);
758
773
  assert.equal(data.effective.embedProvider, "local");
759
774
  assert.equal(data.effective.ollamaBaseUrl, "http://127.0.0.1:11434");
760
775
  assert.equal(data.effective.dreamProvider, "siliconflow");