@sema-agent/server 7.9.0 → 7.11.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 (82) hide show
  1. package/USAGE.md +6 -0
  2. package/dist/adoption/plan.d.ts +152 -0
  3. package/dist/adoption/plan.js +513 -0
  4. package/dist/adoption/runner.d.ts +54 -0
  5. package/dist/adoption/runner.js +505 -0
  6. package/dist/adoption/sql.d.ts +76 -0
  7. package/dist/adoption/sql.js +106 -0
  8. package/dist/adoption/wire.d.ts +250 -0
  9. package/dist/adoption/wire.js +153 -0
  10. package/dist/approval-card.d.ts +24 -0
  11. package/dist/approval-card.js +32 -0
  12. package/dist/approval-reconciler.d.ts +1 -1
  13. package/dist/approval-reconciler.js +1 -1
  14. package/dist/boot/adoption.d.ts +30 -0
  15. package/dist/boot/adoption.js +57 -0
  16. package/dist/boot/coordinators.d.ts +4 -0
  17. package/dist/boot/coordinators.js +3 -1
  18. package/dist/boot/parked-revive-gate.d.ts +38 -5
  19. package/dist/boot/parked-revive-gate.js +53 -6
  20. package/dist/boot/reapers.js +3 -3
  21. package/dist/boot/runner-deps.d.ts +10 -2
  22. package/dist/boot/runner-deps.js +12 -1
  23. package/dist/capabilities/repo-tools.d.ts +36 -2
  24. package/dist/capabilities/repo-tools.js +125 -11
  25. package/dist/capabilities/scenarios.d.ts +2 -2
  26. package/dist/capabilities/scenarios.js +1 -1
  27. package/dist/config-types.d.ts +22 -2
  28. package/dist/config.js +44 -2
  29. package/dist/fleet/fleet-terminal-window.d.ts +1 -1
  30. package/dist/fleet/fleet-terminal-window.js +1 -1
  31. package/dist/git-api-kind.d.ts +7 -0
  32. package/dist/git-api-kind.js +8 -0
  33. package/dist/http/routes/adoption.d.ts +26 -0
  34. package/dist/http/routes/adoption.js +120 -0
  35. package/dist/http/routes/approvals-assistant.js +2 -2
  36. package/dist/http/routes/capabilities.js +12 -0
  37. package/dist/http/routes/rules.d.ts +23 -0
  38. package/dist/http/routes/rules.js +117 -0
  39. package/dist/http/routes/shared-memory.d.ts +31 -0
  40. package/dist/http/routes/shared-memory.js +181 -0
  41. package/dist/http/routes/trace-usage.js +139 -3
  42. package/dist/http/server.d.ts +19 -1
  43. package/dist/http/server.js +47 -5
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/main.js +54 -5
  47. package/dist/observability/fail-open.d.ts +8 -0
  48. package/dist/observability/fail-open.js +8 -0
  49. package/dist/plugins/adoption-log-sql.d.ts +191 -0
  50. package/dist/plugins/adoption-log-sql.js +273 -0
  51. package/dist/plugins/checkpoint-store-sql.d.ts +22 -9
  52. package/dist/plugins/checkpoint-store-sql.js +40 -29
  53. package/dist/plugins/local-checkpoint-store.d.ts +11 -1
  54. package/dist/plugins/local-checkpoint-store.js +10 -2
  55. package/dist/plugins/memory-engine-pg.js +3 -3
  56. package/dist/plugins/permission-rule-store-sql.d.ts +242 -0
  57. package/dist/plugins/permission-rule-store-sql.js +817 -0
  58. package/dist/plugins/pg-pool.js +44 -7
  59. package/dist/plugins/run-store-sql.d.ts +3 -3
  60. package/dist/plugins/run-store-sql.js +3 -3
  61. package/dist/plugins/session-policy-store-sql.d.ts +6 -0
  62. package/dist/plugins/session-policy-store-sql.js +7 -1
  63. package/dist/plugins/shared-memory-store-sql.d.ts +223 -0
  64. package/dist/plugins/shared-memory-store-sql.js +516 -0
  65. package/dist/plugins/store-backend.d.ts +30 -0
  66. package/dist/plugins/store-backend.js +14 -0
  67. package/dist/plugins/tidb-pool.js +55 -8
  68. package/dist/plugins/workflow-journal-store-sql.d.ts +3 -3
  69. package/dist/plugins/workflow-journal-store-sql.js +6 -6
  70. package/dist/plugins/workflow-run-store-sql.d.ts +1 -1
  71. package/dist/plugins/workflow-run-store-sql.js +12 -12
  72. package/dist/rules-consent.d.ts +126 -0
  73. package/dist/rules-consent.js +198 -0
  74. package/dist/run-local.js +2 -2
  75. package/dist/shared-memory-scope-authorizer.d.ts +29 -0
  76. package/dist/shared-memory-scope-authorizer.js +17 -0
  77. package/dist/tool-approval.d.ts +55 -0
  78. package/dist/tool-approval.js +124 -5
  79. package/dist/trace/core-keyset-guard.d.ts +2 -2
  80. package/dist/trace/project.d.ts +1 -0
  81. package/dist/trace/project.js +1 -0
  82. package/package.json +3 -3
@@ -0,0 +1,516 @@
1
+ import { SHARED_MEMORY_READ_CAP_BYTES, SharedMemoryStoreError, } from "@sema-agent/core";
2
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
3
+ /** 唯一键冲突的方言判别(approval-ask/checkpoint 同一对常量)——mysql2 挂 `errno`,node-pg 挂 SQLSTATE。
4
+ * 只用来把并发登记撞 PK 翻成一句人话,不让驱动错误对象漏给调用方。 */
5
+ const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
6
+ const PG_UNIQUE_VIOLATION = "23505";
7
+ function isDupKey(dialect, err) {
8
+ if (err === null || typeof err !== "object")
9
+ return false;
10
+ // Reflect.get (not a spread): driver error classes carry these on the PROTOTYPE chain in some
11
+ // versions, and `{...err}` would silently miss them — a missed dup-key leaks the raw driver object.
12
+ return dialect === "tidb" ? Number(Reflect.get(err, "errno")) === DUP_ENTRY : String(Reflect.get(err, "code")) === PG_UNIQUE_VIOLATION;
13
+ }
14
+ /** Per-org plane state (`connecting` / `unavailable` / `connected`). One row per org scope. */
15
+ export const SHARED_MEMORY_SCOPE_TABLE = "shared_memory_scope";
16
+ /** The model-facing store registry: one row per browsable library. */
17
+ export const SHARED_MEMORY_STORE_TABLE = "shared_memory_store";
18
+ /** The documents. One row per (store, path). */
19
+ export const SHARED_MEMORY_ENTRY_TABLE = "shared_memory_entry";
20
+ /** core's store-id rule, restated on the WRITE side: `/^[A-Za-z0-9_.-]{1,64}$/` and no `__` (the
21
+ * protocol table's namespace separator). A row failing it would be dropped from every listing — an id
22
+ * the model cannot echo back is worse than an absent one — so we refuse to store it in the first place. */
23
+ const STORE_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
24
+ /** core's org scope shape, shared with org-memory-admission. */
25
+ const ORG_SCOPE_RE = /^org:\S{1,185}$/;
26
+ /** core's FORBIDDEN_PATH_CHARS, restated. A path carrying any of these is dropped by core's listing gate. */
27
+ const FORBIDDEN_PATH_CHARS = /[\p{Cc}\p{Cf}\p{Co}\p{Cn}\p{Default_Ignorable_Code_Point}\u2028\u2029\\]/u;
28
+ /** core's DOCUMENT_SUFFIXES, restated. */
29
+ const DOCUMENT_SUFFIXES = [".md", ".txt", ".json", ".jsonl"];
30
+ /** The `path` column width. core's own bound is 1024 UTF-16 units; the column is the tighter of the two
31
+ * and is enforced here so an over-long path fails LOUDLY instead of being truncated by the engine. */
32
+ const PATH_MAX_CHARS = 512;
33
+ const DESCRIPTION_MAX_CHARS = 512;
34
+ const DEFAULT_MAX_LIST_LIMIT = 500;
35
+ export function assertStoreId(storeId) {
36
+ if (!STORE_ID_RE.test(storeId) || storeId.includes("__")) {
37
+ throw new Error(`shared-memory store id ${JSON.stringify(storeId)} is not usable: it must match /^[A-Za-z0-9_.-]{1,64}$/ and must not contain "__"`);
38
+ }
39
+ }
40
+ export function assertScopeKey(scopeKey) {
41
+ if (!ORG_SCOPE_RE.test(scopeKey)) {
42
+ throw new Error(`shared-memory scope key ${JSON.stringify(scopeKey)} is not usable: it must match /^org:\\S{1,185}$/`);
43
+ }
44
+ }
45
+ /**
46
+ * WRITE-side document-path admission. Deliberately at least as strict as core's READ-side listing gate
47
+ * (`isListablePath`): a row core would silently drop is refused here where an operator can see it. The
48
+ * ONE rule that is stricter on purpose is the non-BMP refusal — see the header's ORDERING section.
49
+ */
50
+ export function assertDocumentPath(path) {
51
+ const refuse = (why) => {
52
+ throw new Error(`shared-memory document path ${JSON.stringify(path)} is not usable: ${why}`);
53
+ };
54
+ if (!path.startsWith("/"))
55
+ refuse("it must be absolute (leading \"/\")");
56
+ if (path.length > PATH_MAX_CHARS)
57
+ refuse(`it is ${path.length} UTF-16 units, over the ${PATH_MAX_CHARS} the column stores`);
58
+ if (FORBIDDEN_PATH_CHARS.test(path))
59
+ refuse("it carries a control/format/private-use character or a backslash");
60
+ const segments = path.slice(1).split("/");
61
+ if (!segments.every((s) => s !== "" && !s.startsWith(".")))
62
+ refuse("every folder segment must be non-empty and must not start with \".\"");
63
+ const last = segments.at(-1);
64
+ if (last === undefined || !DOCUMENT_SUFFIXES.some((s) => last.endsWith(s)))
65
+ refuse(`the filename must end in ${DOCUMENT_SUFFIXES.join(", ")}`);
66
+ // Non-BMP refusal: SQL orders by code point, core's cursor progress check compares in UTF-16 code
67
+ // units, and the two disagree ONLY above the BMP. Refusing here keeps "the listing is complete" a
68
+ // structural fact instead of a probabilistic one.
69
+ for (const ch of path) {
70
+ if (ch.codePointAt(0) > 0xffff)
71
+ refuse("it carries a supplementary-plane character, which would make SQL ordering and core's UTF-16 cursor ordering disagree");
72
+ }
73
+ }
74
+ /** `path` values that start with `prefix` live in `[prefix, succ(prefix))`. `undefined` upper bound ⇒
75
+ * the range degenerates to `>= prefix`, which is still a legal SUPERSET (core re-filters), just wider. */
76
+ function prefixUpperBound(prefix) {
77
+ const lastUnit = prefix.charCodeAt(prefix.length - 1);
78
+ // Stop short of the surrogate block: incrementing into 0xD800 would build a lone surrogate, which is
79
+ // not encodable as UTF-8 and would be rejected by both drivers.
80
+ if (!Number.isFinite(lastUnit) || lastUnit >= 0xd7ff)
81
+ return undefined;
82
+ return prefix.slice(0, -1) + String.fromCharCode(lastUnit + 1);
83
+ }
84
+ /** `unavailable` beats `connecting` beats `connected`. A caller whose orgs are PARTLY down is told the
85
+ * plane is down rather than served a silently partial store set — a partial set is indistinguishable
86
+ * from "your team's library has these three documents", which is the failure this state exists to name. */
87
+ function foldPlaneState(states) {
88
+ let sawConnecting = false;
89
+ for (const state of states) {
90
+ switch (state) {
91
+ case "unavailable":
92
+ return "unavailable";
93
+ case "connecting":
94
+ sawConnecting = true;
95
+ break;
96
+ case "connected":
97
+ break;
98
+ }
99
+ }
100
+ return sawConnecting ? "connecting" : "connected";
101
+ }
102
+ /** The stored word, re-read. An unknown word in the column is a corrupted/foreign write: refuse the
103
+ * whole snapshot rather than guess (this is the authorization/availability axis — no silent default). */
104
+ function planeStateOf(raw) {
105
+ const word = String(raw);
106
+ switch (word) {
107
+ case "connecting":
108
+ case "unavailable":
109
+ case "connected":
110
+ return word;
111
+ default:
112
+ throw new Error(`${SHARED_MEMORY_SCOPE_TABLE}.state holds an unknown word ${JSON.stringify(word)} (expected connecting|unavailable|connected)`);
113
+ }
114
+ }
115
+ function strOrNull(v) {
116
+ return v === null || v === undefined ? null : String(v);
117
+ }
118
+ function throwIfAborted(signal) {
119
+ if (signal?.aborted !== true)
120
+ return;
121
+ const reason = signal.reason;
122
+ throw reason instanceof Error ? reason : new Error("shared-memory read aborted");
123
+ }
124
+ /**
125
+ * 回滚,但**绝不让回滚的失败顶掉主错**(codex 轮2 收尾建议)。裸 `await conn.rollback()` 放在 catch 里,
126
+ * 一旦连接已经坏掉(回滚本身抛),抛出去的就变成了回滚错误,真正的失败原因当场丢失 —— 那是排障时最贵的
127
+ * 一种信息损失。这里吞的只有**次要**错误,而且吞得出声(warn 带上主错);连接由调用方的 finally 归还,
128
+ * 池会在下次取用时体检它。
129
+ */
130
+ async function rollbackPreservingError(conn, primary) {
131
+ try {
132
+ await conn.rollback();
133
+ }
134
+ catch (rollbackErr) {
135
+ console.warn("[shared-memory-store] rollback failed after a failed transaction; reporting the PRIMARY error:", { primary, rollbackErr });
136
+ }
137
+ }
138
+ function storeGone(storeId) {
139
+ // core folds `store_not_found` to "The memory store was not found — it may not be provisioned yet."
140
+ // The alternative (an empty listing) would read to the model as "your team's library is empty".
141
+ return new SharedMemoryStoreError("store_not_found", { message: `shared memory store ${JSON.stringify(storeId)} is no longer provisioned` });
142
+ }
143
+ function rowToStoreInfo(r) {
144
+ return { id: String(r.store_id), description: String(r.description), writable: Boolean(Number(r.writable)) };
145
+ }
146
+ /** 列举行。`sizeBytes` 取 `size_bytes` 列 —— 这是**展示用提示**,不是安全判据:该列独立可写,一个外来
147
+ * 写者可以让它与真实长度不符。任何**决定**(读帽)都必须现算 `OCTET_LENGTH(content)`(见 readDocument);
148
+ * 列举不现算是刻意的取舍:对一整窗 LONGTEXT 逐行算字节等于把每次列举变成全文扫描。 */
149
+ function rowToEntry(r) {
150
+ const updatedAt = strOrNull(r.updated_at_iso);
151
+ return {
152
+ path: String(r.path),
153
+ sizeBytes: Number(r.size_bytes),
154
+ ...(updatedAt !== null ? { updatedAt } : {}),
155
+ };
156
+ }
157
+ /**
158
+ * The dual-dialect implementation. Every statement is written out in BOTH texts at its call site
159
+ * (design/158 A12 判据:方言差异必须显式) — the `q(tidbSql, pgSql)` helper never builds one text.
160
+ */
161
+ export class SqlSharedMemoryStore {
162
+ db;
163
+ authorizer;
164
+ now;
165
+ maxListLimit;
166
+ constructor(db, authorizer, opts = {}) {
167
+ this.db = db;
168
+ this.authorizer = authorizer;
169
+ this.now = opts.now ?? Date.now;
170
+ this.maxListLimit = opts.maxListLimit ?? DEFAULT_MAX_LIST_LIMIT;
171
+ }
172
+ q(tidb, pg) {
173
+ return this.db.dialect === "tidb" ? tidb : pg;
174
+ }
175
+ // ───────────────────────────── the core-facing half (read only) ─────────────────────────────
176
+ /**
177
+ * ONE registry-binding snapshot per tool call, as core's seam requires: the state, the store identity
178
+ * set and each reader binding all come from this one call, so the set cannot change between "list the
179
+ * stores" and "resolve one". Document state stays live — which is why a store can still vanish between
180
+ * binding and read, the case {@link storeGone} names.
181
+ */
182
+ async snapshot(ctx, opts) {
183
+ throwIfAborted(opts.signal);
184
+ const grant = await this.authorizer.resolve(ctx.principal);
185
+ if (grant.kind === "unavailable") {
186
+ // Host-authored reason, bounded and folded to one line by core. NOT "no stores" — see header.
187
+ return { state: "unavailable", message: `shared memory authorization is unavailable (${grant.reason})` };
188
+ }
189
+ const scopes = [...new Set(grant.scopes)];
190
+ if (scopes.length === 0)
191
+ return { state: "connected", stores: [] };
192
+ throwIfAborted(opts.signal);
193
+ // 🔴 codex 复审轮2 F3:盘状态与库登记必须来自**同一个数据库快照**。两条独立的池查询之间,一次
194
+ // 导入可以把 scope 翻成 connecting 再发布新库 —— 请求于是看到"老状态 connected + 新库",一个
195
+ // 数据库瞬间都不曾出现过的组合,而 connecting 这道门的全部意义正是拦住半截导入的库。
196
+ // 事务成本(一次连接借出 + 两次往返)由 core 每个工具调用取一次快照的频次承担;这是授权/可见性
197
+ // 面,一致性优先于省一次借出。
198
+ const { scopeRows, storeRows } = await this.readBinding(scopes);
199
+ // A scope with no row has never been provisioned — that is "connected with nothing in it", not a fault.
200
+ const folded = foldPlaneState(scopeRows.map((r) => planeStateOf(r.state)));
201
+ if (folded === "unavailable") {
202
+ const message = strOrNull(scopeRows.find((r) => planeStateOf(r.state) === "unavailable")?.message);
203
+ return { state: "unavailable", ...(message !== null ? { message } : {}) };
204
+ }
205
+ if (folded === "connecting")
206
+ return { state: "connecting" };
207
+ return {
208
+ state: "connected",
209
+ stores: storeRows.map((r) => ({
210
+ info: rowToStoreInfo(r),
211
+ // The reader is PINNED to (store, scope) taken at binding time: a store re-homed to another org
212
+ // between snapshot and read cannot be reached through the stale binding (it reads as vanished).
213
+ reader: this.readerFor(String(r.store_id), String(r.scope_key)),
214
+ })),
215
+ };
216
+ }
217
+ /**
218
+ * 盘状态 + 库登记,**一个事务一个快照**(轮2 F3 修)。方言差异显式:
219
+ * · MySQL/TiDB 的默认隔离级别就是 REPEATABLE READ ⇒ 一个事务内的两条 SELECT 天然同快照;
220
+ * · PG 默认 READ COMMITTED,**每条语句各自取快照** ⇒ 必须显式抬到 REPEATABLE READ,否则这个事务
221
+ * 对本条竞态毫无作用(那正是"包了事务就以为一致"最容易踩空的地方)。
222
+ */
223
+ async readBinding(scopes) {
224
+ const conn = await this.db.connect();
225
+ try {
226
+ await conn.begin();
227
+ if (this.db.dialect === "pg")
228
+ await conn.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
229
+ const scopeRows = await conn.query(this.q(`SELECT scope_key, state, message FROM ${SHARED_MEMORY_SCOPE_TABLE} WHERE scope_key IN (${scopes.map(() => "?").join(", ")})`, `SELECT scope_key, state, message FROM ${SHARED_MEMORY_SCOPE_TABLE} WHERE scope_key IN (${scopes.map((_, i) => `$${i + 1}`).join(", ")})`), [...scopes]);
230
+ const storeRows = await conn.query(this.q(`SELECT store_id, scope_key, description, writable FROM ${SHARED_MEMORY_STORE_TABLE} WHERE scope_key IN (${scopes.map(() => "?").join(", ")}) ORDER BY store_id`, `SELECT store_id, scope_key, description, writable FROM ${SHARED_MEMORY_STORE_TABLE} WHERE scope_key IN (${scopes.map((_, i) => `$${i + 1}`).join(", ")}) ORDER BY store_id`), [...scopes]);
231
+ await conn.commit();
232
+ return { scopeRows: scopeRows.rows, storeRows: storeRows.rows };
233
+ }
234
+ catch (err) {
235
+ await rollbackPreservingError(conn, err);
236
+ throw err;
237
+ }
238
+ finally {
239
+ conn.release();
240
+ }
241
+ }
242
+ readerFor(storeId, scopeKey) {
243
+ return {
244
+ list: (prefix, opts) => this.listDocuments(storeId, scopeKey, prefix, opts),
245
+ read: (path, opts) => this.readDocument(storeId, scopeKey, path, opts),
246
+ };
247
+ }
248
+ /** One keyset window. Always the PAGED shape — a large library must never be materialized per call. */
249
+ async listDocuments(storeId, scopeKey, prefix, opts) {
250
+ throwIfAborted(opts.signal);
251
+ const limit = Math.max(1, Math.min(Math.floor(opts.limit ?? this.maxListLimit) || 1, this.maxListLimit));
252
+ // The predicate set is genuinely dynamic (cursor and prefix are each optional and independent), so
253
+ // the two texts are built from ONE clause list rather than written out twice — but the divergence
254
+ // stays visible: `ph()` is the only thing that differs, and both texts are logged side by side by
255
+ // the parity harness. Column list and ORDER BY are byte-identical on both dialects.
256
+ const clauses = [];
257
+ const params = [];
258
+ const bind = (value, clause) => {
259
+ params.push(value);
260
+ const position = params.length;
261
+ clauses.push((ph) => clause(ph(position)));
262
+ };
263
+ // 🔴 codex 复审轮1 F4(真缺陷,已红先复现):列举必须**在主查询里**要求绑定时的 scope,不能只在
264
+ // 空结果时补查。原式只按 store_id 过滤 ⇒ 一个 id 被回收给另一个 org 之后,旧 reader 只要新属主有
265
+ // 文档就能列出对方的 path/大小/时间(正文读那条腿一直核 scope,所以这是一次纯元数据泄漏——更隐蔽)。
266
+ bind(scopeKey, (p) => `s.scope_key = ${p}`);
267
+ bind(storeId, (p) => `e.store_id = ${p}`);
268
+ // EXCLUSIVE lower bound — core's cursor vocabulary, unchanged.
269
+ if (opts.cursor !== undefined)
270
+ bind(opts.cursor, (p) => `e.path > ${p}`);
271
+ if (prefix !== undefined && prefix !== "" && prefix !== "/") {
272
+ bind(prefix, (p) => `e.path >= ${p}`);
273
+ const upper = prefixUpperBound(prefix);
274
+ // Absent upper bound ⇒ the range degenerates to `>= prefix`: a WIDER superset, which core
275
+ // re-filters. Never a miss, so this arm needs no compensating knob.
276
+ if (upper !== undefined)
277
+ bind(upper, (p) => `e.path < ${p}`);
278
+ }
279
+ const whereFor = (ph) => clauses.map((c) => c(ph)).join(" AND ");
280
+ const limitPosition = params.length + 1;
281
+ const from = `FROM ${SHARED_MEMORY_ENTRY_TABLE} e JOIN ${SHARED_MEMORY_STORE_TABLE} s ON s.store_id = e.store_id`;
282
+ // Fetch one past the window: the extra row IS the "more remain" evidence, so `nextCursor` is a fact
283
+ // rather than a guess (core consumes it as a HAS-MORE declaration).
284
+ const res = await this.db.query(this.q(`SELECT e.path AS path, e.size_bytes AS size_bytes, e.updated_at_iso AS updated_at_iso ${from} WHERE ${whereFor(() => "?")} ORDER BY e.path LIMIT ?`, `SELECT e.path AS path, e.size_bytes AS size_bytes, e.updated_at_iso AS updated_at_iso ${from} WHERE ${whereFor((n) => `$${n}`)} ORDER BY e.path LIMIT $${limitPosition}`), [...params, limit + 1]);
285
+ if (res.rows.length === 0 && !(await this.storeExists(storeId, scopeKey)))
286
+ throw storeGone(storeId);
287
+ const window = res.rows.slice(0, limit).map(rowToEntry);
288
+ const hasMore = res.rows.length > limit;
289
+ const last = window.at(-1);
290
+ return { entries: window, ...(hasMore && last !== undefined ? { nextCursor: last.path } : {}) };
291
+ }
292
+ /** `null` = the document does not exist. Absence is a VALUE here; only a VANISHED STORE is an error. */
293
+ async readDocument(storeId, scopeKey, path, opts) {
294
+ throwIfAborted(opts.signal);
295
+ // One statement answers both questions: the LEFT JOIN keeps the store row even when the document is
296
+ // absent, so zero rows means the STORE is gone (error) and a row with a NULL path means the DOCUMENT
297
+ // is gone (value). Two separate SELECTs would leave a window where they disagree.
298
+ //
299
+ // 🔴 codex 轮1 F2b + 轮2 F1:`content` 走一层 CASE 门,超帽的行**在数据库里就不发字节** —— 否则一份
300
+ // LONGTEXT 级的文档每次读都要整个物化进进程内存,而它到了 core 那里横竖是被拒的。写面已经把帽挡在
301
+ // 入库(见 putDocument),这道是第二层:接管旧版/别的写者留下的行。
302
+ //
303
+ // 判据取 `OCTET_LENGTH(e.content)` 而**不是** `size_bytes` 列(轮2 修):那一列是独立可写的元数据,
304
+ // 没有任何约束把它绑在真实内容长度上。**低报**方向才是危险的那个 —— 一行"小 size_bytes + 巨大
305
+ // content"会让 CASE 门整个失效并把全文物化。真实字节数由引擎现算,元数据说什么都不影响安全判定
306
+ // (`size_bytes` 因此降级为纯展示用的列举提示,见 rowToEntry)。
307
+ const res = await this.db.query(this.q(`SELECT e.path AS path, OCTET_LENGTH(e.content) AS content_bytes, CASE WHEN OCTET_LENGTH(e.content) <= ? THEN e.content ELSE NULL END AS content, e.updated_at_iso AS updated_at_iso
308
+ FROM ${SHARED_MEMORY_STORE_TABLE} s
309
+ LEFT JOIN ${SHARED_MEMORY_ENTRY_TABLE} e ON e.store_id = s.store_id AND e.path = ?
310
+ WHERE s.store_id = ? AND s.scope_key = ?`, `SELECT e.path AS path, OCTET_LENGTH(e.content) AS content_bytes, CASE WHEN OCTET_LENGTH(e.content) <= $1 THEN e.content ELSE NULL END AS content, e.updated_at_iso AS updated_at_iso
311
+ FROM ${SHARED_MEMORY_STORE_TABLE} s
312
+ LEFT JOIN ${SHARED_MEMORY_ENTRY_TABLE} e ON e.store_id = s.store_id AND e.path = $2
313
+ WHERE s.store_id = $3 AND s.scope_key = $4`), [SHARED_MEMORY_READ_CAP_BYTES, path, storeId, scopeKey]);
314
+ const row = res.rows[0];
315
+ if (row === undefined)
316
+ throw storeGone(storeId);
317
+ if (strOrNull(row.path) === null)
318
+ return null;
319
+ if (Number(row.content_bytes) > SHARED_MEMORY_READ_CAP_BYTES) {
320
+ // A typed refusal, not a truncation and not a `null`: core folds `refused` with the host reason,
321
+ // and the model is told the document exists but cannot be served — which is true. The byte count
322
+ // in the message is the ENGINE's, so it is right even when the metadata column lies.
323
+ throw new SharedMemoryStoreError("refused", {
324
+ reason: "document_too_large",
325
+ message: `shared memory document ${JSON.stringify(path)} is ${String(row.content_bytes)} bytes, over the ${SHARED_MEMORY_READ_CAP_BYTES}-byte read cap`,
326
+ });
327
+ }
328
+ const updatedAt = strOrNull(row.updated_at_iso);
329
+ // Content is returned code-unit EXACT: no normalization, no scrubbing. core owns sanitizing, and a
330
+ // provider that pre-sanitized would make the model read different bytes than the library holds.
331
+ return { content: String(row.content), ...(updatedAt !== null ? { updatedAt } : {}) };
332
+ }
333
+ async storeExists(storeId, scopeKey) {
334
+ const res = await this.db.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ?`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2`), [storeId, scopeKey]);
335
+ return res.rows.length > 0;
336
+ }
337
+ // ───────────────────────── the DEPLOYMENT admin half (never model-facing) ─────────────────────────
338
+ /** Declare an org's plane state. Absent row ⇒ `connected` (a never-provisioned org simply has no stores). */
339
+ async putScopeState(scopeKey, state, message) {
340
+ assertScopeKey(scopeKey);
341
+ const now = this.now();
342
+ await this.db.query(this.q(`INSERT INTO ${SHARED_MEMORY_SCOPE_TABLE} (scope_key, state, message, updated_at_ms) VALUES (?, ?, ?, ?)
343
+ ON DUPLICATE KEY UPDATE state = VALUES(state), message = VALUES(message), updated_at_ms = VALUES(updated_at_ms)`, `INSERT INTO ${SHARED_MEMORY_SCOPE_TABLE} (scope_key, state, message, updated_at_ms) VALUES ($1, $2, $3, $4)
344
+ ON CONFLICT (scope_key) DO UPDATE SET state = EXCLUDED.state, message = EXCLUDED.message, updated_at_ms = EXCLUDED.updated_at_ms`), [scopeKey, state, message ?? null, now]);
345
+ }
346
+ /**
347
+ * Register (or re-describe) one browsable store.
348
+ *
349
+ * 🔴 codex 复审轮1 F1(真缺陷,已红先复现):**属主 org 不可变**。原式是一条 upsert,`scope_key` 跟着
350
+ * 被改写 —— 而文档行只按 `store_id` 编键、原地不动。于是一次撞 id 的导入(管理面手滑、重试、坏配置,
351
+ * 零竞态)就把 A 的整座库连内容一起交给了 B,同时 A 失去它。所以这里改成:已存在的 id 只允许它自己的
352
+ * org 更新描述/writable 位,换 org 一律响亮拒。真要迁库,那是一次需要显式审计的搬迁(先删再建,内容
353
+ * 由搬迁方决定重导还是丢弃),不能是一条 upsert 的副作用。
354
+ *
355
+ * 事务 + 行锁:属主判定与写入之间不留窗口(同一把锁 {@link deleteStore} 也拿)。并发到达的两个新建
356
+ * 会有一个撞 PK —— 那条也翻成同一句拒绝,不让驱动错误对象漏给调用方。
357
+ */
358
+ async putStore(record) {
359
+ assertStoreId(record.storeId);
360
+ assertScopeKey(record.scopeKey);
361
+ if (record.description.length > DESCRIPTION_MAX_CHARS) {
362
+ throw new Error(`shared-memory store ${JSON.stringify(record.storeId)} description is ${record.description.length} chars, over the ${DESCRIPTION_MAX_CHARS} the column stores`);
363
+ }
364
+ const now = this.now();
365
+ // 冲突事实在事务里查明、在事务外抛:避免"分支里 rollback + catch 里再 rollback"的双回滚吞异常形。
366
+ let ownedBy;
367
+ const conn = await this.db.connect();
368
+ try {
369
+ await conn.begin(); // 为什么不是 beginPessimistic:见 {@link SqlSharedMemoryStore} 头注的"事务与锁"段
370
+ const existing = await conn.query(this.q(`SELECT scope_key FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? FOR UPDATE`, `SELECT scope_key FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 FOR UPDATE`), [record.storeId]);
371
+ const owner = existing.rows[0] === undefined ? undefined : String(existing.rows[0].scope_key);
372
+ if (owner !== undefined && owner !== record.scopeKey) {
373
+ ownedBy = owner;
374
+ await conn.rollback();
375
+ }
376
+ else if (owner !== undefined) {
377
+ await conn.query(this.q(`UPDATE ${SHARED_MEMORY_STORE_TABLE} SET description = ?, writable = ?, updated_at_ms = ? WHERE store_id = ?`, `UPDATE ${SHARED_MEMORY_STORE_TABLE} SET description = $1, writable = $2, updated_at_ms = $3 WHERE store_id = $4`), [record.description, record.writable ? 1 : 0, now, record.storeId]);
378
+ await conn.commit();
379
+ }
380
+ else {
381
+ await conn.query(this.q(`INSERT INTO ${SHARED_MEMORY_STORE_TABLE} (store_id, scope_key, description, writable, updated_at_ms) VALUES (?, ?, ?, ?, ?)`, `INSERT INTO ${SHARED_MEMORY_STORE_TABLE} (store_id, scope_key, description, writable, updated_at_ms) VALUES ($1, $2, $3, $4, $5)`), [record.storeId, record.scopeKey, record.description, record.writable ? 1 : 0, now]);
382
+ await conn.commit();
383
+ }
384
+ }
385
+ catch (err) {
386
+ await rollbackPreservingError(conn, err);
387
+ if (isDupKey(this.db.dialect, err)) {
388
+ throw new Error(`shared-memory store id ${JSON.stringify(record.storeId)} was concurrently registered by another writer — store ids are globally unique and their owning org is immutable`);
389
+ }
390
+ throw err;
391
+ }
392
+ finally {
393
+ conn.release();
394
+ }
395
+ if (ownedBy !== undefined) {
396
+ throw new Error(`shared-memory store id ${JSON.stringify(record.storeId)} is already owned by ${JSON.stringify(ownedBy)} — a store's org is IMMUTABLE (re-homing it would hand every document already written under it to ${JSON.stringify(record.scopeKey)}). Delete it explicitly first if a transfer is really intended.`);
397
+ }
398
+ }
399
+ /**
400
+ * Write one document. Refuses when the store is not registered — an orphan document is unreachable
401
+ * through the read face, so accepting it would be a silent no-op the writer never learns about, AND a
402
+ * live landmine: whoever re-registers that id later inherits it.
403
+ *
404
+ * 🔴 codex 复审轮1 F2/F3 两修:
405
+ * · 帽在**入库**成立 —— 超 `SHARED_MEMORY_READ_CAP_BYTES` 的文档 core 横竖恒拒(从不截断),存下来
406
+ * 只是一行永远读不出来的死数据,却让每次读都要物化它。写面拒 = 那个帽在结构上为真。库里真有超大
407
+ * 文档的话,导入方分块——这是它自己就得做的事,因为模型面本来也读不到整份。
408
+ * · 存在性检查与写入**同事务同行锁**(与 {@link deleteStore} 争同一把):否则一次并发删除可以从检查
409
+ * 与插入之间穿过去,留下一行孤儿文档 —— 被删的内容在 id 复用时复活。
410
+ */
411
+ async putDocument(record) {
412
+ assertStoreId(record.storeId);
413
+ assertScopeKey(record.scopeKey);
414
+ assertDocumentPath(record.path);
415
+ const sizeBytes = Buffer.byteLength(record.content, "utf8");
416
+ if (sizeBytes > SHARED_MEMORY_READ_CAP_BYTES) {
417
+ throw new Error(`shared-memory document ${JSON.stringify(record.path)} is ${sizeBytes} bytes, over the ${SHARED_MEMORY_READ_CAP_BYTES}-byte shared-store read cap — core refuses an over-cap document outright (it is never truncated), so storing it would create a row nothing can ever read. Split it at import.`);
418
+ }
419
+ const now = this.now();
420
+ let unregistered = false;
421
+ const conn = await this.db.connect();
422
+ try {
423
+ await conn.begin(); // 为什么不是 beginPessimistic:见 {@link SqlSharedMemoryStore} 头注的"事务与锁"段
424
+ const registered = await conn.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ? FOR UPDATE`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2 FOR UPDATE`), [record.storeId, record.scopeKey]);
425
+ if (registered.rows.length === 0) {
426
+ unregistered = true;
427
+ await conn.rollback();
428
+ }
429
+ else {
430
+ await conn.query(this.q(`INSERT INTO ${SHARED_MEMORY_ENTRY_TABLE} (store_id, path, content, updated_at_iso, size_bytes, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?)
431
+ ON DUPLICATE KEY UPDATE content = VALUES(content), updated_at_iso = VALUES(updated_at_iso), size_bytes = VALUES(size_bytes), updated_at_ms = VALUES(updated_at_ms)`, `INSERT INTO ${SHARED_MEMORY_ENTRY_TABLE} (store_id, path, content, updated_at_iso, size_bytes, updated_at_ms) VALUES ($1, $2, $3, $4, $5, $6)
432
+ ON CONFLICT (store_id, path) DO UPDATE SET content = EXCLUDED.content, updated_at_iso = EXCLUDED.updated_at_iso, size_bytes = EXCLUDED.size_bytes, updated_at_ms = EXCLUDED.updated_at_ms`), [record.storeId, record.path, record.content, record.updatedAtIso ?? null, sizeBytes, now]);
433
+ await conn.commit();
434
+ }
435
+ }
436
+ catch (err) {
437
+ await rollbackPreservingError(conn, err);
438
+ throw err;
439
+ }
440
+ finally {
441
+ conn.release();
442
+ }
443
+ if (unregistered) {
444
+ throw new Error(`shared-memory store ${JSON.stringify(record.storeId)} is not registered under ${JSON.stringify(record.scopeKey)} — either it was never created, or the id has since been recycled by another org (call putStore() first; a stale retry must NOT land in the new tenant's library)`);
445
+ }
446
+ }
447
+ /** Remove one document. Scope-fenced like {@link putDocument} (a stale delete must not reach a
448
+ * successor tenant's library). Returns whether a row was actually there. */
449
+ async deleteDocument(storeId, scopeKey, path) {
450
+ await this.assertOwnedBy(storeId, scopeKey);
451
+ const res = await this.db.query(this.q(`DELETE FROM ${SHARED_MEMORY_ENTRY_TABLE} WHERE store_id = ? AND path = ?`, `DELETE FROM ${SHARED_MEMORY_ENTRY_TABLE} WHERE store_id = $1 AND path = $2`), [storeId, path]);
452
+ return res.affected > 0;
453
+ }
454
+ /** 属主围栏的共用断言(轮3 F1)。**不**覆盖同一个 org 内的"删了又建"代际重用——那不是跨租户面,
455
+ * 真要挡住需要一枚不可变的 generation token;这里如实说明边界,不假装它被覆盖了。 */
456
+ async assertOwnedBy(storeId, scopeKey) {
457
+ assertStoreId(storeId);
458
+ assertScopeKey(scopeKey);
459
+ const res = await this.db.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ?`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2`), [storeId, scopeKey]);
460
+ if (res.rows.length === 0) {
461
+ throw new Error(`shared-memory store ${JSON.stringify(storeId)} is not registered under ${JSON.stringify(scopeKey)} — either it does not exist, or the id has since been recycled by another org`);
462
+ }
463
+ }
464
+ /**
465
+ * De-register a store AND its documents, ATOMICALLY.
466
+ *
467
+ * 🔴 codex 复审轮1 F3(真缺陷):原式是两条独立语句。崩在中间会留下「登记着但被悄悄清空」的库,而
468
+ * 并发的 {@link putDocument} 能从检查与插入之间穿过去留下孤儿行 —— 被删的内容会在这个 id 被重新登记时
469
+ * 复活。现在两条删除同事务,并且**先拿注册行的行锁**:写面争的是同一把锁,两者因此串行。
470
+ *
471
+ * 🔴 轮3 F1 同族:必须带 `scopeKey` 围栏。id 删后可复用,而一条**迟到的** deleteStore 若只按 id 寻址,
472
+ * 会把继任租户刚建好的库连内容一起抹掉 —— 一次歧义的重试变成一次跨租户的破坏性操作。
473
+ */
474
+ async deleteStore(storeId, scopeKey) {
475
+ assertStoreId(storeId);
476
+ assertScopeKey(scopeKey);
477
+ let notOwned = false;
478
+ const conn = await this.db.connect();
479
+ try {
480
+ await conn.begin(); // 为什么不是 beginPessimistic:见 {@link SqlSharedMemoryStore} 头注的"事务与锁"段
481
+ const owned = await conn.query(this.q(`SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ? AND scope_key = ? FOR UPDATE`, `SELECT store_id FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1 AND scope_key = $2 FOR UPDATE`), [storeId, scopeKey]);
482
+ if (owned.rows.length === 0) {
483
+ notOwned = true;
484
+ await conn.rollback();
485
+ }
486
+ else {
487
+ await conn.query(this.q(`DELETE FROM ${SHARED_MEMORY_ENTRY_TABLE} WHERE store_id = ?`, `DELETE FROM ${SHARED_MEMORY_ENTRY_TABLE} WHERE store_id = $1`), [storeId]);
488
+ await conn.query(this.q(`DELETE FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = ?`, `DELETE FROM ${SHARED_MEMORY_STORE_TABLE} WHERE store_id = $1`), [storeId]);
489
+ await conn.commit();
490
+ }
491
+ }
492
+ catch (err) {
493
+ await rollbackPreservingError(conn, err);
494
+ throw err;
495
+ }
496
+ finally {
497
+ conn.release();
498
+ }
499
+ if (notOwned) {
500
+ throw new Error(`shared-memory store ${JSON.stringify(storeId)} is not registered under ${JSON.stringify(scopeKey)} — refusing to delete: either it is already gone, or the id has since been recycled by another org (a stale retry must never erase the successor's library)`);
501
+ }
502
+ }
503
+ }
504
+ /** MySQL-protocol (MySQL / TiDB) binding. */
505
+ export class TiDBSharedMemoryStore extends SqlSharedMemoryStore {
506
+ constructor(pool, authorizer, opts) {
507
+ super(mysqlDriver(pool), authorizer, opts);
508
+ }
509
+ }
510
+ /** PostgreSQL binding. */
511
+ export class PgSharedMemoryStore extends SqlSharedMemoryStore {
512
+ constructor(pool, authorizer, opts) {
513
+ super(pgDriver(pool), authorizer, opts);
514
+ }
515
+ }
516
+ //# sourceMappingURL=shared-memory-store-sql.js.map
@@ -22,6 +22,9 @@ import { PgCostQuota } from "./pg-cost-quota.js";
22
22
  import { TiDBRateLimiter } from "./tidb-rate-limiter.js";
23
23
  import type { CounterDegradeHook } from "./write-behind-counter.js";
24
24
  import { PgRateLimiter } from "./pg-rate-limiter.js";
25
+ import { type SqlSharedMemoryStore, type SharedMemoryScopeAuthorizer } from "./shared-memory-store-sql.js";
26
+ import { type AdoptionLogStore } from "./adoption-log-sql.js";
27
+ import { type PermissionRuleStores } from "./permission-rule-store-sql.js";
25
28
  export type RunStore = TiDBRunStore | PgRunStore | FileRunStore;
26
29
  /** E18 resume-at eventId→entryId anchor map — tidb/pg/local 3-backend (works LOCAL; needs only the session tree +
27
30
  * this map, no cloud-only checkpoint). Union = the three nominal twins; consumers use put/resolve/deleteBySession.
@@ -125,6 +128,20 @@ export interface StoreBackend {
125
128
  * 无 backend 的 env-only worker 压根没有 `StoreBackend` ⇒ 协调器拿到 undefined askStore ⇒ 车2 的
126
129
  * D1 逐字现行为(store 缺席 = 现行 `tool_approval` 活卡腿一字不变)。 */
127
130
  approvalAsk(): ApprovalAskStoreType;
131
+ /**
132
+ * #154 车二(core 5.18.0 design/179 + 5.22.0 design/182):**持久化权限规则店**(「不再询问」车道)
133
+ * 三面 —— 规则桶 provider / durable 审批记录 / server 自铸的 CC 导入票。
134
+ *
135
+ * 🔴 **可选**(与上面那些 REQUIRED 家族不同),缺席是一个真答案:core 的 `RunnerDeps.permissionRuleStore`
136
+ * 本身就是 optional,缺席 ⇒ 引擎的 `permissionRules.storeWired` 如实报 `false`,ask 帧不投 `ruleSuggestions`
137
+ * (发一格按下去无处可兑的「不再询问」= wire 谎言,比缺席更坏)。
138
+ *
139
+ * `local` 车道**刻意不给 in-memory twin**:一条规则是「人授权过的持久事实」,进程内 Map 形会在重启时
140
+ * 静默丢掉那份授权,而消费端(下一次同命令的 ask)读到的是「没有规则」——那是**放宽面**上的静默降级。
141
+ * 诚实缺席(storeWired:false)比一个会遗忘的店正确。要在 local 真上,先落 File 形(core 现成
142
+ * `FilePermissionRuleStoreProvider` 即可当模子)。
143
+ */
144
+ permissionRule(): PermissionRuleStores | undefined;
128
145
  /** SendUserFile scope↔object ledger (multi-tenant list/revoke handle; the hashed key segment hides the
129
146
  * mapping from URLs). REQUIRED on all backends (works local — one JSONL, like approvalExemption). */
130
147
  sendFileLedger(): SendFileLedger;
@@ -161,6 +178,14 @@ export interface StoreBackend {
161
178
  /** Cloud-only stores below are OPTIONAL: the `local` (in-memory/file) backend omits them (it has no core
162
179
  * in-memory twin carrying the service extras, and these are fleet/multi-replica concepts), so a local HTTP
163
180
  * service degrades EXACTLY like today's no-DB env (the consumers gate on `backend?.X?.()`). TiDB/PG provide all. */
181
+ /** design/177 —— org 共享记忆库的供给面(core `RunnerDeps.sharedMemoryStores` + `/v1/shared-memory/*`
182
+ * 只读面)。授权折叠**注入**:谁属于哪个 org 归 server 的目录面判,持久层只按判决出的 scope 集查表。
183
+ *
184
+ * 🔴 local 车道诚实缺席(不是"待补"):这个面的全部意义是**跨用户**共享一个团队库,而 local 后端是
185
+ * 单机单用户、没有租户边界、也没有 org 目录源。造一个 file 形等于给一个只有一个人的部署做"团队共享",
186
+ * 能力面会因此对消费方说谎。要在单机真上这条面,先接一个目录源(`MEMORY_ORG_DIRECTORY_JSON`)并选
187
+ * SQL 后端 —— 那时它自然点亮。 */
188
+ sharedMemoryStore?(authorizer: SharedMemoryScopeAuthorizer): SqlSharedMemoryStore;
164
189
  checkpoint?(logger?: {
165
190
  info?(msg: string, meta?: unknown): void;
166
191
  }): CheckpointStoreFull;
@@ -179,6 +204,11 @@ export interface StoreBackend {
179
204
  /** S3-TOB(设计 §1.4):the raw PG pool for the memory-engine DB backend's PgQueryFn binding (mirror of
180
205
  * mysqlPool). undefined on non-PG backends. */
181
206
  pgPool(): PgPool | undefined;
207
+ /** design/183 form b(纯身份重绑)的 SQL 收编日志。**SQL 后端专有**(mysql/pg),`local` 刻意省略:
208
+ * 收编重绑的是**多租身份轴**,而 local 后端整个数据根就属那一个用户、库里没有 owner/scope 分区面
209
+ * ——在那儿开这道口只会让一个够不到任何东西的动作看起来可用。消费点按 `backend?.adoptionLog?.()`
210
+ * 缺席即 501(诚实拒绝),与其它云专有 store 同姿势。 */
211
+ adoptionLog?(): AdoptionLogStore;
182
212
  /** S10 (SILENT-FALLBACK P1): the DB server's wall clock in epoch ms — the reaper probes it against the local
183
213
  * clock to expose replica clock skew (the write-behind counters bucket on LOCAL floor(now/windowMs); skew
184
214
  * splits a fleet window into disjoint buckets = soft-limit leak). OPTIONAL: local backend omits (no fleet). */
@@ -40,6 +40,11 @@ import { TiDBRateLimiter } from "./tidb-rate-limiter.js";
40
40
  import { PgRateLimiter } from "./pg-rate-limiter.js";
41
41
  import { TiDBSessionStore } from "./tidb-session-store.js";
42
42
  import { PgSessionStore } from "./pg-session-storage.js";
43
+ import { TiDBSharedMemoryStore, PgSharedMemoryStore } from "./shared-memory-store-sql.js";
44
+ import { TiDBAdoptionLogStore, PgAdoptionLogStore } from "./adoption-log-sql.js";
45
+ // #154 车二:持久化权限规则店(三面一束)。`mysqlDriver`/`pgDriver` 是既有的方言中立 seam。
46
+ import { createSqlPermissionRuleStores } from "./permission-rule-store-sql.js";
47
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
43
48
  /** Build the snapshot-BLOB object-store backend from config, or undefined to fall back to the SqlBlobBackend default.
44
49
  * When `config.snapshotBlobStore` (MINIO_* env) is set, the E19/2c file-snapshot stores route ONLY the blob BYTES to
45
50
  * MinIO; the MANIFEST stays in SQL (the transcript hot read never touches MinIO). The MinioBlobBackend is given the
@@ -97,6 +102,7 @@ class TiDBBackend {
97
102
  resumeAnchor() { return new TiDBResumeAnchorStore(this.pool); }
98
103
  approvalExemption() { return new TiDBApprovalExemptionStore(this.pool); }
99
104
  approvalAsk() { return new TiDBApprovalAskStore(this.pool); } // #151 流内审批协议持久层(无状态 store,每次新建同兄弟)
105
+ permissionRule() { return createSqlPermissionRuleStores(mysqlDriver(this.pool)); } // #154 车二 持久化权限规则店
100
106
  sendFileLedger() { return new TiDBSendFileLedger(this.pool); }
101
107
  sessionPolicy() { return new TiDBSessionPolicyStore(this.pool); }
102
108
  fileSnapshot() { return new TiDBFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "tidb", this.pool), snapshotBoundsFromConfig(this.config)); }
@@ -105,6 +111,7 @@ class TiDBBackend {
105
111
  workflowRun(onWarn) { return new TiDBWorkflowRunStore(this.pool, onWarn); } // P1: cross-replica workflow record(onWarn = C5 oversize-slim 留痕,completionInbox 同款先例)
106
112
  completionInbox(onWarn) { return new TiDBWorkflowCompletionInbox(this.pool, onWarn); } // P1: cross-replica push half
107
113
  notifyJournal() { return new TiDBWorkflowNotifyJournalStore(this.pool); } // 1.108: cross-replica at-least-once notify
114
+ sharedMemoryStore(authorizer) { return new TiDBSharedMemoryStore(this.pool, authorizer); } // design/177 org 共享记忆库
108
115
  checkpoint(logger) { return new TiDBCheckpointStore(this.pool, logger); }
109
116
  toolResult() { return new TiDBToolResultStore(this.pool); }
110
117
  imageIndex() { return new TiDBImageIndex(this.pool); }
@@ -113,6 +120,7 @@ class TiDBBackend {
113
120
  costQuota(limitMicroUsd, windowMs, onDegraded) { return new TiDBCostQuota(this.pool, limitMicroUsd, windowMs, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
114
121
  rateLimiter(limit, onDegraded) { return new TiDBRateLimiter(this.pool, limit, undefined, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
115
122
  session() { return new TiDBSessionStore(this.pool); }
123
+ adoptionLog() { return new TiDBAdoptionLogStore(this.pool); } // design/183 form b 收编日志(无状态 store,每次新建同兄弟)
116
124
  mysqlPool() { return this.pool; }
117
125
  pgPool() { return undefined; }
118
126
  async dbNowMs() { const [rows] = await this.pool.query("SELECT ROUND(UNIX_TIMESTAMP(NOW(3))*1000) AS ms"); return Number(rows[0]?.ms); } // S10
@@ -131,6 +139,7 @@ class PgBackend {
131
139
  resumeAnchor() { return new PgResumeAnchorStore(this.pool); }
132
140
  approvalExemption() { return new PgApprovalExemptionStore(this.pool); }
133
141
  approvalAsk() { return new PgApprovalAskStore(this.pool); } // #151 流内审批协议持久层(SQL twin)
142
+ permissionRule() { return createSqlPermissionRuleStores(pgDriver(this.pool)); } // #154 车二 持久化权限规则店(SQL twin)
134
143
  sendFileLedger() { return new PgSendFileLedger(this.pool); }
135
144
  sessionPolicy() { return new PgSessionPolicyStore(this.pool); }
136
145
  fileSnapshot() { return new PgFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "pg", this.pool), snapshotBoundsFromConfig(this.config)); }
@@ -139,6 +148,7 @@ class PgBackend {
139
148
  workflowRun(onWarn) { return new PgWorkflowRunStore(this.pool, onWarn); } // P1: cross-replica workflow record(onWarn = C5 oversize-slim 留痕,completionInbox 同款先例)
140
149
  completionInbox(onWarn) { return new PgWorkflowCompletionInbox(this.pool, onWarn); } // P1: cross-replica push half
141
150
  notifyJournal() { return new PgWorkflowNotifyJournalStore(this.pool); } // 1.108: cross-replica at-least-once notify
151
+ sharedMemoryStore(authorizer) { return new PgSharedMemoryStore(this.pool, authorizer); } // design/177 org 共享记忆库
142
152
  checkpoint(logger) { return new PgCheckpointStore(this.pool, logger); }
143
153
  toolResult() { return new PgToolResultStore(this.pool); }
144
154
  imageIndex() { return new PgImageIndex(this.pool); }
@@ -147,6 +157,7 @@ class PgBackend {
147
157
  costQuota(limitMicroUsd, windowMs, onDegraded) { return new PgCostQuota(this.pool, limitMicroUsd, windowMs, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
148
158
  rateLimiter(limit, onDegraded) { return new PgRateLimiter(this.pool, limit, undefined, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
149
159
  session() { return new PgSessionStore(this.pool); }
160
+ adoptionLog() { return new PgAdoptionLogStore(this.pool); } // design/183 form b 收编日志(SQL twin)
150
161
  mysqlPool() { return undefined; }
151
162
  pgPool() { return this.pool; }
152
163
  async dbNowMs() { const res = await this.pool.query("SELECT ROUND(EXTRACT(EPOCH FROM now())*1000) AS ms"); return Number(res.rows[0]?.ms); } // S10
@@ -233,6 +244,9 @@ class LocalBackend {
233
244
  * new 一个就等于每个消费者各拿一份互不可见的账(而 SQL twin 天然共享一张表)。易失代价 +
234
245
  * 「local 车道默认不上协议」的裁定见 StoreBackend.approvalAsk() 的接口注。 */
235
246
  approvalAsk() { return this.approvalAskStore; }
247
+ /** #154 车二:local 车道**诚实缺席**(理由逐字见 `StoreBackend.permissionRule()` 的接口注:
248
+ * 一个会在重启时遗忘授权的规则店,在放宽面上比没有店更坏)。 */
249
+ permissionRule() { return undefined; }
236
250
  sendFileLedger() { return this.sendFileLedgerStore; } // singleton — append fd lives in the instance
237
251
  /** Durable HITL / plan-review parking on the LOCAL lane. Lazy singleton (the inner core store holds
238
252
  * an append fd). taskId join ← FileRunStore; E21 owner guard ← LocalSessionStore.ownerOf (same root). */