@sema-agent/server 7.10.0 → 7.12.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.
- package/README.md +1 -1
- package/USAGE.md +56 -0
- package/dist/adoption/plan.d.ts +152 -0
- package/dist/adoption/plan.js +513 -0
- package/dist/adoption/runner.d.ts +54 -0
- package/dist/adoption/runner.js +505 -0
- package/dist/adoption/sql.d.ts +76 -0
- package/dist/adoption/sql.js +106 -0
- package/dist/adoption/wire.d.ts +250 -0
- package/dist/adoption/wire.js +153 -0
- package/dist/approval-card.d.ts +24 -0
- package/dist/approval-card.js +32 -0
- package/dist/auth-keys.d.ts +28 -4
- package/dist/auth-keys.js +60 -15
- package/dist/boot/adoption.d.ts +30 -0
- package/dist/boot/adoption.js +57 -0
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/parked-revive-gate.d.ts +56 -7
- package/dist/boot/parked-revive-gate.js +177 -8
- package/dist/boot/permission-rules-audit.d.ts +49 -0
- package/dist/boot/permission-rules-audit.js +57 -0
- package/dist/boot/resolve-spec.js +43 -12
- package/dist/boot/runner-deps.d.ts +10 -2
- package/dist/boot/runner-deps.js +12 -1
- package/dist/budget.js +22 -0
- package/dist/config-types.d.ts +24 -1
- package/dist/config.d.ts +28 -2
- package/dist/config.js +348 -75
- package/dist/governance-ask-marks.js +8 -2
- package/dist/http/route-ctx.d.ts +6 -3
- package/dist/http/routes/adoption.d.ts +26 -0
- package/dist/http/routes/adoption.js +120 -0
- package/dist/http/routes/approvals-assistant.js +2 -1
- package/dist/http/routes/capabilities.js +49 -2
- package/dist/http/routes/rules.d.ts +35 -0
- package/dist/http/routes/rules.js +293 -0
- package/dist/http/routes/shared-memory.d.ts +31 -0
- package/dist/http/routes/shared-memory.js +181 -0
- package/dist/http/routes/trace-usage.js +139 -3
- package/dist/http/server.d.ts +23 -3
- package/dist/http/server.js +123 -3
- package/dist/http/wire-types.d.ts +48 -0
- package/dist/main.js +70 -3
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/tool-trace.d.ts +5 -1
- package/dist/observability/tool-trace.js +33 -6
- package/dist/parked-decide.d.ts +13 -3
- package/dist/parked-decide.js +10 -1
- package/dist/plugins/adoption-log-sql.d.ts +191 -0
- package/dist/plugins/adoption-log-sql.js +273 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +13 -0
- package/dist/plugins/checkpoint-store-sql.js +11 -0
- package/dist/plugins/local-checkpoint-store.d.ts +10 -0
- package/dist/plugins/local-checkpoint-store.js +8 -0
- package/dist/plugins/permission-rule-store-file.d.ts +83 -0
- package/dist/plugins/permission-rule-store-file.js +371 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +249 -0
- package/dist/plugins/permission-rule-store-sql.js +828 -0
- package/dist/plugins/pg-pool.js +37 -0
- package/dist/plugins/session-policy-store-sql.d.ts +6 -0
- package/dist/plugins/session-policy-store-sql.js +7 -1
- package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
- package/dist/plugins/shared-memory-store-sql.js +516 -0
- package/dist/plugins/store-backend.d.ts +38 -2
- package/dist/plugins/store-backend.js +69 -6
- package/dist/plugins/tidb-pool.js +47 -0
- package/dist/rules-consent.d.ts +194 -0
- package/dist/rules-consent.js +240 -0
- package/dist/run-local.js +120 -13
- package/dist/runtime-governance.js +9 -3
- package/dist/shared-memory-scope-authorizer.d.ts +29 -0
- package/dist/shared-memory-scope-authorizer.js +17 -0
- package/dist/task-settings.d.ts +44 -0
- package/dist/task-settings.js +57 -1
- package/dist/tool-approval.d.ts +60 -0
- package/dist/tool-approval.js +223 -25
- package/dist/trace/core-keyset-guard.d.ts +15 -4
- package/dist/trace/project.d.ts +20 -2
- package/dist/trace/project.js +25 -4
- package/package.json +3 -3
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/183 §4.3 —— **form b 收编的迁移计划表**(server 半场细设计;183 §1 明写「表级细设计归 server」)。
|
|
3
|
+
*
|
|
4
|
+
* 这张表是本车的**单一真源**:phase 0 的目的地冲突 preflight、phase 3 的行迁/桶迁、回执里的 `legs`
|
|
5
|
+
* 三者全部由它派生 —— 三处各抄一份表正是「preflight 说没冲突、迁移腿却撞 UNIQUE」这类分家的温床。
|
|
6
|
+
*
|
|
7
|
+
* ── 判定轴(183 §4.1 身份轴三形,落到本仓真 schema)────────────────────────────────────────────────
|
|
8
|
+
* · **形Ⅰ 行迁**(`row-rewrite`):数据里有 owner/principal 结构位,且该位**不在任何唯一键里**
|
|
9
|
+
* ⇒ 一条 `UPDATE … SET col = :to WHERE col = :from` 即可,重跑改到同值 = 零行变化(幂等),
|
|
10
|
+
* 且**不可能撞唯一键**(`residualKey: undefined` 就是这句话的机器形)。
|
|
11
|
+
* · **形Ⅱ 桶迁**(`bucket-rebind`):键是 scope,而本部署的 scope 词表把身份编进去(server durable
|
|
12
|
+
* scope ≡ principal 的恒等涟漪)。scope 若**在**唯一键里,重绑就可能与目的地已有的同逻辑键撞
|
|
13
|
+
* ⇒ `residualKey` 记下「唯一键去掉身份轴之后剩下的列」,preflight 按它查重叠。
|
|
14
|
+
* · **形Ⅲ 无身份轴**:键=session/ref/内容寻址(session_event / task_event / task_active /
|
|
15
|
+
* checkpoint_ctx / tool_result / snapshot_manifest / snapshot_blob / approval_batch /
|
|
16
|
+
* task_list_* / workflow_completion_inbox 的 session_id 那一半 …)⇒ **零改写**:归属随它挂靠的
|
|
17
|
+
* 资产(会话 / run / 内容)一起走,本表不列它们,也不该列 —— 列了就是给「跟着 session_id 走的行」
|
|
18
|
+
* 再造一个第二真源。
|
|
19
|
+
*
|
|
20
|
+
* ── 一张表两根身份轴的处置(agent_roster / background_agent)────────────────────────────────────
|
|
21
|
+
* 逐轴各一条腿,各带**自己**的 `residualKey`(= 该表唯一键去掉这根轴)。两条腿在**同一个事务**里
|
|
22
|
+
* 顺序跑;preflight 是按迁移**前**的库态逐轴算的,理论上仍存在一个跨轴的组合形撞唯一键
|
|
23
|
+
* (A 行只动 owner、B 行只动 scope 后同键)—— 那一形由 phase 3 事务里的唯一键违例**响亮拒 + 整体
|
|
24
|
+
* 回滚**兜住(两侧字节零变更),而不是靠 preflight 声称自己算全了。fail-closed,不 fail-open。
|
|
25
|
+
*
|
|
26
|
+
* ── 为什么腿的集合比小票逐字点名的多(**如实登记,候属主裁**)────────────────────────────────────
|
|
27
|
+
* 小票 §行迁/桶迁 点名的是 `session_meta.owner` / `session_policy` / checkpoint / mailbox / workflow /
|
|
28
|
+
* background-agent / snapshot / memory。本表在此之外还迁了 `task_run.owner`、`resume_anchor.owner`、
|
|
29
|
+
* `sendfile_link.scope`、`task_attachment.owner`、`approval_ask.owner`、`agent_roster` 两轴、
|
|
30
|
+
* `workflow_completion_inbox.owner` / `workflow_inbox_fence.owner` / `workflow_notify_journal.principal`、
|
|
31
|
+
* `sandbox_image_index.tenant_id`。判据不是「多迁点保险」,是小票自己写的那条**类判据**:
|
|
32
|
+
* 「形Ⅰ = owner/principal 结构位」「形Ⅱ = scope ≡ principal 分区面」—— 上面每一列都逐字落在这两句里,
|
|
33
|
+
* 且每一列都是**活的授权/可见性轴**(不是史实字段):`session_meta.owner` 迁了而 `task_run.owner` 没迁,
|
|
34
|
+
* 结果就是「会话看得见、它的 run 历史读不到」= 183 §6 明令要防的第一病形「owner 变了调用方够不着」。
|
|
35
|
+
* 反向的「不该迁」一律进 {@link NOT_MIGRATED_BY_DESIGN}(D1/D2 裁决号在案),不进本表。
|
|
36
|
+
*/
|
|
37
|
+
import { formatUserScope, encodeScopeSegment, SCOPE_SEGMENT_MAX_ENCODED } from "@sema-agent/core";
|
|
38
|
+
/**
|
|
39
|
+
* 全部重绑腿。**次序即执行序**(同一事务内顺序跑):同表两轴相邻,workflow 家族相邻
|
|
40
|
+
* (183 §4.3「run + journal 同事务」在本车是「**全部腿同一个事务**」的更强形)。
|
|
41
|
+
*/
|
|
42
|
+
export const REBIND_LEGS = [
|
|
43
|
+
// ── 形Ⅰ:owner/principal 结构位,不在任何唯一键里 ⇒ 结构性无冲突 ────────────────────────────
|
|
44
|
+
{
|
|
45
|
+
leg: "session_meta#owner",
|
|
46
|
+
table: "session_meta",
|
|
47
|
+
kind: "bulk-rebind",
|
|
48
|
+
action: "row-rewrite",
|
|
49
|
+
columns: ["owner"],
|
|
50
|
+
matchColumn: "owner",
|
|
51
|
+
encoding: "verbatim",
|
|
52
|
+
residualKey: undefined, // PK(session_id) 不含 owner
|
|
53
|
+
why: "会话归属主列(183 §4.3 形Ⅰ 首项;存量 `anon:shell-live` 哨兵行同走此腿——它只是一个 principal 字面量,不需要 ad hoc 分支)",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
leg: "task_run#owner",
|
|
57
|
+
table: "task_run",
|
|
58
|
+
kind: "bulk-rebind",
|
|
59
|
+
action: "row-rewrite",
|
|
60
|
+
columns: ["owner"],
|
|
61
|
+
matchColumn: "owner",
|
|
62
|
+
encoding: "verbatim",
|
|
63
|
+
residualKey: undefined, // PK(task_id)
|
|
64
|
+
why: "run 列表/属主门的过滤列(GET /v1/runs 与 trace 属主门读它)——不迁 = 会话迁了它的 run 历史读不到",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
leg: "resume_anchor#owner",
|
|
68
|
+
table: "resume_anchor",
|
|
69
|
+
kind: "bulk-rebind",
|
|
70
|
+
action: "row-rewrite",
|
|
71
|
+
columns: ["owner"],
|
|
72
|
+
matchColumn: "owner",
|
|
73
|
+
encoding: "verbatim",
|
|
74
|
+
residualKey: undefined, // PK(session_id, event_id)
|
|
75
|
+
why: "E18 anchor 的单库舰队纵深防御列(与 session 同生命周期)",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
leg: "sendfile_link#scope",
|
|
79
|
+
table: "sendfile_link",
|
|
80
|
+
kind: "bulk-rebind",
|
|
81
|
+
action: "row-rewrite",
|
|
82
|
+
columns: ["scope"],
|
|
83
|
+
matchColumn: "scope",
|
|
84
|
+
encoding: "verbatim",
|
|
85
|
+
residualKey: undefined, // PK(id)
|
|
86
|
+
why: "SendUserFile 台账的租户列(DDL 行内注:scope = VERIFIED principal verbatim)= 逐租户 list/revoke 句柄",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
leg: "task_attachment#owner",
|
|
90
|
+
table: "task_attachment",
|
|
91
|
+
kind: "bulk-rebind",
|
|
92
|
+
action: "row-rewrite",
|
|
93
|
+
columns: ["owner"],
|
|
94
|
+
matchColumn: "owner",
|
|
95
|
+
encoding: "verbatim",
|
|
96
|
+
residualKey: undefined, // PK(id)
|
|
97
|
+
why: "D-1 附件面的属主列(auth principal ?? default)",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
leg: "approval_ask#owner",
|
|
101
|
+
table: "approval_ask",
|
|
102
|
+
kind: "bulk-rebind",
|
|
103
|
+
action: "row-rewrite",
|
|
104
|
+
columns: ["owner"],
|
|
105
|
+
matchColumn: "owner",
|
|
106
|
+
encoding: "verbatim",
|
|
107
|
+
residualKey: undefined, // PK(ask_id);UNIQUE(task_id, idempotency_key) 不含 owner
|
|
108
|
+
why: "#151 流内审批 ask 行的租户列(读面按它过滤)。**decision_actor / decision_note 不动** —— 那是史实(D2)",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
leg: "sandbox_image_index#tenant_id",
|
|
112
|
+
table: "sandbox_image_index",
|
|
113
|
+
kind: "bulk-rebind",
|
|
114
|
+
action: "row-rewrite",
|
|
115
|
+
columns: ["tenant_id"],
|
|
116
|
+
matchColumn: "tenant_id",
|
|
117
|
+
encoding: "verbatim",
|
|
118
|
+
residualKey: undefined, // PK(id);UNIQUE(repo, digest) 不含 tenant_id
|
|
119
|
+
why: "镜像可见性的租户列(tenant-scoped 镜像只对该 principal 可见)—— 活的可见性轴,不是史实",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
leg: "workflow_completion_inbox#owner",
|
|
123
|
+
table: "workflow_completion_inbox",
|
|
124
|
+
kind: "bulk-rebind",
|
|
125
|
+
action: "row-rewrite",
|
|
126
|
+
columns: ["owner"],
|
|
127
|
+
matchColumn: "owner",
|
|
128
|
+
encoding: "verbatim",
|
|
129
|
+
residualKey: undefined, // PK(session_id, run_id)
|
|
130
|
+
why: "workflow 完成收件箱的属主列(投递门读它)",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
leg: "workflow_inbox_fence#owner",
|
|
134
|
+
table: "workflow_inbox_fence",
|
|
135
|
+
kind: "bulk-rebind",
|
|
136
|
+
action: "row-rewrite",
|
|
137
|
+
columns: ["owner"],
|
|
138
|
+
matchColumn: "owner",
|
|
139
|
+
encoding: "verbatim",
|
|
140
|
+
residualKey: undefined, // PK(session_id, kind, run_id)
|
|
141
|
+
why: "收件箱围栏行的属主列(与 inbox 同轴,漏迁 ⇒ 围栏对新身份失效)",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
leg: "workflow_notify_journal#principal",
|
|
145
|
+
table: "workflow_notify_journal",
|
|
146
|
+
kind: "bulk-rebind",
|
|
147
|
+
action: "row-rewrite",
|
|
148
|
+
columns: ["principal"],
|
|
149
|
+
matchColumn: "principal",
|
|
150
|
+
encoding: "verbatim",
|
|
151
|
+
residualKey: undefined, // PK(run_id)
|
|
152
|
+
why: "notify 日志的投递身份列(未 ack 的通知按它重投)",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
// 🔴 这条腿是**普查门逮出来的**(2026-08-10 建门当天):本表有 `principal` 与 `scope` **两根**身份轴,
|
|
156
|
+
// 初版只迁了 principal。漏掉 scope 的后果是恢复扫描按 `WHERE scope = <新身份>` 一条都捞不到,
|
|
157
|
+
// 未 ack 的通知对新身份永久不可见 —— 而 principal 那半迁了,黑盒上看起来「迁过了」。
|
|
158
|
+
leg: "workflow_notify_journal#scope",
|
|
159
|
+
table: "workflow_notify_journal",
|
|
160
|
+
kind: "bulk-rebind",
|
|
161
|
+
action: "bucket-rebind",
|
|
162
|
+
columns: ["scope"],
|
|
163
|
+
matchColumn: "scope",
|
|
164
|
+
encoding: "verbatim",
|
|
165
|
+
residualKey: undefined, // PK(run_id) 不含 scope
|
|
166
|
+
why: "notify 日志的 scope 分区(与 workflow run/journal 同轴同事务;principal 是投递身份、scope 是租户分区,两根轴各迁各的)",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
// 两根轴(scope_key / owner)都可能带旧身份 ⇒ 匹配面必须是它们的**并**,否则只在 owner 上带旧值的行找不到。
|
|
170
|
+
leg: "background_agent#record_json",
|
|
171
|
+
table: "background_agent",
|
|
172
|
+
kind: "blob-rewrite",
|
|
173
|
+
action: "row-rewrite",
|
|
174
|
+
columns: ["scope_key"],
|
|
175
|
+
matchColumn: "scope_key",
|
|
176
|
+
encoding: "verbatim",
|
|
177
|
+
residualKey: undefined,
|
|
178
|
+
blob: { column: "record_json", pk: ["scope_key", "handle"], fields: ["scope", "owner"], alsoMatch: ["owner"], revColumn: "rev", revInPayload: "rev" },
|
|
179
|
+
why: "store 自注「真源在 record_json,摘要投影截断是诚实形」—— 只迁投影列 = 说了算的那份没迁",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
leg: "background_agent#owner",
|
|
183
|
+
table: "background_agent",
|
|
184
|
+
kind: "bulk-rebind",
|
|
185
|
+
action: "row-rewrite",
|
|
186
|
+
columns: ["owner"],
|
|
187
|
+
matchColumn: "owner",
|
|
188
|
+
encoding: "verbatim",
|
|
189
|
+
residualKey: undefined, // PK(scope_key, handle) 不含 owner
|
|
190
|
+
why: "后台代理行的 default-deny 谓词两根轴之一(owner);另一根 scope 见下面的桶迁腿",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
// 🔴 载荷腿排在同表的列腿**之前**:改写要按**旧**身份值找行,列一旦换掉就再也找不到了。
|
|
194
|
+
leg: "checkpoint#checkpoint_blob",
|
|
195
|
+
table: "checkpoint",
|
|
196
|
+
kind: "blob-rewrite",
|
|
197
|
+
action: "row-rewrite",
|
|
198
|
+
columns: ["scope"],
|
|
199
|
+
matchColumn: "scope",
|
|
200
|
+
encoding: "verbatim",
|
|
201
|
+
residualKey: undefined, // 逐行按 PK 回写,不可能撞唯一键
|
|
202
|
+
blob: { column: "checkpoint", pk: ["token"], fields: ["scope"], revColumn: "rev" },
|
|
203
|
+
why: "core `Checkpoint.scope`(类型注:Multi-tenant isolation key)住在 blob 里 —— 只迁 `scope` 列会让载荷里的隔离键留在旧身份",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
leg: "checkpoint#scope",
|
|
207
|
+
table: "checkpoint",
|
|
208
|
+
kind: "bulk-rebind",
|
|
209
|
+
action: "bucket-rebind",
|
|
210
|
+
columns: ["scope"],
|
|
211
|
+
matchColumn: "scope",
|
|
212
|
+
encoding: "verbatim",
|
|
213
|
+
residualKey: undefined, // PK(token) 不含 scope ⇒ 桶迁语义、结构性无冲突
|
|
214
|
+
why: "durable HITL 检查点的租户分区列(scope = `principal ?? \"_\"`)——不迁 ⇒ 新身份 resume 不了自己的挂起任务",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
leg: "workflow_run#run_blob",
|
|
218
|
+
table: "workflow_run",
|
|
219
|
+
kind: "blob-rewrite",
|
|
220
|
+
action: "row-rewrite",
|
|
221
|
+
columns: ["scope"],
|
|
222
|
+
matchColumn: "scope",
|
|
223
|
+
encoding: "verbatim",
|
|
224
|
+
residualKey: undefined,
|
|
225
|
+
blob: { column: "run", pk: ["id"], fields: ["scope"], revColumn: "rev", revInPayload: "rev" },
|
|
226
|
+
why: "`WorkflowRunStore.get()` 是 `JSON.parse(run)` 且**只**覆盖 rev —— 不改载荷,`get().scope` 恒旧身份,回传给 scope-guarded 的 update() 就命中不了已迁移的行(活 workflow 挂死)",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
leg: "workflow_run#scope",
|
|
230
|
+
table: "workflow_run",
|
|
231
|
+
kind: "bulk-rebind",
|
|
232
|
+
action: "bucket-rebind",
|
|
233
|
+
columns: ["scope"],
|
|
234
|
+
matchColumn: "scope",
|
|
235
|
+
encoding: "verbatim",
|
|
236
|
+
residualKey: undefined, // PK(id)
|
|
237
|
+
why: "workflow run 行的 scope 分区(scope = `principal ?? \"default\"`)。183 §4.2 D4:不变量属 update 动词面,存储层迁移豁免",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
leg: "workflow_journal#scope",
|
|
241
|
+
table: "workflow_journal",
|
|
242
|
+
kind: "bulk-rebind",
|
|
243
|
+
action: "bucket-rebind",
|
|
244
|
+
columns: ["scope"],
|
|
245
|
+
matchColumn: "scope",
|
|
246
|
+
encoding: "verbatim",
|
|
247
|
+
residualKey: undefined, // PK(run_id, ordinal)
|
|
248
|
+
why: "resume 重放日志的 scope 守卫列 —— 与 run 行**同事务**(错配会让 load 守卫拒掉整条 resume)",
|
|
249
|
+
},
|
|
250
|
+
// ── 形Ⅱ:身份轴**在**唯一键里 ⇒ residualKey 非 undefined,preflight 必须逐族查重叠 ──────────
|
|
251
|
+
{
|
|
252
|
+
leg: "workflow_resume_claim#scope",
|
|
253
|
+
table: "workflow_resume_claim",
|
|
254
|
+
kind: "bulk-rebind",
|
|
255
|
+
action: "bucket-rebind",
|
|
256
|
+
columns: ["scope"],
|
|
257
|
+
matchColumn: "scope",
|
|
258
|
+
encoding: "verbatim",
|
|
259
|
+
residualKey: ["source_run_id"], // PK(source_run_id, scope)
|
|
260
|
+
why: "跨副本 resume 准入租约(与 run/journal 同族同事务)",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
leg: "mailbox#scope",
|
|
264
|
+
table: "mailbox",
|
|
265
|
+
kind: "bulk-rebind",
|
|
266
|
+
action: "bucket-rebind",
|
|
267
|
+
columns: ["scope_key", "scope"],
|
|
268
|
+
matchColumn: "scope_key",
|
|
269
|
+
encoding: "verbatim",
|
|
270
|
+
residualKey: ["handle"], // PK(scope_key, handle)
|
|
271
|
+
why: "信箱行的 scope 分区。`scope_key`(字节列)与 `scope`(可读列)存的是同一个值,必须一起改",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
leg: "mailbox_message#scope_key",
|
|
275
|
+
table: "mailbox_message",
|
|
276
|
+
kind: "bulk-rebind",
|
|
277
|
+
action: "bucket-rebind",
|
|
278
|
+
columns: ["scope_key"],
|
|
279
|
+
matchColumn: "scope_key",
|
|
280
|
+
encoding: "verbatim",
|
|
281
|
+
residualKey: ["handle", "seq"], // PK(scope_key, handle, seq)
|
|
282
|
+
why: "未投递通知 = 183 [3265] A 类资产(消息体随信箱同迁;本表无 `scope` 可读列)",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
leg: "background_agent#scope",
|
|
286
|
+
table: "background_agent",
|
|
287
|
+
kind: "bulk-rebind",
|
|
288
|
+
action: "bucket-rebind",
|
|
289
|
+
columns: ["scope_key", "scope"],
|
|
290
|
+
matchColumn: "scope_key",
|
|
291
|
+
encoding: "verbatim",
|
|
292
|
+
residualKey: ["handle"], // PK(scope_key, handle)
|
|
293
|
+
why: "后台代理行的 scope 分区(default-deny 谓词的第二根轴)",
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
leg: "agent_roster#owner",
|
|
297
|
+
table: "agent_roster",
|
|
298
|
+
kind: "bulk-rebind",
|
|
299
|
+
action: "bucket-rebind",
|
|
300
|
+
columns: ["owner_key", "owner"],
|
|
301
|
+
matchColumn: "owner_key",
|
|
302
|
+
encoding: "verbatim",
|
|
303
|
+
residualKey: ["name_key", "scope_key"], // PK(name_key, owner_key, scope_key)
|
|
304
|
+
why: "花名册行的 owner 轴(core 1.365 起 owner/scope 双轴必填 = default-deny 谓词的两根轴)",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
leg: "agent_roster#scope",
|
|
308
|
+
table: "agent_roster",
|
|
309
|
+
kind: "bulk-rebind",
|
|
310
|
+
action: "bucket-rebind",
|
|
311
|
+
columns: ["scope_key", "scope"],
|
|
312
|
+
matchColumn: "scope_key",
|
|
313
|
+
encoding: "verbatim",
|
|
314
|
+
residualKey: ["name_key", "owner_key"], // PK(name_key, owner_key, scope_key)
|
|
315
|
+
why: "花名册行的 scope 轴(与 owner 轴同表两根轴,逐轴各一条腿——见顶注「一张表两根身份轴」段)",
|
|
316
|
+
},
|
|
317
|
+
// ── 形Ⅱ(memory 族):scope 是 core 的 v2 typed 键,身份在**段**里 ⇒ 值对由 DISTINCT 扫描解出 ──
|
|
318
|
+
{
|
|
319
|
+
leg: "agent_memory_engine_entry#scope",
|
|
320
|
+
table: "agent_memory_engine_entry",
|
|
321
|
+
kind: "bulk-rebind",
|
|
322
|
+
action: "bucket-rebind",
|
|
323
|
+
columns: ["scope"],
|
|
324
|
+
matchColumn: "scope",
|
|
325
|
+
encoding: "memory-scope",
|
|
326
|
+
residualKey: ["slug"], // UNIQUE(scope, slug)
|
|
327
|
+
why: "记忆条目盘(user:/proj:/userproj: 三形键的 tenant 段 = principal)",
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
leg: "agent_memory_engine_cursor#scope",
|
|
331
|
+
table: "agent_memory_engine_cursor",
|
|
332
|
+
kind: "bulk-rebind",
|
|
333
|
+
action: "bucket-rebind",
|
|
334
|
+
columns: ["scope"],
|
|
335
|
+
matchColumn: "scope",
|
|
336
|
+
encoding: "memory-scope",
|
|
337
|
+
residualKey: [], // PK(scope) —— 唯一键**就是**身份轴:两侧各有一行即冲突
|
|
338
|
+
why: "记忆盘游标(每 scope 一行)",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
leg: "agent_memory_engine_sync_cursor#scope",
|
|
342
|
+
table: "agent_memory_engine_sync_cursor",
|
|
343
|
+
kind: "bulk-rebind",
|
|
344
|
+
action: "bucket-rebind",
|
|
345
|
+
columns: ["scope"],
|
|
346
|
+
matchColumn: "scope",
|
|
347
|
+
encoding: "memory-scope",
|
|
348
|
+
residualKey: ["peer"], // PK(scope, peer)
|
|
349
|
+
why: "记忆同步的 per-peer 游标",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
leg: "agent_memory_engine_push_queue#scope",
|
|
353
|
+
table: "agent_memory_engine_push_queue",
|
|
354
|
+
kind: "bulk-rebind",
|
|
355
|
+
action: "bucket-rebind",
|
|
356
|
+
columns: ["scope"],
|
|
357
|
+
matchColumn: "scope",
|
|
358
|
+
encoding: "memory-scope",
|
|
359
|
+
residualKey: undefined, // PK(id)
|
|
360
|
+
why: "记忆推送队列(未推完的条目随盘走)",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
leg: "agent_memory_engine_history#scope",
|
|
364
|
+
table: "agent_memory_engine_history",
|
|
365
|
+
kind: "bulk-rebind",
|
|
366
|
+
action: "bucket-rebind",
|
|
367
|
+
columns: ["scope"],
|
|
368
|
+
matchColumn: "scope",
|
|
369
|
+
encoding: "memory-scope",
|
|
370
|
+
residualKey: undefined, // PK(id, rev)
|
|
371
|
+
why: "记忆修订史的 scope 列(随盘走;`op`/`snapshot` 是内容不是身份)",
|
|
372
|
+
},
|
|
373
|
+
// ── 唯一一条要逐行重算主键的腿 ────────────────────────────────────────────────────────────────
|
|
374
|
+
{
|
|
375
|
+
leg: "session_policy#principal",
|
|
376
|
+
table: "session_policy",
|
|
377
|
+
kind: "session-policy-rekey",
|
|
378
|
+
action: "row-rewrite",
|
|
379
|
+
columns: ["principal"], // policy_key 由 (session_id, principal) 重算,不是等值搬运
|
|
380
|
+
matchColumn: "principal",
|
|
381
|
+
encoding: "verbatim",
|
|
382
|
+
residualKey: ["session_id"], // policy_key = sha256([session_id, principal]) ⇒ 同 session 的目的地行即冲突
|
|
383
|
+
why: "E6 逐会话逐 principal 的工具收紧规则:身份在**主键的 hash 原像**里,等值 UPDATE 够不着 ⇒ 逐行读出→重算 policy_key→改写",
|
|
384
|
+
},
|
|
385
|
+
];
|
|
386
|
+
/**
|
|
387
|
+
* server 侧**按设计不迁**的面(183 §12 裁决号在案)。这份清单进回执的 `notMigratedByDesign`,
|
|
388
|
+
* 让「按设计不迁」与「漏了没迁」在黑盒上可分(183 §10.5)。
|
|
389
|
+
*
|
|
390
|
+
* 🔴 D1 三员为什么同族:它们全是**按对齐窗口分桶的治理瞬态计数**(usage_window / cost_quota /
|
|
391
|
+
* rate_limit),窗口语义与身份绑在一起搬过去要跨窗口对齐,复杂度与价值不成比例;清零重计的代价是
|
|
392
|
+
* 新身份在当前窗口拿到一份新额度 —— 一次性部署级动作里这是可接受的,而且**方向偏松不偏紧**,
|
|
393
|
+
* 不会把人锁在门外。
|
|
394
|
+
* 🔴 D2 三员为什么不改:它们是**谁在什么时候做了什么**的史实字段。改写史实 = 伪造审计。
|
|
395
|
+
*/
|
|
396
|
+
/**
|
|
397
|
+
* 🔴 本数组的成员集合必须与 {@link NOT_MIGRATED_FACES}(冻结词表)**全等** —— 词表说有而这里没有
|
|
398
|
+
* 就是一条下游按名写了断言、回执里却永远不出现的幽灵成员。等式由 `test/adoption-arc.test.ts` 钉住。
|
|
399
|
+
* (建表时真发生过:`outcome-ledger` 一度只进了词表 —— 它压根没有身份轴,属于「无可迁」而不是
|
|
400
|
+
* 「按设计不迁」,两者混在一起会让 D2 的语义从「史实不改写」滑成「凡是没迁的都算设计」。)
|
|
401
|
+
*/
|
|
402
|
+
export const NOT_MIGRATED_BY_DESIGN = [
|
|
403
|
+
{ face: "usage-window", ruling: "D1", reason: "deployment token-governance window (usage_window): a windowed governance transient — not carried, recounted from zero" },
|
|
404
|
+
{ face: "cost-quota", ruling: "D1", reason: "per-window cumulative spend counter (cost_quota.scope_key) — same windowed-transient class as usage-window" },
|
|
405
|
+
{ face: "rate-limit", ruling: "D1", reason: "per-window request counter (rate_limit.limit_key) — same windowed-transient class as usage-window" },
|
|
406
|
+
{ face: "approval-exemption-grantor", ruling: "D2", reason: "approval_exemption.granted_by is a historical fact (who granted this exemption), not a live authorization axis" },
|
|
407
|
+
{ face: "approval-ask-decision-actor", ruling: "D2", reason: "approval_ask.decision_actor / decision_note record who decided — historical audit fields, never rewritten" },
|
|
408
|
+
{ face: "image-bake-requested-by", ruling: "D2", reason: "image_bake.requested_by records who requested the build — historical audit field" },
|
|
409
|
+
];
|
|
410
|
+
export const AFFECTED_CONFIG_TEMPLATES = [
|
|
411
|
+
{ deployment: "web-client-bff", key: "BFF injected principal (bff/server.mjs)", requiredValue: "%TO%" },
|
|
412
|
+
{ deployment: "cli", key: "~/.sema/cloud/credentials + cloud/profiles.json", requiredValue: "%TO%" },
|
|
413
|
+
{ deployment: "cli", key: "SEMA_LIVE_PRINCIPAL", requiredValue: "%TO%" },
|
|
414
|
+
{ deployment: "env", key: "REQUIRE_PRINCIPAL", requiredValue: "true (multi-tenant posture must stay on)" },
|
|
415
|
+
{ deployment: "core-decl", key: "localOwnerRules", requiredValue: "(withdraw — the declaration resolves to the adopted principal after adoption)" },
|
|
416
|
+
];
|
|
417
|
+
/**
|
|
418
|
+
* 目的地身份值的**列宽约束**(codex 对抗复审 R1-F3,亲核属实)。
|
|
419
|
+
*
|
|
420
|
+
* 🔴 病:请求面收 ≤190 字符的 principal,而迁移腿要把它写进**最窄**只有 `VARCHAR(64)` 的列
|
|
421
|
+
* (`sandbox_image_index.tenant_id`),两根键列还是 `VARBINARY(190)`(**字节**,不是字符),memory scope
|
|
422
|
+
* 更是在 principal 外面再套前缀 + 百分号编码(非 ASCII 膨胀约 9 倍)。于是一个**合法**请求可以跑到腿的
|
|
423
|
+
* 中途才被库以截断/超宽打回 —— 那时 phase 停在 INTENT,每次重发与每次 boot 扫描都会再撞一次同样的墙。
|
|
424
|
+
*
|
|
425
|
+
* 🔴 姿势:在**铸行之前**按最窄真列校验并 typed 拒(fail-closed)。**不**在这里加自动截断——截断身份
|
|
426
|
+
* 就是把 A 的数据交给 B。
|
|
427
|
+
*
|
|
428
|
+
* 另一条可选解是把 `tenant_id` 拓宽到与其它身份列同宽(183/codex 的「统一身份契约」建议)。本车不动
|
|
429
|
+
* 别的特性的 schema:那是一次跨车道的列变更,该由镜像面属主拍。此处如实登记这条不对称。
|
|
430
|
+
*/
|
|
431
|
+
export const IDENTITY_WIDTH_LIMITS = [
|
|
432
|
+
{ what: "sandbox_image_index.tenant_id VARCHAR(64)", unit: "chars", max: 64 },
|
|
433
|
+
{ what: "owner/principal/scope columns VARCHAR(190)", unit: "chars", max: 190 },
|
|
434
|
+
{ what: "mailbox/background_agent/agent_roster key columns VARBINARY(190) (MySQL: bytes, not characters)", unit: "bytes", max: 190 },
|
|
435
|
+
];
|
|
436
|
+
/**
|
|
437
|
+
* core 的段编码器对超长/不可编码的段是**抛错**而不是返回。这里把那个异常**转成调用方的拒绝理由**
|
|
438
|
+
* (错误本身被带出去,不是吞掉换个默认值)—— 拒绝理由要能指着具体是哪一侧、为什么装不下。
|
|
439
|
+
*/
|
|
440
|
+
function scopeSegmentRejection(label, principal) {
|
|
441
|
+
let encoded;
|
|
442
|
+
try {
|
|
443
|
+
encoded = encodeScopeSegment(principal);
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
return `${label} cannot be encoded as a core memory scope segment: ${err instanceof Error ? err.message : String(err)}`;
|
|
447
|
+
}
|
|
448
|
+
return encoded.length > SCOPE_SEGMENT_MAX_ENCODED
|
|
449
|
+
? `${label} does not fit core's memory scope-segment limit (${SCOPE_SEGMENT_MAX_ENCODED} encoded characters; percent-encoding expands non-ASCII ~9x)`
|
|
450
|
+
: undefined;
|
|
451
|
+
}
|
|
452
|
+
/** memory scope 列宽(两方言 VARCHAR(190) 同宽;段编码后才是真长度)。 */
|
|
453
|
+
export const MEMORY_SCOPE_MAX_CHARS = 190;
|
|
454
|
+
/**
|
|
455
|
+
* 目的地身份能不能被每一根被写列容纳?返回**人可读的拒绝理由**,`undefined` = 可以。
|
|
456
|
+
* 纯函数(零 I/O),所以校验发生在任何库动作之前。
|
|
457
|
+
*/
|
|
458
|
+
export function checkAdoptionWidths(fromPrincipal, toPrincipal) {
|
|
459
|
+
// 🔴 判据的**次序**有讲究(不是随手排):先查「写进列装不装得下」,再查「core 的段编码上限」。
|
|
460
|
+
// 反过来排的话,段上限(64 个**编码后**字符)对任何带 `:` 的 principal 都比 `tenant_id VARCHAR(64)`
|
|
461
|
+
// 更早触发 ⇒ 列宽那条臂**永远走不到**(一条摆着好看的死支;本仓有过「变异打在不可达死支上」的判例)。
|
|
462
|
+
const bytes = Buffer.byteLength(toPrincipal, "utf8");
|
|
463
|
+
for (const limit of IDENTITY_WIDTH_LIMITS) {
|
|
464
|
+
const got = limit.unit === "bytes" ? bytes : toPrincipal.length;
|
|
465
|
+
if (got > limit.max)
|
|
466
|
+
return `toPrincipal is ${got} ${limit.unit}, but ${limit.what} caps it at ${limit.max}`;
|
|
467
|
+
}
|
|
468
|
+
// **两侧都要查**(本车自测逮到,与 codex R3-F3 同族):`from` 不写进任何列,但它要经
|
|
469
|
+
// `encodeScopeSegment` 铸出 memory 扫描的前缀,而 core 对**段**的上限是 64 个编码后字符且是**抛错**。
|
|
470
|
+
// 只查 `to` 的话,一个超长的源身份会让弧在 phase 0 的扫描里抛一个未分类异常:意图行已经落库、
|
|
471
|
+
// phase 停在 INTENT,此后每次重发与每次 boot 续跑都确定性地再抛一次 —— 一条永远跑不完的弧。
|
|
472
|
+
for (const [label, p] of [["fromPrincipal", fromPrincipal], ["toPrincipal", toPrincipal]]) {
|
|
473
|
+
const rejection = scopeSegmentRejection(label, p);
|
|
474
|
+
if (rejection !== undefined)
|
|
475
|
+
return rejection;
|
|
476
|
+
}
|
|
477
|
+
// 派生的 memory scope 键(前缀 + 段编码)也要能装下 —— 非 ASCII 在这里膨胀最猛。
|
|
478
|
+
const derived = formatUserScope(toPrincipal);
|
|
479
|
+
if (derived.length > MEMORY_SCOPE_MAX_CHARS) {
|
|
480
|
+
return `the derived memory scope key for toPrincipal is ${derived.length} characters (percent-encoding expands non-ASCII ~9x), but the scope column caps it at ${MEMORY_SCOPE_MAX_CHARS}`;
|
|
481
|
+
}
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
/** `proj:` / `userproj:` 键的 tenant 段前缀(值对由 DISTINCT 扫描按前缀解出)。 */
|
|
485
|
+
export const MEMORY_PROJECT_SCOPE_PREFIXES = ["proj:", "userproj:"];
|
|
486
|
+
/** `verbatim` 轴的值对(纯数据,零 I/O)。 */
|
|
487
|
+
export function buildVerbatimPair(fromPrincipal, toPrincipal) {
|
|
488
|
+
return { from: fromPrincipal, to: toPrincipal };
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* memory scope 键的重写(纯函数,给扫描出来的每个 scope 值算新值)。
|
|
492
|
+
* · `user:<enc(from)>` ⇒ `user:<enc(to)>`(逐字等值);
|
|
493
|
+
* · `proj:<enc(from)>/…` / `userproj:<enc(from)>/…` ⇒ 只换 tenant 段,项目段原样;
|
|
494
|
+
* · 其它(`org:` 组织盘、无前缀的旧不透明键)⇒ `undefined` = **不属这个 principal,不动**。
|
|
495
|
+
*
|
|
496
|
+
* 🔴 为什么不用 `LIKE '<prefix>%'`:段是**百分号编码**的(`user%3Aweb-demo`),`%` 在 LIKE 里是通配符,
|
|
497
|
+
* 不逐字符转义就会误命中一大片。用「DISTINCT 扫 + 纯函数判前缀」把这道坑整个绕开:判定在 JS 里做,
|
|
498
|
+
* 落到 SQL 的永远只有**等值**谓词。
|
|
499
|
+
*/
|
|
500
|
+
export function rewriteMemoryScope(scope, fromPrincipal, toPrincipal) {
|
|
501
|
+
const fromUser = formatUserScope(fromPrincipal);
|
|
502
|
+
if (scope === fromUser)
|
|
503
|
+
return formatUserScope(toPrincipal);
|
|
504
|
+
const fromSeg = encodeScopeSegment(fromPrincipal);
|
|
505
|
+
const toSeg = encodeScopeSegment(toPrincipal);
|
|
506
|
+
for (const prefix of MEMORY_PROJECT_SCOPE_PREFIXES) {
|
|
507
|
+
const head = `${prefix}${fromSeg}/`;
|
|
508
|
+
if (scope.startsWith(head))
|
|
509
|
+
return `${prefix}${toSeg}/${scope.slice(head.length)}`;
|
|
510
|
+
}
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
//# sourceMappingURL=plan.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type AdoptionLogStore } from "../plugins/adoption-log-sql.js";
|
|
2
|
+
import type { SqlDialect } from "../plugins/sql-driver.js";
|
|
3
|
+
import { type AdoptionReceipt, type AdoptionRejectCode } from "./wire.js";
|
|
4
|
+
/** 冲突表族的逐项读数(进 409 响应体;运维据此知道**该去清理哪张表**,不是一句「冲突了」)。 */
|
|
5
|
+
export interface AdoptionConflict {
|
|
6
|
+
/** 腿标识(`<表名>#<身份轴>`)。 */
|
|
7
|
+
leg: string;
|
|
8
|
+
table: string;
|
|
9
|
+
/** 目的地侧与源侧共享同一逻辑键的行对数(>0 即冲突)。 */
|
|
10
|
+
overlapping: number;
|
|
11
|
+
}
|
|
12
|
+
/** 收编弧的三种结局。**回执与拒绝分家**:拒从不产出 200 回执(183 §7.3 拒绝可判别)。 */
|
|
13
|
+
export type AdoptionOutcome = {
|
|
14
|
+
kind: "receipt";
|
|
15
|
+
adoptionId: string;
|
|
16
|
+
receipt: AdoptionReceipt;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "rejected";
|
|
19
|
+
code: AdoptionRejectCode;
|
|
20
|
+
adoptionId: string;
|
|
21
|
+
detail: string;
|
|
22
|
+
conflicts: AdoptionConflict[];
|
|
23
|
+
} | {
|
|
24
|
+
kind: "source_already_bound";
|
|
25
|
+
adoptionId: string;
|
|
26
|
+
fromPrincipal: string;
|
|
27
|
+
boundTo: string;
|
|
28
|
+
};
|
|
29
|
+
export interface AdoptionRunnerDeps {
|
|
30
|
+
store: AdoptionLogStore;
|
|
31
|
+
dialect: SqlDialect;
|
|
32
|
+
/** 注入时钟(测试要确定性;生产传 `Date.now`)。 */
|
|
33
|
+
now: () => number;
|
|
34
|
+
/** 锁轮询次数/间隔——测试把它调到 1 次好确定性地打到「取不到锁」那条臂。 */
|
|
35
|
+
lockAttempts?: number;
|
|
36
|
+
lockSleepMs?: number;
|
|
37
|
+
logger?: {
|
|
38
|
+
info?(msg: string, meta?: unknown): void;
|
|
39
|
+
warn?(msg: string, meta?: unknown): void;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface AdoptionRunner {
|
|
43
|
+
start(fromPrincipal: string, toPrincipal: string): Promise<AdoptionOutcome>;
|
|
44
|
+
get(adoptionId: string): Promise<AdoptionOutcome | null>;
|
|
45
|
+
/** I6 server 同族:副本 boot 的在飞扫描(续跑或响亮拒,禁静默跳过)。 */
|
|
46
|
+
resumeInFlight(): Promise<{
|
|
47
|
+
resumed: string[];
|
|
48
|
+
stalled: string[];
|
|
49
|
+
rejected: string[];
|
|
50
|
+
failed: string[];
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
export declare function createAdoptionRunner(deps: AdoptionRunnerDeps): AdoptionRunner;
|
|
54
|
+
//# sourceMappingURL=runner.d.ts.map
|