@sema-agent/server 7.116.1 → 7.118.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 (131) hide show
  1. package/MIGRATION.md +195 -0
  2. package/USAGE.md +65 -41
  3. package/dist/approval-card.d.ts +82 -18
  4. package/dist/approval-card.js +46 -4
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/budget-tracing.js +2 -3
  8. package/dist/boot/config-center.d.ts +2 -2
  9. package/dist/boot/config-center.js +5 -7
  10. package/dist/boot/parked-revive-gate.d.ts +1 -7
  11. package/dist/boot/parked-revive-gate.js +2 -12
  12. package/dist/boot/resolve-spec.js +18 -16
  13. package/dist/boot/runner-deps.d.ts +5 -3
  14. package/dist/boot/runner-deps.js +6 -4
  15. package/dist/boot/stage-07-capability-layer.js +2 -2
  16. package/dist/brain.d.ts +17 -8
  17. package/dist/brain.js +10 -7
  18. package/dist/capabilities/hands-lane.d.ts +25 -0
  19. package/dist/capabilities/hands-lane.js +1 -1
  20. package/dist/capabilities/mcp-probe.d.ts +2 -2
  21. package/dist/config-catalog.js +3 -2
  22. package/dist/config-center/apply-effective.d.ts +6 -16
  23. package/dist/config-center/apply-effective.js +0 -6
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/restart-signal.d.ts +1 -1
  27. package/dist/config-types.d.ts +12 -16
  28. package/dist/config.d.ts +32 -29
  29. package/dist/config.js +20 -27
  30. package/dist/deployment-governance.d.ts +13 -19
  31. package/dist/deployment-governance.js +7 -14
  32. package/dist/governance-overrides.d.ts +20 -0
  33. package/dist/governance-overrides.js +8 -0
  34. package/dist/host-decision.d.ts +83 -25
  35. package/dist/host-decision.js +31 -1
  36. package/dist/http/admission.js +13 -7
  37. package/dist/http/approval-sweeps.js +16 -13
  38. package/dist/http/checkpoint-ctx.d.ts +83 -24
  39. package/dist/http/checkpoint-ctx.js +68 -8
  40. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  41. package/dist/http/mcp-dial-deadline.js +21 -0
  42. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  43. package/dist/http/memory-compliance-failure-reply.js +17 -8
  44. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  45. package/dist/http/memory-store-refusal-reply.js +16 -5
  46. package/dist/http/resume-legs.js +18 -18
  47. package/dist/http/retired-skill-manifest.d.ts +1 -1
  48. package/dist/http/route-ctx.d.ts +3 -3
  49. package/dist/http/routes/approvals-assistant.js +21 -24
  50. package/dist/http/routes/capabilities.js +5 -3
  51. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  52. package/dist/http/routes/mcp-reconnect.js +6 -1
  53. package/dist/http/routes/memory-bundle.js +1 -1
  54. package/dist/http/routes/memory-compliance.js +1 -1
  55. package/dist/http/routes/memory-origin.js +1 -1
  56. package/dist/http/routes/runs.js +53 -33
  57. package/dist/http/routes/sessions.d.ts +1 -1
  58. package/dist/http/routes/sessions.js +3 -6
  59. package/dist/http/routes/workflows.js +2 -0
  60. package/dist/http/wire-types.d.ts +6 -1
  61. package/dist/memory-store-refusal.d.ts +42 -17
  62. package/dist/memory-store-refusal.js +25 -6
  63. package/dist/model-select.d.ts +16 -5
  64. package/dist/model-select.js +7 -3
  65. package/dist/model-unreachable-notice.d.ts +1 -1
  66. package/dist/namespaced-rule-name.d.ts +45 -39
  67. package/dist/namespaced-rule-name.js +36 -22
  68. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  69. package/dist/observability/corrupt-read-seat.js +1 -0
  70. package/dist/observability/fail-open.d.ts +15 -3
  71. package/dist/observability/fail-open.js +15 -3
  72. package/dist/observability/permission-rule-events.d.ts +3 -0
  73. package/dist/observability/permission-rule-events.js +4 -0
  74. package/dist/observability/run-terminal.js +21 -1
  75. package/dist/operator-ask.d.ts +4 -1
  76. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  77. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  78. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  79. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  80. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  81. package/dist/orchestration/workflow-notify-journal.js +7 -2
  82. package/dist/outbound-dispatcher.d.ts +47 -20
  83. package/dist/outbound-dispatcher.js +198 -23
  84. package/dist/parked-decide.d.ts +43 -48
  85. package/dist/parked-decide.js +54 -16
  86. package/dist/permission-rule-list.d.ts +20 -31
  87. package/dist/permission-rule-vocab.d.ts +4 -3
  88. package/dist/permission-rule-vocab.js +1 -1
  89. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  90. package/dist/plugins/approval-ask-store-sql.js +0 -3
  91. package/dist/plugins/one-time-migrations.d.ts +25 -0
  92. package/dist/plugins/one-time-migrations.js +192 -6
  93. package/dist/plugins/pg-pool.js +1 -1
  94. package/dist/plugins/tidb-pool.js +1 -1
  95. package/dist/plugins/web-search.d.ts +20 -1
  96. package/dist/plugins/web-search.js +39 -10
  97. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  98. package/dist/plugins/workflow-run-store-sql.js +5 -7
  99. package/dist/request-key-closure.js +1 -1
  100. package/dist/request-value-closure.d.ts +1 -1
  101. package/dist/router/route-orchestration.d.ts +2 -23
  102. package/dist/rule-entry-list.d.ts +88 -0
  103. package/dist/rule-entry-list.js +102 -0
  104. package/dist/rules-consent.js +3 -0
  105. package/dist/run-local.d.ts +4 -2
  106. package/dist/run-local.js +4 -3
  107. package/dist/runs.d.ts +1 -1
  108. package/dist/runs.js +1 -0
  109. package/dist/runtime-governance.d.ts +5 -21
  110. package/dist/runtime-governance.js +4 -8
  111. package/dist/session-policy-wire.d.ts +13 -12
  112. package/dist/session-policy-wire.js +11 -6
  113. package/dist/spec-fields.d.ts +13 -24
  114. package/dist/spec-fields.js +13 -17
  115. package/dist/standing-answer-grant.d.ts +14 -2
  116. package/dist/standing-answer-grant.js +8 -1
  117. package/dist/task-settings.d.ts +18 -18
  118. package/dist/task-settings.js +63 -32
  119. package/dist/tool-approval.d.ts +77 -145
  120. package/dist/tool-approval.js +80 -62
  121. package/dist/trace/core-keyset-guard.d.ts +6 -6
  122. package/dist/trace/engine-notice-wire.d.ts +4 -6
  123. package/dist/trace/engine-notice-wire.js +14 -1
  124. package/dist/trace/self-minted-wire.d.ts +16 -2
  125. package/dist/trace/self-minted-wire.js +7 -0
  126. package/dist/user-input-ledger.d.ts +24 -12
  127. package/dist/user-input-ledger.js +33 -14
  128. package/package.json +2 -2
  129. package/scripts/drain-check-local.mjs +103 -7
  130. package/scripts/drain-check.mysql.sql +59 -8
  131. package/scripts/drain-check.pg.sql +66 -10
@@ -1,4 +1,4 @@
1
- import { readRunParks } from "./orchestration/workflow-agent-session-index.js";
1
+ import { routeWorkflowPark } from "./orchestration/workflow-agent-session-index.js";
2
2
  import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
3
3
  import { backgroundScopesForCheckpointScope, decodeCheckpointScope } from "./security.js";
4
4
  import { ASK_USER_QUESTION_TOOL_NAME } from "./approval-content-kind.js";
@@ -20,19 +20,30 @@ export async function findParkOrigin(sources, cp) {
20
20
  if (index === undefined || runStore === undefined)
21
21
  return undefined;
22
22
  try {
23
- const row = await index.findByAgentSession(cp.sessionId);
24
- if (row === null)
23
+ const pointers = await index.listByAgentSession(cp.sessionId);
24
+ if (pointers.length === 0)
25
25
  return undefined;
26
- const run = await runStore.get(row.runId);
27
- if (run === null)
28
- return undefined;
29
- const parks = readRunParks(run);
30
- if (parks === undefined)
31
- return { kind: "workflow_parks_unreadable", runId: row.runId, run };
32
- const park = parks.find((p) => p.sessionId === cp.sessionId && p.token === cp.token);
33
- if (park === undefined)
34
- return undefined;
35
- return { kind: "workflow", runId: row.runId, run, park };
26
+ const runs = [];
27
+ for (const runId of new Set(pointers.map((p) => p.runId))) {
28
+ const run = await runStore.get(runId);
29
+ if (run !== null)
30
+ runs.push(run);
31
+ }
32
+ const route = routeWorkflowPark(cp.sessionId, runs);
33
+ switch (route.kind) {
34
+ case "held":
35
+ return route.park.token === cp.token ? { kind: "workflow", runId: route.runId, run: route.run, park: route.park } : undefined;
36
+ case "forked":
37
+ return { kind: "workflow_park_forked", runIds: route.runIds };
38
+ case "unproven":
39
+ return { kind: "workflow_parks_unreadable", runId: route.runId, run: route.run, cause: route.cause, ...(route.readableHolderRunId !== undefined ? { readableHolderRunId: route.readableHolderRunId } : {}) };
40
+ case "none":
41
+ return undefined;
42
+ default: {
43
+ const unhandled = route;
44
+ throw new Error(`unhandled workflow park route: ${JSON.stringify(unhandled)}`);
45
+ }
46
+ }
36
47
  }
37
48
  catch (e) {
38
49
  quiet(sources.warn, "workflow_park_probe_failed", { error: e instanceof Error ? e.message : String(e) });
@@ -90,12 +101,39 @@ export function buildParkedResumeOutcome(req) {
90
101
  };
91
102
  }
92
103
  export function denyWorkflowParksUnreadable(origin) {
104
+ switch (origin.cause) {
105
+ case "older_engine":
106
+ return {
107
+ status: 409,
108
+ body: {
109
+ error: "this decision cannot be matched to the pending step it belongs to: the run's record was written by an earlier engine version, which did not record which run is waiting on each pending step — and that record is the only proof that the approval you are deciding belongs to this run. The decision is therefore NOT delivered, rather than answered with a receipt nothing can redeem: the approval card stays pending and nothing is forged. Resume the run itself instead — `Workflow({resumeFromRunId})` with the runId below — and once the resumed run is waiting on this step again it records itself as the one waiting, after which deciding here works normally. Only if the engine also refuses that resume is the run truly unrecoverable",
110
+ errorCode: "decide.workflow_parks_unreadable",
111
+ runId: origin.runId,
112
+ },
113
+ };
114
+ case "unreadable":
115
+ return {
116
+ status: 409,
117
+ body: {
118
+ error: "this decision cannot be matched to the pending step it belongs to: this run's record cannot be read (the list of pending steps the run owns is damaged: one of its entries is malformed, or it is not a list at all) — and that list is the only proof that the approval you are deciding belongs to this run. The decision is therefore NOT delivered, rather than answered with a receipt nothing can redeem: the approval card stays pending and nothing is forged. Resuming does not clear this: the damaged record keeps naming this step whatever runs after it, so the decision stays undeliverable until whoever operates this deployment repairs that record (the runId below); after that, deciding here works normally. When the response also carries readableHolderRunId, that run's record is readable and is the latest one waiting on this step — it is where the work can be carried on, but the damaged record above still keeps this decision from being delivered until that record is fixed",
119
+ errorCode: "decide.workflow_parks_unreadable",
120
+ runId: origin.runId,
121
+ ...(origin.readableHolderRunId !== undefined ? { readableHolderRunId: origin.readableHolderRunId } : {}),
122
+ },
123
+ };
124
+ default: {
125
+ const unhandled = origin.cause;
126
+ throw new Error(`unhandled workflow parks-unreadable cause: ${String(unhandled)}`);
127
+ }
128
+ }
129
+ }
130
+ export function denyWorkflowParkForked(origin) {
93
131
  return {
94
132
  status: 409,
95
133
  body: {
96
- error: "this decision cannot be matched to the pending step it belongs to: this run's record was written by an older engine, or one of its entries is damaged, so it no longer says which pending steps the run owns — and that list is the only proof that the approval you are deciding belongs to this run. The decision is therefore NOT delivered, rather than answered with a receipt nothing can redeem: the approval card stays pending and nothing is forged. Resume the run itself instead — `Workflow({resumeFromRunId})` with the runId below — and the engine rebuilds that list from its own records as it admits the resume, after which deciding here works normally. Only if the engine also refuses that resume is the run truly unrecoverable",
97
- errorCode: "decide.workflow_parks_unreadable",
98
- runId: origin.runId,
134
+ error: "this decision cannot be delivered: the same run was resumed more than once and every resumed copy listed in runIds is waiting on this same step, so there is no single run to hand the decision to. Nothing was delivered and the approval card stays pending. Resume one of the runs listed in runIds; once that run is waiting on this step again, deciding here works normally",
135
+ errorCode: "decide.workflow_park_forked",
136
+ runIds: [...origin.runIds],
99
137
  },
100
138
  };
101
139
  }
@@ -1,25 +1,23 @@
1
1
  /**
2
- * **规则表的唯一编译口** —— 一张工具名规则表(`allow` / `ask` / `deny` 之一)编成一只 `ToolPolicy`。
2
+ * **规则表的唯一编译口** —— 一张工具名规则表(`ask` / `deny` 之一)编成一只 `ToolPolicy`。读者:请求腿的 `settings.permissions.{ask,deny}`
3
+ * 两桶(`task-settings.ts` 的 `deriveSettingsPolicy`)。
3
4
  *
4
- * 两类读者,同一只编译口(编译与校验各只有一份,不在别处另写第二份文法):
5
- * · 请求腿的 `settings.permissions.{allow,ask,deny}` 三桶(`task-settings.ts` 的 `deriveSettingsPolicy`);
6
- * · 运维的 allow 规则 `APPROVAL_GRANT`(`runtime-governance.ts` 的 `applyRuntimeGovernance`,经部署治理链的单一构造口
7
- * `deployment-governance.ts` 交进来 —— HTTP 提交 / durable 续跑 / 停驻后台 agent 的跨副本赎回 / run-local 四条腿同装)。
5
+ * 🔴 core 7.42.0 起策略只说 deny / ask:allow 规则有且只有引擎 allow 层的两个席位 —— 请求的 `settings.permissions.allow` 整份投
6
+ * `TaskSpec.allowedTools`(`task-settings.ts` 的 `applyTaskSettings`),运维的 `APPROVAL_GRANT` 投 `RunnerDeps.allowedTools`
7
+ * (`boot/runner-deps.ts` 的部署姿态座席族)。本口的 `behavior` 型直接是 core 的 `PermissionRule["behavior"]`(不手抄:core 收窄,这里跟着收窄)。
8
8
  *
9
- * 为什么住在一只叶模块里(不留在 `task-settings.ts`):运维那一侧的启动校验在 `config.ts` 里跑(坏值 = 拒启),而
10
- * `task-settings.ts` 值引 `config.ts` —— 编译口留在那里,`config.ts` 要调它就闭合一只 ESM 装载环。本模块只 import core 与
11
- * `text-bidi.ts`(纯叶),三处读者引它都不成环。
9
+ * 为什么住在一只叶模块里(不留在 `task-settings.ts`):本模块只 import core 与 `text-bidi.ts`(纯叶),读者引它不成环。
12
10
  */
13
- import { type PathRuleBases, type RuleBehavior, type ToolPolicy } from "@sema-agent/core";
14
- /** 一张表里规则条数的上限 —— 请求腿 wire 门(`task-settings.ts` 的三桶判形)读的也是这一个名字:core 的缺省帽是 256,
11
+ import { type PathRuleBases, type PermissionRule, type ToolPolicy } from "@sema-agent/core";
12
+ /** 一张表里规则条数的上限 —— 请求腿 wire 门(`task-settings.ts` 的 deny / ask 两桶判形)读的也是这一个名字:core 的缺省帽是 256,
15
13
  * 不对齐的话第 257 条起编译当场抛,等于服务端自己收下的东西自己反悔。 */
16
14
  export declare const MAX_PERMISSION_RULES_PER_LIST = 1000;
17
- /** A name-keyed rule-list ToolPolicy: a tool named by `names` gets `behavior` (an `allow` hit states the allow with
18
- * `decisionReason: "rule"`; `ask` tightens to an approval gate; `deny` hard-blocks); every other tool is out of
19
- * scope (`defaultAction: "allow"` = this policy has no opinion about anything it does not name). Composed deny-wins
20
- * with the rest of the fold, so `ask` / `deny` only ever tighten and an `allow` never overrides a deny or an ask.
15
+ /** A name-keyed rule-list ToolPolicy: a tool named by `names` gets `behavior` (`ask` tightens to an approval gate;
16
+ * `deny` hard-blocks); every other tool is out of scope (`defaultAction: "allow"` = this policy has no opinion about
17
+ * anything it does not name — core 7.42.0: it states nothing and clears nothing). Composed deny-wins with the rest of
18
+ * the fold, so both only ever tighten.
21
19
  *
22
- * `source` = 这张表**写在哪**(请求腿 `settings.permissions.<桶>`;运维腿 `APPROVAL_GRANT`)。它盖进每条规则(core 的 deny / ask
20
+ * `source` = 这张表**写在哪**(请求腿 `settings.permissions.<桶>`)。它盖进每条规则(core 的 deny / ask
23
21
  * 句里点名它:`flagged by permission rule "mcp__figma__*" (settings.permissions.ask)`),编不出来时的错误句也点名它;只是出处,
24
22
  * 不带任何优先级(层序在各自的折叠里)。
25
23
  *
@@ -32,7 +30,7 @@ export declare const MAX_PERMISSION_RULES_PER_LIST = 1000;
32
30
  * `deny:["mcp__figma__*"]` 却**真拦** —— deny 走 `createAllowDenyPolicy`,它用的是 core 的
33
31
  * `namespacedCoveringEntries/Hit`。同族分家,差的只是解析器。
34
32
  *
35
- * ⇒ **规则表的 policy 面现在同源**:三种态度都由 `createPermissionRulePolicy` 的 covering 车道解,底下是
33
+ * ⇒ **规则表的 policy 面现在同源**:两种态度都由 `createPermissionRulePolicy` 的 covering 车道解,底下是
36
34
  * core 的**同一只** `namespacedRuleNameCovers`(协议无关:`mcp__`/`a2a__` 同解;peer 段精确比;工具段 `*`/glob 覆盖)。
37
35
  * 本仓**不自写第二套 glob** —— 那正是当年分家的成因。
38
36
  *
@@ -56,26 +54,17 @@ export declare const MAX_PERMISSION_RULES_PER_LIST = 1000;
56
54
  * 这里;真正新增的是「名字里带反斜杠」「单条 > 512 字符」这两族永不匹配的拼法。
57
55
  * ✅ **不对称已收口([ref]①,[ref] 销)**:codex finding-2 当年如实登记的「同样两族拼法在 `deny`
58
56
  * 上仍是静默 no-op」已修 —— 判据搬进单点成文的 `unmatchableToolNamesRefusal`,它拿 core 自己的 `validatePermissionRules`
59
- * 对同一串跑一遍文法(不自铸第二套判词),于是三张表对这两族说同一句话。S-654 (c) 起 `allow` 也收。
57
+ * 对同一串跑一遍文法(不自铸第二套判词),于是各表对这两族说同一句话。
60
58
  * ⚠️ 存量 resume:带这两族拼法的**已存 body** 在 resume 腿会 422(resolve-spec 翻)。这**不是**新造的姿态——A3
61
59
  * (`rejectUnmatchableSettingsNames`)早就对存量 body 里的退役名/短名同样抛,且 `applyTaskSettings` 腿有专门的回归钉。
62
60
  * 判据同一条:一个**静默不存在的审批门**比一次响亮失败更贵。codex 建议的「给非法存量条目保留旧的逐字比对」被**驳回**:
63
61
  * 那正是把 fail-open 请回来。
64
- * · `caps.maxRules` 对齐 {@link MAX_PERMISSION_RULES_PER_LIST}(理由见那只常量)。core 的 set-level cap 连 `skip` 都照抛,
65
- * 所以运维腿的启动校验还要**试编一次**(`approvalGrantRefusal`)才拦得住超帽 —— 判据仍是本函数这一只。
66
- * · 不再自铸 `decisionReason:"rule"` —— core 的规则策略本就不铸这个键(allow 命中由 core 自己铸 `"rule"`),且 core 侧唯一区分它的
62
+ * · `caps.maxRules` 对齐 {@link MAX_PERMISSION_RULES_PER_LIST}(理由见那只常量);请求腿的受理门先按同一个数拒超帽(400)。
63
+ * · 不自铸 `decisionReason` —— core 的规则策略本就不铸这个键,且 core 侧唯一区分它的
67
64
  * 两处(prepare-task 的继承 grant 复用判据、park 的 rule-suggestion 门)对 `"rule"` 与 `undefined` **同待**(只把 `"hook"`
68
65
  * 排除在外),dist 亲读在案。审计归因改由 `matchedAskRule` + `source` 承担,后者比前者强:它还让 core 的 auto-mode 分类器
69
66
  * **拒绝**替人清掉这条 ask(「运维写下的『每次问我』不是分类器的犹豫」),方向是收紧。
70
- *
71
- * 🔴 **S-654 (c)(7.115.0,core 7.40.0 #1065):三种态度同一个编译口,`allow` 也走这里** —— `behavior: "allow"` 的规则命中 = core 带
72
- * `decisionReason: "rule"` 的放行 —— allow 规则给出「有人表态」—— 它回答的是没人表态的服务器工具 / A2A 技能调用;内置工具上引擎自己的写门、shell 检查照问,deny / ask 照赢(与参照实现的 `permissions.allow` 在内置工具上**不同**:那边 `allow: ["Edit"]` 跳过编辑确认,这里不放掉引擎的写门);未命中 = `defaultAction: "allow"` 的裸 allow
73
- * = 不表态、照常判。修前请求腿的 allow 编成 `createAllowDenyPolicy({ allow })` 的**名单**(名单在场 ⇒ 名单外的工具一律拒),与写它的人要的
74
- * 「替这些工具表态」方向相反;终端因此从不投这只桶。core 7.40.0 起没人表态的服务器工具调用要问,allow 规则就是那张卡的常设答案
75
- * (`mcp__<server>__*` 一条覆盖整台服务器)。allow 规则**不越过** deny / ask:折叠 deny > ask > allow,任何一层的 deny / ask 照赢;
76
- * 它只在没人反对时给出那句表态。core 的 allow 文法只收工具名形(精确名 / `mcp__`·`a2a__` covering 形),内容形放行(`Bash(npm:*)` /
77
- * `Edit(src/**)` / 参数级)core 拒编,两条腿与 deny / ask 同一个出口(判据 `unmatchableToolNamesRefusal`)。
78
- * 7.115.0 起运维的 `APPROVAL_GRANT` 也编在这里(无人值守腿没有请求体,那是运维为自己接的服务器表态的唯一位置)。 */
79
- /** `behavior` = 这张表的态度 —— 型直接是 core 的 `RuleBehavior`(不手抄:core 加一种态度,这里与它同步、读者 tsc 上见到)。 */
80
- export declare function createRuleListPolicy(names: readonly string[], behavior: RuleBehavior, source: string, pathBases: PathRuleBases | undefined): ToolPolicy;
67
+ */
68
+ /** `behavior` = 这张表的态度 —— 型直接是 core 的 `PermissionRule["behavior"]`(不手抄:core 加 / 减一种态度,这里与它同步、读者 tsc 上见到)。 */
69
+ export declare function createRuleListPolicy(names: readonly string[], behavior: PermissionRule["behavior"], source: string, pathBases: PathRuleBases | undefined): ToolPolicy;
81
70
  //# sourceMappingURL=permission-rule-list.d.ts.map
@@ -10,12 +10,13 @@ import type { RuleApprovalRecord, RuleBehavior } from "@sema-agent/core";
10
10
  */
11
11
  export declare function isPersistedRuleTool(name: string): boolean;
12
12
  /**
13
- * `PersistedRuleMatch` 五员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
13
+ * `PersistedRuleMatch` 七员(core 顶注逐字):`exact` / `prefix`(历史 colon-star,永久兼容读、复合前缀形的
14
14
  * 唯一拼写)/ `wildcard`([ref] 尾 space-star,单命令体上与 prefix **同一谓词**,建议面自此铸它)/
15
15
  * `subpath`(Read 目录形,永不容纳任何命令)/ `path`(core 7.9.0 [ref]:路径族 deny/ask/allow 的模式文法
16
- * —— `//abs`、`~/`、`/root-relative`、cwd 相对,段内 `*`、跨段 `**`)。
16
+ * —— `//abs`、`~/`、`/root-relative`、cwd 相对,段内 `*`、跨段 `**`)/ `name` / `name_prefix`(core 7.43.0 #1318:`Skill` 的名字文法 ——
17
+ * `Skill(<名>)` 精确、`Skill(<前缀>:*)` 名字前缀、无词界;技能卡的建议行 `match: "name"`)。
17
18
  */
18
- export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path"];
19
+ export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "subpath", "path", "name", "name_prefix"];
19
20
  /**
20
21
  * S-511 —— `RuleBehavior` 每一员在**无同意仪式的直接写口**(`POST /v1/rules`)上的逐员表态。
21
22
  *
@@ -2,7 +2,7 @@ import { ruleToolGrammarOf } from "@sema-agent/core";
2
2
  export function isPersistedRuleTool(name) {
3
3
  return ruleToolGrammarOf(name) !== undefined;
4
4
  }
5
- export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath", "path"];
5
+ export const PERSISTED_RULE_MATCHES = ["exact", "prefix", "wildcard", "subpath", "path", "name", "name_prefix"];
6
6
  const MATCHES_PIN = true;
7
7
  void MATCHES_PIN;
8
8
  export const DIRECT_RULE_WRITE_ADMITS = {
@@ -82,8 +82,8 @@ export interface AskRow {
82
82
  decisionNote: string | null;
83
83
  /**
84
84
  * 🔴 S-558(#1016 的持久半场)—— 这条决议的**结算来源**:`null` = 一个人答的(= 缺席律,core 对
85
- * 不带这一格的 outcome 逐字按 `"human"` 读),非 null = 宿主自报的那个词(今天 wire 上只开
86
- * `"policy"`,词表属主恒是 core —— 见 {@link HostSettledBy})。
85
+ * 不带这一格的 outcome 逐字按 `"human"` 读),非 null = 宿主自报的那个词(S-1154 起写进来的只有决定体出处位
86
+ * `decidedBy:"policy"` 经 `liveSettledBy` 映射来的 `"policy"`,allow 与 deny 两向都写;词表属主恒是 core —— 见 {@link HostSettledBy})。
87
87
  *
88
88
  * **为什么必须是行上的一格**(病灶,7.93.3 合并树 codex ① 验真):`settledBy` 此前只到达**当次**回决的
89
89
  * 即时回调,行上不留痕 ⇒ 重放 / 再入 / 同 askId 的兄弟等待者 / 歧义提交恢复读回的是裸 `false`,
@@ -230,8 +230,8 @@ export interface DecideAskInput {
230
230
  * 🔴 S-558:本次回决的**结算来源**,与决议同一条 UPDATE 落列 `settled_by`(语义/两态读法/为什么是
231
231
  * 自己一列,全在 {@link AskRow.settledBy})。缺席 ⇒ 列 NULL ⇒ 读回是「一个人答的」。
232
232
  *
233
- * 店**不判词**(词表属主是 core):写什么存什么。店只判**形** —— `assertDecideSettledBy` 的两条
234
- * (长度 ≤ 列宽、只与 `deny` 同行),理由见那只门的顶注;三 twin 同判。
233
+ * 店**不判词**(词表属主是 core):写什么存什么。店只判**形** —— `assertDecideSettledBy` 的那一条
234
+ * (长度 ≤ 列宽;S-1154 起 approve 行也可以带来源位),理由见那只门的顶注;三 twin 同判。
235
235
  */
236
236
  settledBy?: HostSettledBy;
237
237
  /** [ref]:ctrl+g 编辑实参,与决议同一条 UPDATE 落列(语义三态见 {@link AskRow.updatedInput})。
@@ -270,26 +270,23 @@ export declare function assertIntegralEpochMs(field: string, value: number): voi
270
270
  export declare function assertIdempotencyKeyShape(key: string): void;
271
271
  /** `settled_by` 列宽 = 这一格的唯一尺(两方言 DDL 与下面的形门读同一个常量,不手抄第二份)。 */
272
272
  export declare const MAX_SETTLED_BY_CHARS = 16;
273
- /** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 形门 ① 的**谓词本身**,店门口
274
- * ({@link assertDecideSettledBy})与耐久 ask 回决口的 CAS 前体门(S-843:确定的体形错误拒成 400,不落进提交歧义臂)
275
- * 读同一只,尺不抄第二份。 */
273
+ /** `settled_by` 这一列装得下这个词吗(1..{@link MAX_SETTLED_BY_CHARS})—— 店门口形门({@link assertDecideSettledBy})的**谓词本身**。
274
+ * S-1154 起决定口不再读它:wire 上的出处位是闭集(`host-decision.ts` 的 `APPROVAL_DECIDED_BY`),映射到这一列的词恒在尺内。 */
276
275
  export declare function settledByFitsColumn(v: string): boolean;
277
276
  /**
278
277
  * S-558 的**入店卫生门**(三 twin 同判,与 {@link assertIdempotencyKeyShape} / {@link assertIntegralEpochMs} 同族)。
279
- * 判的是**形**,不是**词** —— 词表属主恒是 core(`resolveAsk` 对自报词表外的值是响亮 fail-closed deny),
280
- * 本层一个词都不认识。两条:
278
+ * 判的是**形**,不是**词** —— 词表属主恒是 core,本层一个词都不认识。一条:
281
279
  *
282
- * ① **长度 ≤ 列宽**。`settled_by` 是 `VARCHAR(16)` 的闭集词列,而值来自 wire(两条回决腿只证得出「它是个串」)。
283
- * 不拦的话:MySQL 协议侧非严格模式**静默截断**、PG 侧直接报错 —— 同一份调用方代码在两个后端拿到两种
284
- * 答案(`assertIdempotencyKeyShape` 的 PAD SPACE 论证逐字同源),而**被截断过的词与原词是两个词**:
285
- * 一条决议的来源在库里变成了另一件事。空串同拒(core 那边它不是词,存进去只会读成一个假的「非人」)。
286
- * ② **只与 `deny` 同行**。core 的契约明写两个非人词只在 `allow:false` 上合法(`{allow:true,settledBy}` 是
287
- * fail-closed 拒),两条回决腿也都在**裁决之前**照此响亮拒(`tool-approval.ts` 的
288
- * `SETTLED_BY_DENY_ONLY`,两腿共用一份文案)。在店门口再执行一次不是「第二个判官」:判的不是词而是**这一对列能不能共存**。
289
- * 收益是结构性的 —— 「approve 行带来源位」从此在写面不可表示,读回铸点(`outcomeOfDecidedRow`)的 allow 臂
290
- * 因此**证得出**不会丢掉任何真事实,而不是靠记性。
280
+ * **长度 ≤ 列宽**。`settled_by` 是 `VARCHAR(16)` 的闭集词列。不拦的话:MySQL 协议侧非严格模式**静默截断**、PG 侧直接
281
+ * 报错 —— 同一份调用方代码在两个后端拿到两种答案(`assertIdempotencyKeyShape` 的 PAD SPACE 论证逐字同源),而**被截断过的
282
+ * 词与原词是两个词**:一条决议的来源在库里变成了另一件事。空串同拒(core 那边它不是词,存进去只会读成一个假的「非人」)。
283
+ *
284
+ * 🔴 S-1154:此前的第二条「只与 `deny` 同行」**删了** —— 它的前提(「只有人能批准一个要执行的动作,core 对 `{allow:true,settledBy}`
285
+ * 一律 fail-closed 拒」)自 core 7.39.0 起不成立:策略的放行铸 `policy_allowed`,决定体的出处位 `decidedBy:"policy"` 在 allow 与 deny
286
+ * 上都落这一列,approve 行带来源位是一条合法的行(读回铸点 `outcomeOfDecidedRow` 两向都读它)。今天写这一列的只有两条路:决定口经
287
+ * `liveSettledBy` 映射来的 `"policy"`,与 `claimTerminal(refuse)` 的 {@link POLICY_REFUSAL_DECISION} —— 都是 core 在两向上都收的词。
291
288
  */
292
- export declare function assertDecideSettledBy(decision: Pick<DecideAskInput, "decision" | "settledBy">): void;
289
+ export declare function assertDecideSettledBy(decision: Pick<DecideAskInput, "settledBy">): void;
293
290
  /**
294
291
  * [ref] 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
295
292
  *
@@ -324,7 +321,7 @@ export type AskTerminalClaimIntent = "expire" | "cancel" | "refuse";
324
321
  * `refuse` 意图落行的决议(NP-2)—— **部署政策拒**:deny + 结算来源 `policy`。来源词的型取 core 的 `HostSettledBy`
325
322
  * (core 改词 / 退词 ⇒ 这里 tsc 红);协调器交给 core 的那一只(`tool-approval.ts` 的 `UnattendedPolicyRefusal`)读的是
326
323
  * **同一个**常量,于是「行上记的来源」与「首判交出去的来源」在构造上是一个词。三 twin 落行前都过 {@link assertDecideSettledBy}
327
- * (settled_by 的唯一形门:长度 ≤ 列宽、只与 deny 同行)。
324
+ * (settled_by 的唯一形门:长度 ≤ 列宽)。
328
325
  */
329
326
  export declare const POLICY_REFUSAL_DECISION: {
330
327
  readonly decision: "deny";
@@ -24,9 +24,6 @@ export function assertDecideSettledBy(decision) {
24
24
  if (!settledByFitsColumn(v)) {
25
25
  throw new Error(`invalid settledBy: the settlement source is one word from the engine's closed set and lands in a VARCHAR(${MAX_SETTLED_BY_CHARS}) column — must be 1-${MAX_SETTLED_BY_CHARS} chars (got ${JSON.stringify(v.slice(0, 32))}${v.length > 32 ? "…" : ""}, length ${v.length})`);
26
26
  }
27
- if (decision.decision !== "deny") {
28
- throw new Error(`invalid settledBy: a settlement source is only representable on a deny row (got decision="${decision.decision}") — only a person approves an action that EXECUTES, so an approve row carrying a non-human settlement source is a row no replay could honestly hand back to the engine`);
29
- }
30
27
  }
31
28
  export class ApprovalAskReadAbortedError extends Error {
32
29
  constructor(where) {
@@ -24,6 +24,15 @@ export interface OneTimeMigrationReport {
24
24
  readonly sessionIncarnationsBackfilled: number;
25
25
  /** M6:本次给「只有句子」的死亡判决行补上机读码的 run 行数(SQL `task_run` / local `run.json`)。非零 = 这次迁移真写了存量行(与 M4 同一条纪律)。 */
26
26
  readonly holderDeathCodeRowsBackfilled: number;
27
+ /** M8:本次把 `checkpoint.risk_descriptor` 的旧披露键 `shadowedRule` 改写成 `allowRuleShadowed` 的行数(SQL 独有;local 恒 0)。非零 = 真写了存量行。 */
28
+ readonly allowRuleShadowRowsMigrated: number;
29
+ /** M8:带旧键字样、但这一格的 JSON 读不懂 ⇒ 没改写的行数(留给读口自己的响亮臂;非零 = 运维该看一眼这些行)。SQL 独有;local 恒 0。 */
30
+ readonly allowRuleShadowRowsUnreadable: number;
31
+ /** M9:workflow park 指针索引的主键 —— `reshaped` = 本次把只有 `session_id` 的主键改成 `(session_id, run_id)`;`current` = 早已是新形
32
+ * (新库 / 第二跑);`file-ledger` = local 文件车道(账本一行本来就是一条 run 的指针集,没有主键可改)。 */
33
+ readonly workflowParkIndexKey: "reshaped" | "current" | "file-ledger";
34
+ /** M9:改形时原样搬进新表的指针行数(`current` / `file-ledger` 时恒 0)。非零 = 这次迁移真重建了一张表,运维该看见一次。 */
35
+ readonly workflowParkIndexRowsCarried: number;
27
36
  }
28
37
  /**
29
38
  * S-590 —— 一条一次性迁移的窗(登记表的一行;纯数据)。字段注写着每个值的取证方式,改一行之前照着重核。
@@ -95,6 +104,22 @@ export declare class RetiredSteerSeatError extends Error {
95
104
  * @throws {RetiredSteerSeatError} M2 探到非空的退役席位(唯一的拒启臂)。
96
105
  */
97
106
  export declare function runSqlOneTimeMigrations(dialect: "tidb" | "pg", pool: MysqlPool | PgPool): Promise<OneTimeMigrationReport>;
107
+ /** M8 一行改写的结论:改写后的文本 / 这一行不用动 / 读不懂(解析错误原样交出,调用方计数进读数)。 */
108
+ export type ShadowedRuleRewrite = {
109
+ readonly kind: "rewrite";
110
+ readonly text: string;
111
+ } | {
112
+ readonly kind: "untouched";
113
+ } | {
114
+ readonly kind: "unreadable";
115
+ readonly error: unknown;
116
+ };
117
+ /**
118
+ * M8 的**一行**改写(纯函数;导出给钉用):`risk_descriptor` 的 JSON 文本里顶层带字符串 `shadowedRule` ⇒ 改写(旧键删、`allowRuleShadowed`
119
+ * 由旧值铸一条 `persisted` 记录;行上已经有 `allowRuleShadowed` ⇒ 只删旧键,不并);不是对象 / 没有旧键 / 旧键不是字符串 ⇒ `untouched`;
120
+ * JSON 读不懂 ⇒ `unreadable`(这一行不动,交给读口自己的响亮臂;迁移不替它猜,但把它数进读数,不静默)。
121
+ */
122
+ export declare function migrateShadowedRuleDescriptor(text: string): ShadowedRuleRewrite;
98
123
  /**
99
124
  * M3 的**纯**改写(local 车道的账本行逐条走它;SQL 两腿在服务端用等价的 JSON 路径表达同一件事)。
100
125
  * 不改原对象(调用方与账本共享引用):有变更时换一只信封,原记录一个字节不动;无变更 ⇒ 返回
@@ -1,6 +1,8 @@
1
- import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
2
- import { acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
3
- import { jsonColumnTextExpr } from "./sql-json-column.js";
1
+ import { SCHEMA_STATEMENTS, acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
2
+ import { PG_SCHEMA_STATEMENTS, acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
3
+ import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
4
+ import { ensureIndexes } from "./ensure-index.js";
5
+ import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn } from "./sql-json-column.js";
4
6
  import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
5
7
  import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
6
8
  export const ONE_TIME_MIGRATIONS = [
@@ -76,6 +78,47 @@ export const ONE_TIME_MIGRATIONS = [
76
78
  "丢掉的 steer 不再告诉用户;`GET /v1/runs/:id` 上 `errorCode` 缺席、只剩 `error` 句子",
77
79
  reportKeys: ["holderDeathCodeRowsBackfilled"],
78
80
  },
81
+ {
82
+ id: "M7",
83
+ introducedIn: "7.117.0",
84
+ releasedOn: "2026-10-06",
85
+ removableAfter: "2026-11-05",
86
+ removal: "删 `http/checkpoint-ctx.ts` 的读法记号判据与拒臂(`allowListReadingUnstated` / `allowListReadingEraOf` / `allowListReadingRefusal` / " +
87
+ "`PERMISSIONS_ALLOW_READINGS` / `ALLOW_LIST_READING_ERAS`、`buildCheckpointCtx` 里的记号写点)、`CheckpointCtx.permissionsAllowReading`、" +
88
+ "`resume-legs.ts` 两个判点与 `approval-sweeps.ts` 的跳过臂、随包排空判据 `permissions-allow-list`(两份 SQL + 本地脚本那一条)",
89
+ afterRemoval: "没有响亮臂:删码之后仍停驻、体里带非空 allow 的旧读法行(把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署,或跳过了带本条的版本直接升的库)会被按" +
90
+ "当前读法静默重放(名单时代的行:名单外的工具放开;表态时代的行:普通问不再问)—— 升级前按 MIGRATION 7.117.0 节跑排空判据清零",
91
+ reportKeys: [],
92
+ },
93
+ {
94
+ id: "M8",
95
+ introducedIn: "7.117.0",
96
+ releasedOn: "2026-10-06",
97
+ removableAfter: "2026-11-05",
98
+ removal: "删 migrateOn 的 M8 步(`checkpoint.risk_descriptor` 的旧键扫描 + CAS 回写)与 `migrateShadowedRuleKey` / `migrateShadowedRuleDescriptor`;" +
99
+ "报告、日志行与 `NO_SQL_MIGRATIONS` 的 `allowRuleShadowRowsMigrated` / `allowRuleShadowRowsUnreadable`",
100
+ afterRemoval: "没有响亮臂:删码之后仍带旧键的停驻行(跳过了带本条的版本直接升的库)在运维待批队列上只缺结构化披露 `allowRuleShadowed`;旧键的文本仍经脱敏叶表" +
101
+ "「认不得的键按内容脱敏」一条脱敏后原样显示,卡上 `message` 里引擎写好的那句话不受影响",
102
+ reportKeys: ["allowRuleShadowRowsMigrated", "allowRuleShadowRowsUnreadable"],
103
+ },
104
+ {
105
+ id: "M9",
106
+ introducedIn: "7.118.0",
107
+ releasedOn: "2026-10-07",
108
+ removableAfter: "2026-11-06",
109
+ removal: "删 migrateOn 的 M9 步(`reshapeWorkflowParkIndex` / `execAtomically` / `finishParkIndexShape`:收尾段、主键宽度探针、暂存表复制、换名、删旧表、索引补建、主键约束名)与 " +
110
+ "`primaryKeyWidth` / `WORKFLOW_PARK_INDEX_TABLE` / `WORKFLOW_PARK_INDEX_STAGING` / `WORKFLOW_PARK_INDEX_RETIRED`;报告、日志行与 `NO_SQL_MIGRATIONS` 的 " +
111
+ "`workflowParkIndexKey` / `workflowParkIndexRowsCarried`;**连同读侧半场一起删**(都是「引擎 7.43 及以前写下的 workflow 子代停驻记录」这同一件事,同窗同删,不另起登记行):" +
112
+ "`orchestration/workflow-agent-session-index.ts` 的 `older_engine` 读臂(`routeWorkflowPark` 规则 ③、`WorkflowParkRoute` 与 `parked-decide.ts` `ParkOrigin` 上 `cause` 里的这一词," +
113
+ "`denyWorkflowParksUnreadable` 的对应分支;`unreadable` 成为唯一成因)与随包排空判据 `workflow-park-holder`(`scripts/drain-check.mysql.sql` / `scripts/drain-check.pg.sql` 的那一段" +
114
+ "与文件头判据清单里的那一行、`scripts/drain-check-local.mjs` 的 `DRAIN_CRITERIA` 那一条与 `readParkIndex`,以及 `MIGRATION.md` / `docs/DEPLOY-PREREQS.md` 里点名它的说明)",
115
+ afterRemoval: "没有响亮臂:删码之后仍是旧主键(只有 `session_id`)的库(跳过了带本条的版本直接升)上,同一个子会话只能有一枚指针 —— 续跑链 / 分叉里" +
116
+ "后写的那条记录覆盖先写的,判决只看得到一条记录:分叉判不出、链尾可能被更早的记录盖掉(决定投给那一条)。运维手工照中央 DDL 重建这张表即可" +
117
+ "(它是派生投影,丢了只丢升级那一刻还停驻着的旧卡的指针 —— 那些卡改答诚实 miss)。读侧半场删后:升级那一刻还停驻着、点名它的条目没有持有者位的存量卡" +
118
+ "(跳过了带本条的版本直接升的库)路由判 `none` ⇒ 落 legacy 腿,答 `409 conflict.resume_context_unavailable`(响亮,但成因是子会话没有续跑上下文,不再是 " +
119
+ "`decide.workflow_parks_unreadable`);出路不变(续跑发起它的那条 workflow run);随包排空判据没了,换钉前没有机器读数可数这些卡",
120
+ reportKeys: ["workflowParkIndexKey", "workflowParkIndexRowsCarried"],
121
+ },
79
122
  ];
80
123
  export const NO_SQL_MIGRATIONS = {
81
124
  retiredSnapshotTable: "absent",
@@ -85,6 +128,10 @@ export const NO_SQL_MIGRATIONS = {
85
128
  auditDetailJsonNullRows: 0,
86
129
  sessionIncarnationColumn: "lazy-on-read",
87
130
  sessionIncarnationsBackfilled: 0,
131
+ allowRuleShadowRowsMigrated: 0,
132
+ allowRuleShadowRowsUnreadable: 0,
133
+ workflowParkIndexKey: "file-ledger",
134
+ workflowParkIndexRowsCarried: 0,
88
135
  };
89
136
  const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
90
137
  const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
@@ -224,6 +271,8 @@ async function migrateOn(db, dialect) {
224
271
  (await db.exec(dialect === "tidb"
225
272
  ? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
226
273
  : "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
274
+ const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
275
+ const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
227
276
  let retiredSnapshotTable = "absent";
228
277
  let retiredSnapshotRows = 0;
229
278
  let retiredSteerSeatColumn = "absent";
@@ -256,9 +305,138 @@ async function migrateOn(db, dialect) {
256
305
  }
257
306
  return {
258
307
  retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows,
259
- sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled,
308
+ sessionIncarnationColumn, sessionIncarnationsBackfilled, holderDeathCodeRowsBackfilled, allowRuleShadowRowsMigrated, allowRuleShadowRowsUnreadable,
309
+ workflowParkIndexKey, workflowParkIndexRowsCarried,
260
310
  };
261
311
  }
312
+ const WORKFLOW_PARK_INDEX_TABLE = "workflow_agent_session";
313
+ async function primaryKeyWidth(db, dialect, table) {
314
+ const sql = dialect === "tidb"
315
+ ? `SELECT COUNT(*) AS n FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND table_name = '${table}' AND constraint_name = 'PRIMARY'`
316
+ : `SELECT COUNT(*) AS n FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ` +
317
+ `ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name ` +
318
+ `WHERE tc.table_schema = current_schema() AND tc.table_name = '${table}' AND tc.constraint_type = 'PRIMARY KEY'`;
319
+ return Number((await db.rows(sql))[0]?.n ?? 0);
320
+ }
321
+ const WORKFLOW_PARK_INDEX_STAGING = `${WORKFLOW_PARK_INDEX_TABLE}_m9`;
322
+ const WORKFLOW_PARK_INDEX_RETIRED = `${WORKFLOW_PARK_INDEX_TABLE}_m9_old`;
323
+ async function reshapeWorkflowParkIndex(db, dialect) {
324
+ const T = WORKFLOW_PARK_INDEX_TABLE;
325
+ const S = WORKFLOW_PARK_INDEX_STAGING;
326
+ const O = WORKFLOW_PARK_INDEX_RETIRED;
327
+ const exists = (t) => tableExists(db, dialect, t);
328
+ const ddl = (dialect === "tidb" ? SCHEMA_STATEMENTS : PG_SCHEMA_STATEMENTS).find((stmt) => stmt.includes(`CREATE TABLE IF NOT EXISTS ${T} (`));
329
+ if (ddl === undefined)
330
+ throw new Error(`one-time migration M9: the central schema carries no definition of ${T} — refusing to reshape a table it cannot rebuild`);
331
+ const copyPointers = (to, from) => db.exec(dialect === "tidb"
332
+ ? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
333
+ : `INSERT INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from} ON CONFLICT DO NOTHING`);
334
+ let carried = 0;
335
+ const resumed = await exists(S);
336
+ if (resumed) {
337
+ await db.exec(ddl);
338
+ carried += await copyPointers(T, S);
339
+ await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
340
+ }
341
+ if (await exists(O))
342
+ await execUntil(db, `DROP TABLE ${O}`, async () => !(await exists(O)));
343
+ if ((await primaryKeyWidth(db, dialect, T)) !== 1) {
344
+ if (await exists(T))
345
+ await finishParkIndexShape(db, dialect);
346
+ return resumed ? { key: "reshaped", carried } : { key: "current", carried: 0 };
347
+ }
348
+ await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
349
+ carried += await copyPointers(S, T);
350
+ const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
351
+ if (dialect === "tidb") {
352
+ await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
353
+ await execUntil(db, `DROP TABLE IF EXISTS ${O}`, async () => !(await exists(O)));
354
+ }
355
+ else {
356
+ await execAtomically(db, [`ALTER TABLE ${T} RENAME TO ${O}`, `ALTER TABLE ${S} RENAME TO ${T}`, `DROP TABLE ${O}`], async () => (await swapped()) && !(await exists(O)));
357
+ }
358
+ await finishParkIndexShape(db, dialect);
359
+ return { key: "reshaped", carried };
360
+ }
361
+ async function execAtomically(db, statements, postcondition) {
362
+ try {
363
+ await db.exec("BEGIN");
364
+ for (const sql of statements)
365
+ await db.exec(sql);
366
+ await db.exec("COMMIT");
367
+ }
368
+ catch (e) {
369
+ try {
370
+ await db.exec("ROLLBACK");
371
+ }
372
+ catch (rollbackError) {
373
+ throw new Error(`one-time migration: a transactional DDL step failed (${e instanceof Error ? e.message : String(e)}) and could not be rolled back on this connection ` +
374
+ `(${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}) — the database rolls the transaction back when the connection closes; the next start resumes`, { cause: e });
375
+ }
376
+ if (!(await postcondition()))
377
+ throw e;
378
+ }
379
+ }
380
+ async function finishParkIndexShape(db, dialect) {
381
+ await ensureIndexes(async (sql, params) => (dialect === "tidb" ? [await db.rows(sql, params)] : { rows: await db.rows(sql, params) }), dialect === "tidb" ? "mysql" : "pg", CENTRAL_SCHEMA_INDEXES.filter((spec) => spec.table === WORKFLOW_PARK_INDEX_TABLE));
382
+ if (dialect !== "pg")
383
+ return;
384
+ const conventional = `${WORKFLOW_PARK_INDEX_TABLE}_pkey`;
385
+ const pk = await db.rows(`SELECT constraint_name FROM information_schema.table_constraints WHERE table_schema = current_schema() AND table_name = '${WORKFLOW_PARK_INDEX_TABLE}' AND constraint_type = 'PRIMARY KEY'`);
386
+ const name = pk[0]?.constraint_name;
387
+ if (typeof name === "string" && name !== conventional)
388
+ await db.exec(`ALTER TABLE ${WORKFLOW_PARK_INDEX_TABLE} RENAME CONSTRAINT ${name} TO ${conventional}`);
389
+ }
390
+ const RETIRED_SHADOWED_RULE_KEY = "shadowedRule";
391
+ export function migrateShadowedRuleDescriptor(text) {
392
+ let parsed;
393
+ try {
394
+ parsed = decodeJsonColumn(text, readNonNullJsonValueColumn, "checkpoint.risk_descriptor (M8 rewrite)");
395
+ }
396
+ catch (error) {
397
+ if (isJsonColumnBodyProblem(error))
398
+ return { kind: "unreadable", error };
399
+ throw error;
400
+ }
401
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
402
+ return { kind: "untouched" };
403
+ const { [RETIRED_SHADOWED_RULE_KEY]: old, ...rest } = parsed;
404
+ if (typeof old !== "string")
405
+ return { kind: "untouched" };
406
+ const next = Object.hasOwn(rest, "allowRuleShadowed") ? rest : { ...rest, allowRuleShadowed: [{ member: "persisted", entries: [old] }] };
407
+ return { kind: "rewrite", text: JSON.stringify(next) };
408
+ }
409
+ const SHADOWED_RULE_BATCH = 500;
410
+ async function migrateShadowedRuleKey(db, dialect) {
411
+ const like = `%"${RETIRED_SHADOWED_RULE_KEY}"%`;
412
+ let migrated = 0;
413
+ let unreadable = 0;
414
+ let after = "";
415
+ for (;;) {
416
+ const rows = await db.rows(dialect === "tidb"
417
+ ? `SELECT token, ${jsonColumnText(dialect, "risk_descriptor")} FROM checkpoint WHERE ${jsonColumnTextExpr(dialect, "risk_descriptor")} LIKE ? AND token > ? ORDER BY token LIMIT ${SHADOWED_RULE_BATCH}`
418
+ : `SELECT token, ${jsonColumnText(dialect, "risk_descriptor")} FROM checkpoint WHERE ${jsonColumnTextExpr(dialect, "risk_descriptor")} LIKE $1 AND token > $2 ORDER BY token LIMIT ${SHADOWED_RULE_BATCH}`, [like, after]);
419
+ for (const r of rows) {
420
+ const token = String(r.token);
421
+ after = token;
422
+ const text = r.risk_descriptor;
423
+ if (typeof text !== "string")
424
+ continue;
425
+ const next = migrateShadowedRuleDescriptor(text);
426
+ if (next.kind === "unreadable") {
427
+ unreadable += 1;
428
+ continue;
429
+ }
430
+ if (next.kind === "untouched")
431
+ continue;
432
+ migrated += await db.exec(dialect === "tidb"
433
+ ? `UPDATE checkpoint SET risk_descriptor = ? WHERE token = ? AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = ?`
434
+ : `UPDATE checkpoint SET risk_descriptor = $1 WHERE token = $2 AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = $3`, [next.text, token, text]);
435
+ }
436
+ if (rows.length < SHADOWED_RULE_BATCH)
437
+ return { migrated, unreadable };
438
+ }
439
+ }
262
440
  const INCARNATION_BACKFILL_BATCH = 500;
263
441
  async function migrateSessionIncarnations(db, dialect) {
264
442
  if (!(await tableExists(db, dialect, "session_meta")))
@@ -329,7 +507,8 @@ export function migrateRetiredAskOrigin(type, data) {
329
507
  }
330
508
  export function logOneTimeMigrations(log, backend, report) {
331
509
  const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0 ||
332
- report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0;
510
+ report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
511
+ report.workflowParkIndexKey === "reshaped";
333
512
  (changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
334
513
  backend,
335
514
  retiredSnapshotTable: report.retiredSnapshotTable,
@@ -341,13 +520,20 @@ export function logOneTimeMigrations(log, backend, report) {
341
520
  sessionIncarnationColumn: report.sessionIncarnationColumn,
342
521
  sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
343
522
  holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
523
+ allowRuleShadowRowsMigrated: report.allowRuleShadowRowsMigrated,
524
+ allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
525
+ workflowParkIndexKey: report.workflowParkIndexKey,
526
+ workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
344
527
  removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
345
528
  note: "one-time persisted-data migrations (M1: retired whole-tree snapshot index table dropped; M2: retired single-seat " +
346
529
  "steering column dropped; M3: retired ask-origin spelling rewritten in place; M4: audit details written as the JSON " +
347
530
  "text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
348
531
  "session incarnation column added and every session row without one minted its own token; M6: run rows failed by a " +
349
532
  "holder-death verdict written by an earlier version with only the sentence got the machine-readable code run.stalled / " +
350
- "run.orphaned). Each step " +
533
+ "run.orphaned; M7 writes nothing — it is the window of the refusal that keeps a suspended task whose stored allow list was written " +
534
+ "under an earlier reading from resuming under the current one; M8: parked rows' allow-layer disclosure written by an earlier engine " +
535
+ "under its old key shadowedRule rewritten to allowRuleShadowed; M9: the workflow park pointer index rebuilt with the key (session, run), " +
536
+ "its rows carried over). Each step " +
351
537
  "runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
352
538
  "removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
353
539
  "version that still carries a step at least once before upgrading past its deletion.",
@@ -282,7 +282,7 @@ export const PG_SCHEMA_STATEMENTS = [
282
282
  run_id VARCHAR(191) COLLATE "C" NOT NULL,
283
283
  agent_key VARCHAR(191) COLLATE "C" NOT NULL,
284
284
  scope VARCHAR(190) COLLATE "C" NOT NULL,
285
- PRIMARY KEY (session_id)
285
+ PRIMARY KEY (session_id, run_id)
286
286
  )`,
287
287
  `CREATE TABLE IF NOT EXISTS workflow_completion_inbox (
288
288
  seq BIGSERIAL,
@@ -528,7 +528,7 @@ export const SCHEMA_STATEMENTS = [
528
528
  run_id VARCHAR(191) NOT NULL,
529
529
  agent_key VARCHAR(191) NOT NULL,
530
530
  scope VARCHAR(190) NOT NULL,
531
- PRIMARY KEY (session_id),
531
+ PRIMARY KEY (session_id, run_id),
532
532
  KEY idx_wfagentsess_run (run_id)
533
533
  ) COLLATE utf8mb4_bin`,
534
534
  `CREATE TABLE IF NOT EXISTS workflow_completion_inbox (