@sema-agent/server 7.122.0 → 7.123.0

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 (69) hide show
  1. package/MIGRATION.md +77 -0
  2. package/README.md +5 -0
  3. package/README.zh-CN.md +3 -0
  4. package/USAGE.md +23 -5
  5. package/dist/audit.d.ts +1 -1
  6. package/dist/audit.js +52 -22
  7. package/dist/auto-mode-face.d.ts +12 -8
  8. package/dist/auto-mode-face.js +8 -7
  9. package/dist/boot/config-center.js +3 -0
  10. package/dist/config-catalog.js +1 -1
  11. package/dist/config-center/apply-effective.d.ts +1 -1
  12. package/dist/config-center/apply-effective.js +5 -3
  13. package/dist/config-center/http-client.js +3 -3
  14. package/dist/config-center/read-warnings.d.ts +56 -35
  15. package/dist/config-center/read-warnings.js +67 -19
  16. package/dist/config-center/types.d.ts +7 -8
  17. package/dist/config-lkg.d.ts +12 -9
  18. package/dist/config-lkg.js +38 -39
  19. package/dist/config-provider.d.ts +3 -2
  20. package/dist/config-provider.js +2 -2
  21. package/dist/config-types.d.ts +9 -1
  22. package/dist/config.d.ts +8 -8
  23. package/dist/config.js +69 -92
  24. package/dist/fleet/engine-signal.d.ts +54 -23
  25. package/dist/fleet/engine-signal.js +29 -5
  26. package/dist/fleet-client.d.ts +8 -6
  27. package/dist/fleet-client.js +4 -3
  28. package/dist/http/dispatch.js +5 -14
  29. package/dist/http/health-self-report.d.ts +3 -0
  30. package/dist/http/routes/capabilities.js +2 -1
  31. package/dist/http/routes/sessions.js +1 -4
  32. package/dist/http/stored-body-replay.d.ts +4 -3
  33. package/dist/http/stored-body-replay.js +2 -3
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +1 -0
  36. package/dist/leader/leader.d.ts +7 -6
  37. package/dist/leader/leader.js +3 -1
  38. package/dist/leader/run-entry.d.ts +2 -2
  39. package/dist/leader/run-entry.js +3 -3
  40. package/dist/leader/wire.d.ts +2 -2
  41. package/dist/model-entry-refusal.d.ts +18 -9
  42. package/dist/model-entry-refusal.js +19 -14
  43. package/dist/model-route-endpoint.d.ts +21 -1
  44. package/dist/model-route-endpoint.js +9 -3
  45. package/dist/model-route-gap.d.ts +103 -0
  46. package/dist/model-route-gap.js +80 -0
  47. package/dist/namespaced-rule-name.d.ts +16 -30
  48. package/dist/namespaced-rule-name.js +2 -20
  49. package/dist/observability/fail-open.d.ts +0 -4
  50. package/dist/observability/fail-open.js +0 -4
  51. package/dist/plugins/one-time-migrations.js +27 -0
  52. package/dist/plugins/session-policy-store-sql.js +1 -2
  53. package/dist/plugins/workflow-run-store-sql.d.ts +4 -2
  54. package/dist/rule-entry-list.d.ts +3 -13
  55. package/dist/rule-entry-list.js +1 -12
  56. package/dist/run-local.js +4 -4
  57. package/dist/runtime-governance.d.ts +31 -8
  58. package/dist/runtime-governance.js +7 -2
  59. package/dist/session-policy-wire.d.ts +47 -49
  60. package/dist/session-policy-wire.js +41 -36
  61. package/dist/session-sync.d.ts +5 -1
  62. package/dist/session-sync.js +2 -1
  63. package/dist/spec-fields.d.ts +4 -4
  64. package/dist/spec-fields.js +1 -4
  65. package/dist/task-settings.d.ts +4 -5
  66. package/dist/task-settings.js +5 -8
  67. package/dist/unattended-permission-mode.d.ts +3 -3
  68. package/dist/unattended-permission-mode.js +2 -2
  69. package/package.json +3 -3
@@ -1,32 +1,28 @@
1
1
  /**
2
- * 规则里的工具名**够不够得着**一只挂载工具 —— 本仓唯一的一问(判官 + 句子各一只),其余全是引擎判官 `judgeRuleToolName` 的判词。
2
+ * 规则里的工具名**够不够得着**一只挂载工具 —— 判官只有一个:引擎的 `judgeRuleToolName`。本文件只剩「规则文本里工具名那一段怎么取」与「判词怎么说」。
3
3
  *
4
- * 一个名字永远匹配不到工具的成因,判官是谁:
5
- * · 退役名 / 无协议前缀的 `__` 短名 / **协议名的对端段引擎铸不出**(`mcp__github.com__*`、`mcp__*`、`mcp____x`、`mcp__`)—— 引擎判官
6
- * (`retired` / `unprefixed_namespace` / `unmintable_segment`,带 `guidance` 与 1:1 `currentName`)。本仓不再拿 `mintNamespacePrefix` 自比对端段:
7
- * 那是同一问的第二个读者(core 7.43.0 #1355 (b) 起引擎自己给这个判词)。
8
- * · **工具段为空的协议名**(`mcp__srv__`)—— 引擎判官答 `current`、规则文法收下、allow 条目读成一条精确名行,而它一只工具都够不着
9
- * (引擎自己的两种拆法 —— 精确名 `ns.parse`、覆盖形 `isNamespacedCoveringRuleName` —— 都不认它)。这是引擎读不到的一形,本仓在下面点名的那些面上
10
- * 拒它({@link emptyToolSegmentOf}),句子在这里({@link describeEmptyToolSegment})。翻转条件:引擎判官给这一形一个判词 ⇒ 本文件整只退役。
4
+ * 一个名字永远匹配不到工具的成因,全是引擎判官的判词(`retired` / `unprefixed_namespace` / `unmintable_segment`,带 `guidance` 与 1:1 `currentName`):
5
+ * 退役名、无协议前缀的 `__` 短名、协议名的对端段引擎铸不出(`mcp__github.com__*`、`mcp__*`、`mcp____x`、`mcp__`),以及(core 7.48.0 #1400 起)
6
+ * **工具段为空**(`mcp__srv__` / `a2a__peer__`)。最后这一形 7.116.0–7.122.x 是本仓自答的(引擎判官当时答 `current`);引擎给了判词 ⇒ 本仓那一问
7
+ * (本文件那只判与它在会话规则写口、SQL 店写口、四张条目表上的调用)退役,零双读。
11
8
  *
12
- * **调它的面**(点名,不是「每个面」;判官一问与本仓这一问同调):
13
- * · 运维三张名表(`APPROVAL_REQUIRE` / `DENY` / `NEVER_AUTO`)与中心下发的同名表、请求 `settings.permissions.{ask,deny}`(经 `config.ts` 的 `unmatchableToolNamesRefusal`);
14
- * · 会话规则写口(`session-policy-wire.ts`)与 SQL 会话规则店的写口(`assertSessionRuleNamesReachable`);
15
- * · 引擎 allow 层的条目表:请求的 `settings.permissions.allow`、运维的 `APPROVAL_GRANT`、技能的两只清单 `allowedTools` / `disallowedTools`(顶层 `skills[]` 与子代
16
- * `agents[].skills[]` 两处受理口)—— 经 `rule-entry-list.ts` 的 `emptyToolSegmentEntryOf`(本仓这一问;退役名 / 铸不出的判词那一半由引擎 `compileAllowList` 给)。
9
+ * **调它的面**(点名,不是「每个面」):运维三张名表(`APPROVAL_REQUIRE` / `DENY` / `NEVER_AUTO`)与中心下发的同名表、请求
10
+ * `settings.permissions.{ask,deny}`(经 `config.ts` 的 `unmatchableToolNamesRefusal`)。会话规则写口直接调 core 的 `assertSessionRuleNamesCurrent`
11
+ * (同一只判官的店形);引擎 allow 层的条目表(请求 `settings.permissions.allow`、运维 `APPROVAL_GRANT`、技能两只清单)交引擎 `compileAllowList`
12
+ * (名册 = {@link NO_DECLARED_TOOL_NAMES};判官判非现役的条目读成 `tool_name`)。
17
13
  * **不调它的面**:`/v1/rules` 的持久规则写口与 `cc-import` 导入口(`http/routes/rules.ts`、`rules-consent.ts` 与两只持久规则店零判官调用)—— 如实未接,
18
- * 在那里写铸不出的服务器段、工具段为空的协议名仍被收下、永不命中;把它接上判官 = 另一件事(不在本文件里加特例;`unmatchableRuleNameOf` 就是现成的那一只)。
14
+ * 在那里写铸不出的服务器段仍被收下、永不命中;把它接上判官 = 另一件事(`unmatchableRuleNameOf` 就是现成的那一只)。
19
15
  * 子代的工具名表 `agents[].allowTools` / `denyTools` 不是本文件的面:core 7.45.0 #1377 / #1385 起名字归引擎在开跑前一只判(三来源同判,`spec-fields.ts`
20
16
  * 受理口只判形与条数),本仓不对它另做对照。
21
17
  * **精确名的工具段归一不在这里判**:包根没有导出工具段的铸法,不手抄 —— 精确名的工具段按字面收,本仓每张表上都不判它。
22
18
  *
23
- * 规则文本里「工具名那一段」怎么取也只有一种读法:{@link ruleEntryToolName}(配置表与技能清单同读)。
19
+ * 规则文本里「工具名那一段」怎么取也只有一种读法:{@link ruleEntryToolName}(配置表读它)。
24
20
  */
25
- import { type ProtocolNamespace, type RuleToolNameVerdict } from "@sema-agent/core";
21
+ import { type RuleToolNameVerdict } from "@sema-agent/core";
26
22
  /**
27
23
  * 一条**规则文本**里的工具名那一段(判官与旧名判据的主语)= core 包根 `parsePermissionRule` 的 `toolName`,原样 —— 引擎的技能编译器
28
24
  * (`compileSkillGrant`)与规则编译器读条目用的就是这一只:`Bash(npm:*)` ⇒ `Bash`;`mcp__srv(*)` / `mcp__srv()` / 带首尾空格的 `mcp__srv ` ⇒ 光杆名 `mcp__srv`。
29
- * 配置表的规则编译器消费者与技能的两只清单读同一只。不在这里另写解析、不回落整串:修前用 `parseRuleText`(只认 `工具(内容)` 形,光杆名解析不出就回落整串),
25
+ * 配置表的规则编译器消费者读它(技能两只清单的名字层 7.118.0 起交引擎的条目读法)。不在这里另写解析、不回落整串:修前用 `parseRuleText`(只认 `工具(内容)` 形,光杆名解析不出就回落整串),
30
26
  * 整串带着 `(*)` / 尾空格进判官 ⇒ 合法条目被判「铸不出」(修复验证轮 gpt-6 腿)。条目若是引擎文法本身就拒的形(配置表上 MCP 规则带括号:
31
27
  * `unsupported.mcp_paren`),拒由引擎的文法那一问说(配置表的判据链里它排在本判官之前),本判官不抢答。
32
28
  */
@@ -36,14 +32,11 @@ export declare function ruleEntryToolName(entry: string): string;
36
32
  * ⇒ 恒为空集。引擎判官、`compileAllowList` 的第三参、会话规则店同读这一只。
37
33
  */
38
34
  export declare const NO_DECLARED_TOOL_NAMES: ReadonlySet<string>;
39
- /** 一个规则名永远匹配不到工具的判词(闭集):引擎判官的三种非现行判词 ∪ 引擎读不到的那一形。 */
35
+ /** 一个规则名永远匹配不到工具的判词(闭集)= 引擎判官的非现行判词。 */
40
36
  export type UnmatchableRuleName = Exclude<RuleToolNameVerdict, {
41
37
  kind: "current";
42
- }> | {
43
- readonly kind: "empty_tool_segment";
44
- readonly ns: ProtocolNamespace;
45
- };
46
- /** 判:引擎判官先答(非 `current` ⇒ 原样交回),判官放行的再问引擎读不到的那一形;都过 ⇒ `undefined`。 */
38
+ }>;
39
+ /** 判:引擎判官非 `current` ⇒ 原样交回;`current` ⇒ `undefined`。 */
47
40
  export declare function unmatchableRuleNameOf(name: string): UnmatchableRuleName | undefined;
48
41
  /**
49
42
  * 判词 → 「为什么永远匹配不到 + 怎么写」(不复述条目本身 —— 调用方的句子已点名它)。引擎判词的 `guidance` 原样带出,是改法指引的**唯一**一句
@@ -51,11 +44,4 @@ export declare function unmatchableRuleNameOf(name: string): UnmatchableRuleName
51
44
  * 无前缀短名的出路从 core 的 `PROTOCOL_TABLE` 派生。闭集穷尽:判词加一种 ⇒ 编译红。
52
45
  */
53
46
  export declare function describeUnmatchableRuleName(v: UnmatchableRuleName, name: string): string;
54
- /**
55
- * 工具段为空的协议名(`<ns>__<peer>__`)⇒ 交回它的协议;别的名字 ⇒ `undefined`。判据只读引擎:判官答 `current`(对端段为空 / 铸不出的那些 ——
56
- * `mcp__`、`mcp____x` —— 是判官自己的判词,不在这里),而引擎自己的两种拆法(精确名 `ns.parse`、覆盖形 `isNamespacedCoveringRuleName`)都不认它。纯字符串运算。
57
- */
58
- export declare function emptyToolSegmentOf(name: string): ProtocolNamespace | undefined;
59
- /** {@link emptyToolSegmentOf} 的句子(「为什么永远匹配不到 + 怎么写」;不复述条目本身 —— 调用方的句子已点名它)。 */
60
- export declare function describeEmptyToolSegment(ns: ProtocolNamespace): string;
61
47
  //# sourceMappingURL=namespaced-rule-name.d.ts.map
@@ -1,4 +1,4 @@
1
- import { PROTOCOL_TABLE, isNamespacedCoveringRuleName, judgeRuleToolName, parsePermissionRule, protocolOf } from "@sema-agent/core";
1
+ import { PROTOCOL_TABLE, judgeRuleToolName, parsePermissionRule } from "@sema-agent/core";
2
2
  import { displayListEntry } from "./list-entry-display.js";
3
3
  export function ruleEntryToolName(entry) {
4
4
  return parsePermissionRule(entry).toolName;
@@ -6,10 +6,7 @@ export function ruleEntryToolName(entry) {
6
6
  export const NO_DECLARED_TOOL_NAMES = new Set();
7
7
  export function unmatchableRuleNameOf(name) {
8
8
  const verdict = judgeRuleToolName(name, NO_DECLARED_TOOL_NAMES);
9
- if (verdict.kind !== "current")
10
- return verdict;
11
- const ns = emptyToolSegmentOf(name);
12
- return ns === undefined ? undefined : { kind: "empty_tool_segment", ns };
9
+ return verdict.kind === "current" ? undefined : verdict;
13
10
  }
14
11
  export function describeUnmatchableRuleName(v, name) {
15
12
  switch (v.kind) {
@@ -19,25 +16,10 @@ export function describeUnmatchableRuleName(v, name) {
19
16
  return v.guidance;
20
17
  case "unprefixed_namespace":
21
18
  return `pre-prefix short form — the live name carries a protocol prefix: ${PROTOCOL_TABLE.map((e) => displayListEntry(`${e.prefix}${name}`)).join(" or ")}`;
22
- case "empty_tool_segment":
23
- return describeEmptyToolSegment(v.ns);
24
19
  default: {
25
20
  const unhandled = v;
26
21
  throw new Error(`unhandled rule name verdict: ${JSON.stringify(unhandled)}`);
27
22
  }
28
23
  }
29
24
  }
30
- export function emptyToolSegmentOf(name) {
31
- const ns = protocolOf(name);
32
- if (ns === undefined)
33
- return undefined;
34
- if (judgeRuleToolName(name, NO_DECLARED_TOOL_NAMES).kind !== "current")
35
- return undefined;
36
- if (ns.parse(name) !== undefined || isNamespacedCoveringRuleName(name))
37
- return undefined;
38
- return ns;
39
- }
40
- export function describeEmptyToolSegment(ns) {
41
- return `incomplete ${ns.id} name — the live form is ${ns.prefix}<server>__<tool>, or ${ns.prefix}<server> / ${ns.prefix}<server>__* to cover every tool on that server`;
42
- }
43
25
  //# sourceMappingURL=namespaced-rule-name.js.map
@@ -407,10 +407,6 @@ export declare const FAIL_OPEN_TAGS: {
407
407
  readonly cls: "F";
408
408
  readonly note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。";
409
409
  };
410
- readonly "server.config-lkg.landed-path-unresolved": {
411
- readonly cls: "P-DEBT";
412
- readonly note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。";
413
- };
414
410
  readonly "server.fleet.serving-lease-unconfirmed": {
415
411
  readonly cls: "R";
416
412
  readonly ruling: "C-R296";
@@ -367,10 +367,6 @@ export const FAIL_OPEN_TAGS = {
367
367
  cls: "F",
368
368
  note: "S-1195(7.119.0;`plugins/snapshot-bucket-presence.ts` 的 `ensureSnapshotBucket`,boot 期核一次快照桶 `SESSION_SNAPSHOT_BUCKET`;detail = `bucket=… endpoint=<buildDisplaySafeUrl 的显示形> <HTTP 状态 | request failed: 错类>`):presigned 列举对 `MINIO_ENDPOINT` **连不上 / 403 / 5xx / 没有 NoSuchBucket 的 404**,或探针到窗尾一次都没答过(detail 写 `probe still pending when the window ended`)⇒ **证明不了桶缺**,不拒启、照常起服(只有 404 + `NoSuchBucket` 才是「缺」,那一形窗内重试、到窗拒启,不走本 tag)。三问:①错误落到哪 —— 本 tag 的计数 / 探针行 / 记录器的一次性 warn,detail 带端点显示形与状态或错类;②谁受害 —— 桶其实不存在、或凭据不能列举的部署:之后每一次快照 offload 写与 SendUserFile 私有 presigned 请求会答 404 / 403,孤儿 sweep 每分钟一条失败,运维读到的是一串失败请求而不是「桶没建」(与 7.118.x 的既有形状同形,不是新损失);③缺席倒向哪边 —— 倒向**放行起服**:没有证据既不拒启、也不伪造「桶在场」或「桶缺」,不是门 / 权限 / 凭证。最坏后果 = 缺桶的部署晚一步才暴露;不放行的代价是对象存储一次抖动 / 一次 403 就让整只服务起不来。计数非零 = 这台部署启动时没能确认快照桶,按 detail 的端点与状态去核对网络 / 凭据 / 桶。",
369
369
  },
370
- "server.config-lkg.landed-path-unresolved": {
371
- cls: "P-DEBT",
372
- note: "7.120.0(`config-lkg.ts` 的 `landedPathOf`,`lkgReadDenyRows` 每次铸部署姿态座席时调;detail = `segment=<那一段的路径>`):`CONFIG_LKG_PATH` 覆写路径上有一段**目录在、却解不开**(悬空符号链接 / 链接环 / 读不了的目录:`realpath` 失败而 `lstat` 不说「不存在」)⇒ 这一段之下 LKG 真正会落在哪说不出来,读拒行只剩声明的拼写那一行(照旧在),真实落点那一行缺席。段**不存在**(`ENOENT` / `ENOTDIR`:还没建出来)是常态,不进这一臂;末段(文件本身)是悬空链接也不进(落盘的 rename 换掉的是那条目录项,文件就落在声明的拼写上)。归 P 的理由:这一行守的是 LKG 文件(含凭据),链接之后通了、LKG 写到链接目标下,模型按目标路径读得到它,直到重启重铸座席。归 DEBT 的理由:引擎没有「锚定单文件」的读拒行、座席只在 Runner 构造时铸一次,本服务说不出将来的落点;拿文件名在任意深度拒会把用户仓里同名文件一并挡掉。计数非零 = 去看 detail 点名的那一段(链接指向哪、卷挂了没有),修好后重启。",
373
- },
374
370
  "server.fleet.serving-lease-unconfirmed": {
375
371
  cls: "R",
376
372
  ruling: "C-R296",
@@ -162,6 +162,33 @@ export const ONE_TIME_MIGRATIONS = [
162
162
  "`AUTONOMY=plan` 的部署上按体里的原词重放(运维的 plan 不再被按段重钳)—— 升级前把这些停驻的 run 决掉 / 取消,或升级窗内设 `AUTONOMY=read-only`",
163
163
  reportKeys: [],
164
164
  },
165
+ {
166
+ id: "M13",
167
+ introducedIn: "7.123.0",
168
+ releasedOn: "2026-10-10",
169
+ removableAfter: "2026-11-09",
170
+ removal: "删 `config-lkg.ts` 的 formatVersion 2 读回迁移:`migrateV2Lkg` / `migrateDroppedIdentityRow` / `RETIRED_CATALOG_COORDINATE` 与其上的窗注;`loadLkg` 的 2 臂" +
171
+ "(版本判据里的 `2` 与 `formatVersion === 2 ? migrateV2Lkg(…)` 那一支 —— 2 回到「unknown formatVersion」拒)、返回型的 `migrated` 位与 JSDoc 里说它的那一行、" +
172
+ "文件头注「2 读回时一次性迁移」那一句;`boot/config-center.ts` 读 `read.migrated` 打 `config_lkg_migrated` 的那一行(formatVersion 1 迁移的同名事件那一行留);" +
173
+ "`migrateV1Lkg` 里不带 v1 的 etag 那一句旁注对 `migrateV2Lkg` 的引用(不带 etag 的规则本身留 —— v1 载荷同样没有完整的被丢条目记录);契约 §A.1d " +
174
+ "`droppedEntries` 一节「升级窗(如实)」那一条与 §A.1d `[config.model_refused_name_minted]` 那一条里「LKG 载体 `droppedIdentities` 读回时一次性迁移」那半句;手写 formatVersion 2 信封的测试夹具" +
175
+ "(`test/config-lkg.test.ts`、`test/config-hot/batch1-version-ordinal.test.ts` 改写成 3;`test/models-dropped-entries-s1295.test.ts` 的旧 LKG 迁移格改钉 2 读回不可用)",
176
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 2 答「unknown formatVersion 2」):2 读回 ⇒ 不可用,中心可达照拉(整拉,读腿铸出完整载体,新 LKG 按 3 覆盖)、" +
177
+ "断着则 env 兜底,下一份干净候选重写 —— 与回滚到 7.122.0 读 3 同一种处置",
178
+ reportKeys: [],
179
+ },
180
+ {
181
+ id: "M14",
182
+ introducedIn: "7.99.0",
183
+ releasedOn: "2026-09-24",
184
+ removableAfter: "2026-10-24",
185
+ removal: "删 `config-lkg.ts` 的 formatVersion 1 迁移:`legacyV1LkgPath` / `migrateV1Lkg` 与其上的窗注(`loadLkg` 的 1 臂今天已是拒句,不动);" +
186
+ "`boot/config-center.ts` `bootFromLkg` 里调用 `migrateV1Lkg` 的那一段(含它打 `config_lkg_migrated` / 推不出身份时响亮拒的那两行);" +
187
+ "`migrateV1Lkg` 的测试格(`test/config-lkg.test.ts` v1 迁移组)",
188
+ afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 1 答「pre-identity LKG (formatVersion 1) — not read back …」):旧默认路径上的 v1 文件不再被迁进箱、" +
189
+ "原样留在盘上;中心可达照拉(整拉,新 LKG 按现行箱名落盘)、断着则 env 兜底",
190
+ reportKeys: [],
191
+ },
165
192
  ];
166
193
  export function migrationWindowOpen(window, today) {
167
194
  return window.removableAfter === null || today <= window.removableAfter;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { assertSessionRuleNamesCurrent, loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
3
- import { assertSessionRuleNamesReachable, readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
3
+ import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
4
4
  import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText } from "./sql-json-column.js";
5
5
  import { pgHasUnstorable } from "./pg-safe-json.js";
6
6
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
@@ -105,7 +105,6 @@ export class SqlSessionPolicyStore {
105
105
  }
106
106
  const clean = normalizeRules(stripRev(rules));
107
107
  assertSessionRuleNamesCurrent(clean);
108
- assertSessionRuleNamesReachable(clean);
109
108
  if (prior && opts?.operator !== true) {
110
109
  const loosens = loosenReasons(prior, clean);
111
110
  if (loosens.length > 0) {
@@ -68,8 +68,10 @@ type WorkflowRun = Parameters<WorkflowRunStore["put"]>[1];
68
68
  export declare const MAX_RUN_BLOB_BYTES: number;
69
69
  declare const onWarnType: (msg: string, meta: Record<string, unknown>) => void;
70
70
  export type InboxWarn = typeof onWarnType;
71
- /** S-107:workflow_run 行的 `error`(脚本抛出的 `err.message`,core 源头不脱)在**两条写腿**(put / update 常态 + oversize 降级)
72
- * 同一口脱;`result` 由 core 完成时源头脱。幂等,不抛(超大 ⇒ 占位)。 */
71
+ /** S-107:workflow_run 行的 `error`(脚本抛出的 `err.message`)在**两条写腿**(put / update 常态 + oversize 降级)
72
+ * 同一口脱;`result` 由 core 完成时源头脱。幂等,不抛(超大 ⇒ 占位)。
73
+ * core 7.48.0 起 `error` 在铸点也过 core 的 `redactSecrets`,但两张脱敏表互不为超集(本仓的表多认几种记号,core 的表多认另几种)⇒ 这一口暂留;
74
+ * 退役条件 = core 的脱敏表成为两表的并集(本仓的每一种记号它都认)—— 到那时这一口与它的两条调用一起删。 */
73
75
  export declare function withRedactedError<T extends {
74
76
  error?: string | undefined;
75
77
  }>(run: T): T;
@@ -9,10 +9,10 @@
9
9
  * ① **形**({@link ruleEntryListShapeOk}):字符串数组。引擎逐条迭代;一个裸字符串会被逐字符读成几条(deny 侧 = 意图静默落空)。
10
10
  * 本谓词不带条数帽(引擎的 `screenAllowedToolsValue` 只判形);条数帽是另一只函数 {@link allowLayerEntryCountIssue}(`MAX_ALLOW_LAYER_ENTRIES`,
11
11
  * 请求 allow 桶、技能两只清单、运维表四张表同一只、同一句)。
12
- * ② **引擎读不到的一形**({@link emptyToolSegmentEntryOf}):工具段为空的协议名(`mcp__srv__`)—— 引擎的判官答 `current`、条目读成一条精确名行、永不命中;
13
- * 判据与句子在 `namespaced-rule-name.ts`(配置表、会话规则写口同一只)。四张表都拒。
12
+ * ② (7.116.0–7.122.x 的「引擎读不到的一形」—— 工具段为空的协议名 `mcp__srv__` 四张表都拒 —— 随 core 7.48.0 #1400 退役:引擎判官给它判词
13
+ * `unmintable_segment`,条目由下面 ③ 读成 `tool_name`,与对端段铸不出的名字同一条路。)
14
14
  * ③ **引擎读成空的条目**({@link inertRuleEntriesOf},判据 = 引擎包根 `compileAllowList(entries, behavior, 名册)`,名册 = 空集 `NO_DECLARED_TOOL_NAMES`
15
- * —— 条目的工具名也过引擎判官:退役名 / 无前缀短名 / 对端段铸不出读成 `tool_name`,带判词与 1:1 现名;原因词是引擎的 `ALLOW_ENTRY_INERT_REASONS`):
15
+ * —— 条目的工具名也过引擎判官:退役名 / 无前缀短名 / 对端段或工具段铸不出、为空读成 `tool_name`,带判词与 1:1 现名;原因词是引擎的 `ALLOW_ENTRY_INERT_REASONS`):
16
16
  * 运维的 allow 表拒启(运维自己的配置,写错了当场改);技能的拒侧(`disallowedTools`)拒 —— 提交 400、存量体重放 409(下面的 {@link skillDenyInertEntryOf});
17
17
  * 请求的 allow 侧(`settings.permissions.allow`、技能 `allowedTools`)不拒:引擎在开跑时发 `permission.allowed_tool_not_granted`(受众 = 用户)。
18
18
  * 技能拒侧那一拒与引擎 #1372(core 7.43.0:那只技能不激活)是同一件事的两处 —— 7.118.0 重估过、本版**不退**:本仓的续跑腿把引擎的
@@ -20,7 +20,6 @@
20
20
  * (重估读数与退役条件在下面那只函数头注)。
21
21
  */
22
22
  import { compileAllowList } from "@sema-agent/core";
23
- import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
24
23
  /** ① 形:字符串数组(空串也是字符串 —— 引擎把它读成 `grammar` 并通告 / 拒,见 ③)。 */
25
24
  export declare const ruleEntryListShapeOk: (v: unknown) => v is readonly string[];
26
25
  /**
@@ -42,15 +41,6 @@ export declare function allowLayerEntryCountIssue(field: string, list: readonly
42
41
  readonly count: number;
43
42
  readonly cap: number;
44
43
  } | undefined;
45
- /** 一张表里第一条「引擎读不到、却永远匹配不到」的条目(工具段为空的协议名)。 */
46
- export interface EmptyToolSegmentEntry {
47
- readonly entry: string;
48
- readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
49
- }
50
- /** ② 第一条工具段为空的协议名条目(没有 ⇒ `undefined`)。纯字符串运算。 */
51
- export declare function emptyToolSegmentEntryOf(list: readonly string[]): EmptyToolSegmentEntry | undefined;
52
- /** ② 的句子(`<field> names "<entry>", which can never match a tool (…)`;后半与配置表 / 会话规则写口同一只)。 */
53
- export declare function describeEmptyToolSegmentEntry(field: string, u: EmptyToolSegmentEntry): string;
54
44
  /** 一条引擎读成空的条目(引擎的形:原因词,名字层另带判词 / 1:1 现名;包根不导出这只具名型 ⇒ 从 `compileAllowList` 的返回型取,不手抄)。 */
55
45
  export type InertRuleEntry = ReturnType<typeof compileAllowList>["inert"][number];
56
46
  /** ③ 引擎按 `behavior` 读这张表时读成空的条目(引擎的整表读法:allow 表里的 `*` 让整表读成空;deny 表里的 `*` 只读空它自己);条目名过引擎判官(名册 = 空集)。 */
@@ -1,6 +1,6 @@
1
1
  import { compileAllowList } from "@sema-agent/core";
2
2
  import { displayListEntry } from "./list-entry-display.js";
3
- import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES, ruleEntryToolName } from "./namespaced-rule-name.js";
3
+ import { NO_DECLARED_TOOL_NAMES } from "./namespaced-rule-name.js";
4
4
  export const ruleEntryListShapeOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
5
5
  export const MAX_ALLOW_LAYER_ENTRIES = 1_000;
6
6
  export function describeAllowLayerOverCap(field, count) {
@@ -12,17 +12,6 @@ export function allowLayerEntryCountIssue(field, list) {
12
12
  return undefined;
13
13
  return { message: describeAllowLayerOverCap(field, list.length), count: list.length, cap: MAX_ALLOW_LAYER_ENTRIES };
14
14
  }
15
- export function emptyToolSegmentEntryOf(list) {
16
- for (const entry of list) {
17
- const ns = emptyToolSegmentOf(ruleEntryToolName(entry));
18
- if (ns !== undefined)
19
- return { entry, ns };
20
- }
21
- return undefined;
22
- }
23
- export function describeEmptyToolSegmentEntry(field, u) {
24
- return `${field} names ${displayListEntry(u.entry)}, which can never match a tool (${describeEmptyToolSegment(u.ns)})`;
25
- }
26
15
  export function inertRuleEntriesOf(list, behavior) {
27
16
  return compileAllowList(list, behavior, NO_DECLARED_TOOL_NAMES).inert;
28
17
  }
package/dist/run-local.js CHANGED
@@ -14,7 +14,7 @@ import { withUnattendedLeg } from "./unattended-permission-mode.js";
14
14
  import { buildUnattendedPolicyRefusal } from "./tool-approval.js";
15
15
  import { createBrain } from "./brain.js";
16
16
  import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, buildApprovalGrantNotice, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
17
- import { applyRuntimeGovernance, autonomyModeFloor } from "./runtime-governance.js";
17
+ import { applyRuntimeGovernance, autonomyModeFloor, entryPermissionMode } from "./runtime-governance.js";
18
18
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
19
19
  import { taskWallClockSec } from "./task-workflow.js";
20
20
  import { applyCatalogToSource } from "./capabilities/center-prompts.js";
@@ -344,7 +344,7 @@ export async function runLocal(argv, deps = {}) {
344
344
  for (const de of r.domainErrors ?? [])
345
345
  logger.warn("config_domain_invalid", { domain: de.domain, error: de.error, note: "domain fell to schema defaults; other domains applied normally" });
346
346
  for (const d of r.droppedEntries ?? [])
347
- logger.warn("config_entry_dropped", { domain: d.domain, entry: d.entry, error: d.error, version: r.effective.version, phase: "boot", note: "this one entry failed its domain schema and was dropped on its own — the rest of the candidate applies (per-entry tolerance); fix the named entry to bring it back" });
347
+ logger.warn("config_entry_dropped", { domain: d.domain, entry: d.entry, reasonCode: d.reasonCode, error: d.error, version: r.effective.version, phase: "boot", note: "this one entry failed its domain schema and was dropped on its own — the rest of the candidate applies (per-entry tolerance); fix the named entry to bring it back" });
348
348
  effective = r.effective;
349
349
  applyEffective(config, r.effective, logger, { leg: "boot" });
350
350
  if (effective?.mcp)
@@ -633,9 +633,9 @@ export async function runLocal(argv, deps = {}) {
633
633
  ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
634
634
  ...(maxTokens !== undefined ? { maxTokens } : {}),
635
635
  };
636
- const entryFloor = autonomyModeFloor(config.autonomy);
636
+ const entryWord = entryPermissionMode(undefined, autonomyModeFloor(config.autonomy)).effective;
637
637
  const spec = withUnattendedLeg(applyLocalGovernance({
638
- ...(entryFloor !== undefined ? { permissionMode: entryFloor } : {}),
638
+ ...(entryWord !== undefined ? { permissionMode: entryWord } : {}),
639
639
  objective: mention.cleanedObjective,
640
640
  sessionId,
641
641
  backgroundScope: "session",
@@ -90,7 +90,7 @@ export declare function compileCommandPolicyLayers(rules: CommandRule[] | undefi
90
90
  *
91
91
  * 🔴 **规则:按段重折的只有静态围栏,模式词不在这里。** 只读手 + roots、shell 地板没有出口 —— 每一段续跑都该在,所以每一段都折。
92
92
  * `plan` 是有出口的**生命周期词**(`plan_review` 把计划带出 plan),不是围栏:它是**提交入口的模式词地板**({@link autonomyModeFloor}),
93
- * 盖在每一个 spec 出生点一次:受理门盖进存储的提交体(`task-settings.ts` `stampEntryPermissionMode`)、run-local 盖在它自建的 spec 上、
93
+ * 盖在每一个 spec 出生点一次(词 = {@link entryPermissionMode} 的 `effective`):受理门盖进存储的提交体(`task-settings.ts` `stampEntryPermissionMode`)、run-local 盖在它自建的 spec 上、
94
94
  * leader run 盖在它出生时的入口体上(`leader/run-entry.ts`;入口词是 `plan` 的 leader run 进不了那条车道 —— worker 会话没有计划审阅通道 ⇒ 受理口响亮拒)。此后一条 run 的模式词
95
95
  * 只有存储体一个源,写者是每一个出生点(新的持久化写都经 `http/checkpoint-ctx.ts` `buildCheckpointCtx`)与 `plan_review` 写回。按段折一个模式词 = 第二个源:被批准的计划会在下一次挂起→续跑
96
96
  * 被拉回 plan。收紧入参的型上因此没有 `permissionMode`(`governance-overrides.ts`)。(上一个发布版本里本档不碰模式词,按段折的是只读手 +
@@ -118,18 +118,41 @@ export declare function autonomyOverrides(autonomy: Autonomy | undefined): Gover
118
118
  /**
119
119
  * 运维 autonomy 给**提交入口**的模式词地板 —— 唯一属主(规则全文见 {@link autonomyOverrides} 顶注)。`plan` ⇒ `"plan"`:每个新提交的 run
120
120
  * 以 plan 起步,请求的词按 core 的模式秩钳到它之下;其余三档与缺席 ⇒ 无地板(它们的表态全在按段的围栏里)。
121
- * 读者:三个 spec 出生点 —— 受理门(`http/admission.ts`,经 `task-settings.ts` `stampEntryPermissionMode` 盖进存储体)、run-local 自建的 spec、
122
- * leader run 的入口体(`leader/run-entry.ts` `leaderRunEntry`,同一只盖章;leader 的 HTTP 受理口用它判「这条车道收不收」);能力面的两位 ——
123
- * `taskWriteFaceOpen` 的治理半({@link autonomyKeepsOpenWriteFace})与 `permissionModeAuto.armed` 的意图(`auto-mode-face.ts`),两位都经 core
124
- * `clampPermissionMode` 读「地板把请求词钳成什么」,与盖章同一只钳;存量停驻体能否重放的第三问(`http/stored-body-replay.ts`:没有当前读法记号的行,
125
- * 体的生效词被此刻的地板钳会变 ⇒ 拒)。
121
+ * 「一个词在这块地板下变成什么」只有一只函数答:{@link entryPermissionMode}(读者全表在它的头注)。
126
122
  * 闭集**穷尽** switch:契约给 autonomy 加一档 ⇒ 这里编译红([ref])。
127
123
  */
128
124
  export declare function autonomyModeFloor(autonomy: Autonomy | undefined): PermissionMode | undefined;
125
+ /**
126
+ * 一个模式词进入一块地板之后的**读数** —— {@link entryPermissionMode} 的答。四键:调用方声明的词(`requested`,缺席 = 没声明)、地板(`floor`,
127
+ * 缺席 = 这里没有地板)、生效词(`effective`)与「地板改了调用方的词」(`clamped`,core 的同名位:只在声明了词、且那个词被折回地板时为真 ——
128
+ * 没声明词而取地板本身不算改词)。
129
+ */
130
+ export interface EntryPermissionMode<W extends PermissionMode | undefined = PermissionMode | undefined> {
131
+ readonly requested: W;
132
+ readonly floor: PermissionMode | undefined;
133
+ readonly effective: W | PermissionMode;
134
+ readonly clamped: boolean;
135
+ }
136
+ /**
137
+ * 「请求词 `word` 在地板 `floor` 下变成什么」—— **唯一**的一只函数(本仓对 core `clampPermissionMode` 的唯一调用点;秩只在 core 一处)。
138
+ *
139
+ * 规则:地板缺席 ⇒ 词原样、`clamped:false`;地板在场 ⇒ core `clampPermissionMode(word, floor)` 的答(词缺席 ⇒ 地板本身;比地板松的词 ⇒ 折回地板、`clamped:true`)。
140
+ * 本仓「地板」即 core 的天花板(`plan` 秩最低:比它松的词都折成它)。
141
+ *
142
+ * 读者(都调这一只,不另写「缺席原样否则钳」):
143
+ * · spec 出生点写 `effective` —— 受理门(`task-settings.ts` `stampEntryPermissionMode` 盖进存储体)、run-local 自建的 spec、
144
+ * leader run 的入口词(`leader/run-entry.ts` `leaderRunEntry`)、leader 车道合并口(`leader/leader.ts` `withLeaderLeg`:spec 的词钳在入口词之下)、
145
+ * 无人值守腿(`unattended-permission-mode.ts` `withUnattendedLeg`:`default` 钳在 spec 的词之下);
146
+ * · 「这个词会变成什么」的读者 —— 能力面写面开不开({@link autonomyKeepsOpenWriteFace}:bypass 声明过地板)、能力面 auto 读面
147
+ * (`auto-mode-face.ts` `judgeAutoModeArming`:意图过地板;`clamped` 且不是 auto ⇒ 未武装原因 `operator_floor`)、
148
+ * 存量停驻体能否按此刻的地板重放(`http/stored-body-replay.ts`:体的词经钳会变 ⇒ 拒)。
149
+ * `test/entry-permission-mode.test.ts` 按语法树钉:core 钳在 `src/` 只此一处调用,上面八处各调本函数一次。
150
+ */
151
+ export declare function entryPermissionMode<W extends PermissionMode | undefined>(word: W, floor: PermissionMode | undefined): EntryPermissionMode<W>;
129
152
  /**
130
153
  * 运维 autonomy 留不留得住请求层「掀开写围栏」那条声明(`permissionMode:"bypassPermissions"`,引擎模式表那一列 `writeFace:"open"`)——
131
- * 能力面 `taskWriteFaceOpen` 的治理那一半。判据读**两只属主本身**,与一条真提交走的路同序:bypass 声明先过入口地板(core
132
- * `clampPermissionMode`,与受理门的盖章同一只钳;地板缺席 ⇒ 原样),再交给本档的按段围栏(`tightenGoverned` → core `tightenTaskSpec`),
154
+ * 能力面 `taskWriteFaceOpen` 的治理那一半。判据读**两只属主本身**,与一条真提交走的路同序:bypass 声明先过入口地板({@link entryPermissionMode},
155
+ * 与受理门的盖章同一只函数),再交给本档的按段围栏(`tightenGoverned` → core `tightenTaskSpec`),
133
156
  * 看折出来的 spec 还开不开写面 —— 显式席 `"roots"` 胜过模式列(`read-only`),入口词被钳成别的词时读那个词的模式列(`plan`)。
134
157
  * 本仓不另写一张「哪几档钳写面」的表:档位怎么钳 / 怎么折由两只属主说,写面开不开由引擎的模式表说。
135
158
  */
@@ -121,9 +121,14 @@ export function autonomyModeFloor(autonomy) {
121
121
  }
122
122
  }
123
123
  }
124
+ export function entryPermissionMode(word, floor) {
125
+ if (floor === undefined)
126
+ return { requested: word, floor, effective: word, clamped: false };
127
+ const { effective, clamped } = clampPermissionMode(word, floor);
128
+ return { requested: word, floor, effective, clamped };
129
+ }
124
130
  export function autonomyKeepsOpenWriteFace(autonomy) {
125
- const floor = autonomyModeFloor(autonomy);
126
- const entry = floor === undefined ? "bypassPermissions" : clampPermissionMode("bypassPermissions", floor).effective;
131
+ const entry = entryPermissionMode("bypassPermissions", autonomyModeFloor(autonomy)).effective;
127
132
  const folded = tightenGoverned({ objective: "", permissionMode: entry }, autonomyOverrides(autonomy));
128
133
  return folded.writeFace !== "roots" && folded.permissionMode !== undefined && modePolicyOf(folded.permissionMode).writeFace === "open";
129
134
  }
@@ -29,13 +29,14 @@
29
29
  * 里同点同调它;准备阶段的名审计与本仓配置表读的是同一只 `judgeRuleToolName`)—— 本仓手写的那一只(`legacyToolNameOf`)删除,零双读;
30
30
  * 「哪几桶装工具名」归 core(它的 `HOLDS_TOOL_NAMES`)。
31
31
  *
32
- * ── 7.116.0:写口读者再多判一问 —— 够不着挂载工具的协议名;7.118.0 收窄到引擎读不到的那一形 ─────────────────────────
33
- * 对端段铸不出 / 对端段为空(`mcp__github.com__*`、`mcp__`)自 core 7.43.0 #1355 (b) 起是引擎判官自己的判词 `unmintable_segment` ——
34
- * `assertSessionRuleNamesCurrent` 拒(`legacy_tool_name`,`detail.verdict`)、本仓映射 `400 rules.legacy_tool_name`,与退役名同码同句族;本仓 7.116.0 起
35
- * 那只自比对端段的对照随之退役。引擎判官放行、却一只工具都够不着的只剩**工具段为空**(`mcp__srv__`):判据与句子是 `namespaced-rule-name.ts` 那一只
36
- * (配置表同调),码 `400 request.field_invalid`(会话规则写口既有的字段值码;不往 core 的判词闭集里塞本仓的词)。这一问要逐桶判,而 core 不导出
37
- * `HOLDS_TOOL_NAMES` 的运行期值 ⇒ 桶表 {@link TOOL_NAME_FIELDS} 的键集从 core 判官交回的 `field` 型派生(两向 tsc 配对),随引擎判官给这一形判词那天整只退役。
38
- * SQL 店写口经 {@link assertSessionRuleNamesReachable} 同判。
32
+ * ── 7.123.0(core 7.48.0):两问都只剩引擎的判 ──────────────────────────────────────────────────────────────────────
33
+ * ① **桶**能不能写:core 7.48.0 #1471 起 `normalizeRules` 对「不是一列非空字符串」的桶抛 `TypeError`(裸字符串、`null`、混入非字符串、`""` /
34
+ * 纯空白项),句子点名桶与值类型。写口读者逐桶问它({@link engineRuleListOf})⇒ 两条写口是唯一拒点(`400 request.field_invalid`,句子原样),
35
+ * 店里的 `normalizeRules` 对这两条写口的输入不再可能抛。修前本仓的形读者只判「字符串数组」,`[""]` / `[" "]` 过它、在店里抛 ⇒ 500。
36
+ * 本仓自己的「字符串数组」判只留在**读回**({@link readSessionPermissionRules}:SQL 店读出的行,读不出形 ⇒ `corrupt`)—— 读回与写口是两个问题,
37
+ * 读回不许判「引擎还肯不肯写」:7.47.x 及以前写下的空串项存量行必须读得出来(运营方清它、准备阶段照常执行它都要先读到它)。
38
+ * ② **名字**够不够得着工具:core 7.48.0 #1400 起工具段为空的协议名(`mcp__srv__`)也是引擎判官的判词 `unmintable_segment` ⇒ 本仓 7.116.0 起
39
+ * 自答的那一问(「引擎判官放行、却一只工具都够不着」)整只退役,写口拒码随之由 `request.field_invalid` 变 `rules.legacy_tool_name`。
39
40
  *
40
41
  * ── 文案也在这里(codex 一轮追加:两条腿各铸一份句子 = 第二份手抄)───────────────────────────────
41
42
  * 初版把 wire 文案留在两个路由各自铸,理由是错误文案冻结门(`test/api-error-text-freeze.test.ts`)锚的是
@@ -46,24 +47,9 @@
46
47
  */
47
48
  import { SessionPolicyError } from "@sema-agent/core";
48
49
  import type { SessionPermissionRules, SessionRuleNameRefusal, SessionRulesRecord } from "@sema-agent/core";
49
- import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
50
50
  /** core `SessionPermissionRules` 的桶名全集(派生,不是手抄)。 */
51
51
  export declare const SESSION_PERMISSION_RULE_FIELDS: readonly (keyof SessionPermissionRules)[];
52
- /** 工具名桶里一条引擎判官放行、却一只工具都够不着的协议名(工具段为空);`ns` = 它的协议。 */
53
- export interface SessionRuleUnreachableName {
54
- readonly field: SessionRuleNameRefusal["field"];
55
- readonly name: string;
56
- readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
57
- }
58
- /** 逐桶逐条问引擎读不到的那一形;第一条交回,全部够得着 ⇒ `undefined`。调用方先过引擎判官({@link legacyToolNameRefusalOf} / `assertSessionRuleNamesCurrent`)。 */
59
- export declare function unreachableSessionRuleNameOf(rules: SessionPermissionRules): SessionRuleUnreachableName | undefined;
60
- /**
61
- * SQL 会话规则店写口的同一问(core 判官之后、收紧门之前,与 `assertSessionRuleNamesCurrent` 同点):够不着 ⇒ 抛,一条写语句都不发。
62
- * 抛的是普通 `Error`(不是 `SessionPolicyError`:core 那张闭集里没有这个词,也不往里塞)—— 本服务的两条 HTTP 写口在读者那一层先拒,
63
- * 走到这里 = 有一个绕过写口读者的写者,HTTP 上落 500(服务端自身的失误,响亮)。
64
- */
65
- export declare function assertSessionRuleNamesReachable(rules: SessionPermissionRules): void;
66
- /** 一条规则体的**形**问题。判词而非文案(见文件头注的「文案属主」一段)。 */
52
+ /** **读回**的形问题(SQL 店读出的行;判词而非文案,见文件头注的「文案属主」一段)。 */
67
53
  export type SessionPermissionRulesShapeProblem =
68
54
  /** 整体不是一只普通对象(`null` / 数组 / 标量 / 缺席)。 */
69
55
  {
@@ -74,18 +60,23 @@ export type SessionPermissionRulesShapeProblem =
74
60
  kind: "field-not-string-array";
75
61
  field: keyof SessionPermissionRules;
76
62
  };
77
- /** 一条**要写进店**的规则体的问题 = 形问题 ∪ S-800 的名字问题(判官 = core 的 `assertSessionRuleNamesCurrent`)。 */
78
- export type SessionPermissionRulesProblem = SessionPermissionRulesShapeProblem
79
- /** S-800 / S-1047:形对,但工具名桶里有一条写进去就跑不了的名字;`refusal` = core 判官交回的 detail(原样上 wire)。 */
63
+ /** 一条**要写进店**的规则体的问题 —— 三问都只有一个判官:容器形(本仓)、桶(引擎 `normalizeRules`)、名字(引擎 `assertSessionRuleNamesCurrent`)。 */
64
+ export type SessionPermissionRulesWriteProblem = {
65
+ kind: "not-an-object";
66
+ }
67
+ /** 引擎不肯把这一桶写成规则表(core 7.48.0 #1471);`refusal` = 引擎的句子(点名桶与值类型),原样上 wire。 */
80
68
  | {
81
- kind: "legacy-tool-name";
82
- refusal: SessionRuleNameRefusal;
69
+ kind: "field-not-rule-list";
70
+ field: keyof SessionPermissionRules;
71
+ refusal: string;
83
72
  }
84
- /** 形对、core 判官也放行,但工具名桶里有一条一只工具都够不着的协议名(工具段为空 —— 引擎读不到的那一形)。 */
73
+ /** S-800 / S-1047:桶形对,但工具名桶里有一条引擎判官判非现役的名字;`refusal` = core 判官交回的 detail(原样上 wire)。 */
85
74
  | {
86
- kind: "unreachable-tool-name";
87
- unreachable: SessionRuleUnreachableName;
75
+ kind: "legacy-tool-name";
76
+ refusal: SessionRuleNameRefusal;
88
77
  };
78
+ /** 两只读者的判词并集(句子铸点 {@link describeSessionPermissionRulesProblem} 读它)。 */
79
+ export type SessionPermissionRulesProblem = SessionPermissionRulesShapeProblem | SessionPermissionRulesWriteProblem;
89
80
  /** 两只读者的读数:有问题给判词,没问题给**已收窄**的规则体(调用方零 cast)。`P` = 这只读者会给的判词集。 */
90
81
  export type SessionPermissionRulesRead<P extends SessionPermissionRulesProblem = SessionPermissionRulesProblem> = {
91
82
  problem: P;
@@ -95,27 +86,34 @@ export type SessionPermissionRulesRead<P extends SessionPermissionRulesProblem =
95
86
  rules: SessionPermissionRules;
96
87
  };
97
88
  /**
98
- * **形读者**:读一只 `SessionPermissionRules`,形不对给判词,形对给**只含五桶**的规则体。三个消费者:两条写入腿
99
- * (经 {@link readWritableSessionPermissionRules})与 SQL 店的**读回**(`session-policy-store-sql.ts` 的
100
- * `readSessionRulesBody`:读不出形的行判 `corrupt`)。
89
+ * **读回**的形读者:读一只 `SessionPermissionRules`,形不对给判词,形对给**只含五桶**的规则体。消费者 = SQL 店的读回
90
+ * (`session-policy-store-sql.ts` 的 `readSessionRulesBody`:读不出形的行判 `corrupt`)。
101
91
  *
102
- * 认识面窄得刻意:**只判形**,不判内容(桶内字符串的 pattern 文法、路径是否可达、tighten-only 是否成立
103
- * 都不在这里 —— 前两者归 core 的 rule 文法与 `normalizeRules`,后者归 `putRules` 的收紧门;S-800 的名字判官
104
- * 只在**写口**那一只读者里,见下)。未知键**照旧忽略**(不是新的拒因):core 的 `normalizeRules` 本来就只搬这五桶,
105
- * 本读者与它同口径。
92
+ * 认识面窄得刻意:**只判「五桶各是字符串数组」**,不判引擎还肯不肯写(空串 / 纯空白项)、不判名字 —— 那是写口的问题
93
+ * ({@link readWritableSessionPermissionRules})。7.47.x 及以前写下的空串项存量行必须读得出:读回把它判坏 ⇒ 运营方清它的 PUT 在写前快照上 500、
94
+ * 准备阶段也读不到它(同 S-800 的「判官只在写口」)。未知键**照旧忽略**:core 的 `normalizeRules` 本来就只搬这五桶,本读者与它同口径。
106
95
  */
107
96
  export declare function readSessionPermissionRules(raw: unknown): SessionPermissionRulesRead<SessionPermissionRulesShapeProblem>;
108
97
  /**
109
- * **写口读者**(S-800)= 形读者 + 名字判官:两条写入腿(`PUT /v1/sessions/:id/policy`、sync/import 的 `policy`
110
- * 面经 {@link readSessionRulesRecords})往会话规则店写之前的**唯一**判据。两问,按序:
111
- * ① 形(同 {@link readSessionPermissionRules};一份坏形的体答形的 400);
112
- * ② 工具名桶里的每一条过 core 的判官({@link legacyToolNameRefusalOf})—— 写进去就让会话之后每一跑都起不来的名字,在写口拒;
113
- * ③ 工具名桶里的每一条问引擎读不到的那一形({@link unreachableSessionRuleNameOf}:工具段为空的协议名)—— 写进去永远匹配不到任何挂载工具,在写口拒。
114
- * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行(或够不着的协议名行)必须**读得出来** ——
115
- * 那是运营方清掉它的唯一路径(operator 的 PUT 先读写前快照),也是 core 准备阶段报 `config.legacy_tool_name`
116
- * 而不是一只 500 `corrupt` 的前提。把判官放进形读者会让读回把它判坏(7.105.0 车QQ 双库套件实抓,修在同批)。
98
+ * 一份规则体(写口的入参,或店里读出来的一行)里**引擎不肯写**的第一只桶 —— 没有 ⇒ `undefined`。判官同 {@link engineRuleListOf}。
99
+ * 两个读者:写口读者(拒)与 2c 重放的同内容判据(`session-sync.ts` 的 `sameRulesContent`:目的端存量行是这一形 ⇒ 答「不同内容」,写交给店的收紧门)。
100
+ */
101
+ export declare function unwritableRuleListOf(rules: Readonly<Partial<Record<keyof SessionPermissionRules, unknown>>>): {
102
+ field: keyof SessionPermissionRules;
103
+ refusal: string;
104
+ } | undefined;
105
+ /**
106
+ * **写口读者**(S-800)—— 两条写入腿(`PUT /v1/sessions/:id/policy`、sync/import 的 `policy` 面经 {@link readSessionRulesRecords})往会话规则店
107
+ * 写之前的**唯一**判据,三问按序、每问一个判官:
108
+ * ① 容器形 —— 一只普通对象(本仓);
109
+ * ② 每一只桶 —— 引擎肯不肯把它写成规则表({@link engineRuleListOf},core 7.48.0 #1471;与引擎店同序:先 `normalizeRules` 再判名);
110
+ * ③ 工具名桶里的每一条 —— 引擎判官({@link legacyToolNameRefusalOf}:退役名 / 无前缀 `__` 名 / 对端段或工具段铸不出、为空)。
111
+ * 拿到规则体 = 店里的 `normalizeRules` 与 `assertSessionRuleNamesCurrent` 对它都不会再抛(同一只判官先问过)。
112
+ * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形({@link readSessionPermissionRules})。早先写进去的退役名行 / 空串项行必须**读得出来** ——
113
+ * 那是运营方清掉它的唯一路径(operator 的 PUT 先读写前快照),也是 core 准备阶段报 `config.legacy_tool_name` 而不是一只 500 `corrupt` 的前提
114
+ * (7.105.0 车QQ 双库套件实抓,修在同批)。
117
115
  */
118
- export declare function readWritableSessionPermissionRules(raw: unknown): SessionPermissionRulesRead;
116
+ export declare function readWritableSessionPermissionRules(raw: unknown): SessionPermissionRulesRead<SessionPermissionRulesWriteProblem>;
119
117
  /** 一条 `SessionRulesRecord` 的形问题(`policy[]` 的元素级;桶级问题原样转载在 `rules` 位)。 */
120
118
  export type SessionRulesRecordProblem =
121
119
  /** `policy` 本身不是数组。 */
@@ -133,11 +131,11 @@ export type SessionRulesRecordProblem =
133
131
  kind: "principal-not-a-string";
134
132
  index: number;
135
133
  }
136
- /** 第 `index` 条的 `rules` 形不对(转载桶级判词)。 */
134
+ /** 第 `index` 条的 `rules` 写不进(转载写口读者的判词)。 */
137
135
  | {
138
136
  kind: "rules";
139
137
  index: number;
140
- problem: SessionPermissionRulesProblem;
138
+ problem: SessionPermissionRulesWriteProblem;
141
139
  };
142
140
  /** {@link readSessionRulesRecords} 的读数。 */
143
141
  export type SessionRulesRecordsRead = {