@sema-agent/server 7.58.0 → 7.60.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 +4 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +10 -1
- package/dist/approval-card.d.ts +47 -0
- package/dist/approval-card.js +18 -0
- package/dist/boot/parked-revive-gate.d.ts +3 -0
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.js +1 -1
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runner-deps.js +4 -0
- package/dist/boot/stores.d.ts +1 -0
- package/dist/boot/stores.js +7 -4
- package/dist/config-catalog.d.ts +0 -29
- package/dist/config-catalog.js +3 -0
- package/dist/config-types.d.ts +12 -2
- package/dist/config.js +13 -2
- package/dist/cross-session-settings.d.ts +70 -0
- package/dist/cross-session-settings.js +37 -0
- package/dist/deployment-governance.d.ts +30 -1
- package/dist/deployment-governance.js +11 -5
- package/dist/http/routes/approvals-assistant.js +3 -0
- package/dist/http/routes/capabilities.js +2 -0
- package/dist/http/routes/diagnostics.js +1 -0
- package/dist/http/server.d.ts +6 -0
- package/dist/main.js +11 -4
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +9 -1
- package/dist/observability/run-terminal-log.d.ts +4 -0
- package/dist/observability/run-terminal-log.js +14 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +9 -9
- package/dist/plugins/approval-ask-store-sql.js +1 -1
- package/dist/plugins/device-store-sql.d.ts +38 -10
- package/dist/plugins/device-store-sql.js +28 -9
- package/dist/plugins/file-history-store-sql.d.ts +20 -3
- package/dist/plugins/file-history-store-sql.js +39 -36
- package/dist/plugins/image-bake-store-sql.js +2 -2
- package/dist/plugins/mailbox-store-sql.d.ts +5 -2
- package/dist/plugins/mailbox-store-sql.js +2 -2
- package/dist/plugins/permission-rule-store-file.d.ts +4 -4
- package/dist/plugins/permission-rule-store-file.js +4 -4
- package/dist/plugins/permission-rule-store-sql.d.ts +15 -11
- package/dist/plugins/permission-rule-store-sql.js +7 -11
- package/dist/plugins/retention-lane-store-sql.d.ts +10 -6
- package/dist/plugins/retention-lane-store-sql.js +1 -1
- package/dist/plugins/retention-store-sql.d.ts +11 -15
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/session-policy-store-sql.d.ts +9 -2
- package/dist/plugins/shared-memory-store-sql.d.ts +25 -19
- package/dist/plugins/shared-memory-store-sql.js +17 -15
- package/dist/plugins/sql-driver.d.ts +78 -26
- package/dist/plugins/sql-driver.js +97 -14
- package/dist/plugins/store-backend.d.ts +6 -2
- package/dist/plugins/task-list-store-sql.d.ts +2 -2
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/usage-window-store-sql.d.ts +5 -3
- package/dist/plugins/usage-window-store-sql.js +1 -5
- package/dist/rules-consent.d.ts +54 -12
- package/dist/rules-consent.js +4 -4
- package/dist/run-local.d.ts +2 -1
- package/dist/run-local.js +3 -3
- package/dist/runs.d.ts +13 -0
- package/dist/runs.js +2 -0
- package/dist/sql-engine-posture.d.ts +49 -0
- package/dist/sql-engine-posture.js +13 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/task-settings.js +1 -1
- package/dist/tool-approval.d.ts +41 -1
- package/dist/tool-approval.js +7 -1
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +3 -0
- package/dist/trace/project.d.ts +25 -0
- package/dist/trace/project.js +26 -0
- package/dist/trace/sema-provenance.d.ts +7 -1
- package/dist/trace/sema-provenance.js +1 -1
- package/package.json +3 -3
|
@@ -54,18 +54,18 @@
|
|
|
54
54
|
* live library both re-scans the prefix on every page and skips rows when a document is inserted mid-walk.
|
|
55
55
|
*
|
|
56
56
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
57
|
-
* 事务与锁
|
|
57
|
+
* 事务与锁
|
|
58
58
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
59
|
-
*
|
|
60
|
-
* `SELECT … FOR UPDATE`
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
59
|
+
* **四个**写方法(putStore / putDocument / deleteDocument / deleteStore)都是「先读注册行、再按结果写」
|
|
60
|
+
* 的形状,并且都用 `SELECT … FOR UPDATE` 拿同一把行锁,于是彼此串行。判据只有一处:
|
|
61
|
+
* {@link SqlTxConn.begin} 的 `@contract txn.read-semantics` —— 加锁读在两引擎都是当前读,而「当前读」
|
|
62
|
+
* 的前提(TiDB 悲观模式、MySQL 会话级 REPEATABLE READ)是**连接初始化**的结构保证,不是服务器默认值。
|
|
63
|
+
*
|
|
64
|
+
* 本店的**第二道防线**照旧在:`store_id` 的主键唯一性才是最终裁决者,撞键被 {@link isDupKey} 翻成一句
|
|
65
|
+
* 响亮的拒绝(见 putStore)—— 即便某一天读语义出岔,结果也只会是「一方被响亮拒绝」,绝不会是一次静默
|
|
66
|
+
* 的换属主。⚠️ 另一处不可混线的分歧:InnoDB 的加锁读对**不存在的键**取 gap/next-key 锁,TiDB 悲观事务
|
|
67
|
+
* **没有** gap 锁(retention-store 的「先 ON DUPLICATE 造行、再 FOR UPDATE 锁行」就是为它而写)。
|
|
68
|
+
* 「加锁读=当前读」两引擎同真,「gap 锁」只有 InnoDB 有 —— 两条性质不共享一个「同理」。
|
|
69
69
|
*/
|
|
70
70
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
71
71
|
import type { Pool as PgPool } from "pg";
|
|
@@ -154,15 +154,21 @@ export declare class SqlSharedMemoryStore implements SharedMemoryStoreProvider {
|
|
|
154
154
|
signal?: AbortSignal;
|
|
155
155
|
}): Promise<SharedMemorySnapshot>;
|
|
156
156
|
/**
|
|
157
|
-
* 盘状态 +
|
|
157
|
+
* 盘状态 + 库登记,**一条语句一个快照**。
|
|
158
|
+
*
|
|
159
|
+
* 🔴 S-131(codex 对抗复审 r1 [high] 采,[ref] 的终局):这一对读要的不是新鲜度,而是「scope 行与
|
|
160
|
+
* store 行读齐、不撕」——一个 importer 若落在两条读之间,快照会把**旧的 connected 盘态**和**新登记的
|
|
161
|
+
* 库**拼在一起,于是壳拿到一个「已连接」的盘和一份不完整的库,而 reader 只复核 (store, scope) 归属、
|
|
162
|
+
* 从不复核盘态。
|
|
163
|
+
*
|
|
164
|
+
* 此前这条保证挂在**隔离级**上(轮2 F3 起用事务,[ref] 又把两臂都钉成 REPEATABLE READ)。S-131 把
|
|
165
|
+
* 隔离级收进连接初始化之后,那条路在 PG 腿上走不通:PG 的 REPEATABLE READ 是快照隔离,把会话钉过去
|
|
166
|
+
* 会让本仓每一条 `SELECT … FOR UPDATE` 从「等锁后读当前」变成 `40001` 序列化失败(`@contract
|
|
167
|
+
* txn.read-semantics` ② 的 PG 段),而 PG 的 `BEGIN` 是 READ COMMITTED、**每条语句各自取快照**。
|
|
158
168
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* MySQL 的厂商默认、中间层代理、运维手改)会把「一个事务一个快照」悄悄降成「每条语句各自取快照」,
|
|
163
|
-
* 而本方法**不会报任何错**——它只是读到一对撕裂的 scope/store 行。「靠默认值成立」不是结构保证。
|
|
164
|
-
* 两臂现在都走 {@link SqlTxConn.beginRepeatableRead}(语句文本与**摆放位置**的方言分歧写在那里:
|
|
165
|
-
* MySQL 必须在 BEGIN **之前**发、PG 必须在 BEGIN **之后**发)。
|
|
169
|
+
* ⇒ 改用**一条语句**读齐两张表(`UNION ALL` + `kind` 判别列)。一条语句在**任何**引擎、任何隔离级
|
|
170
|
+
* 下都是一个快照,于是这条保证不再依赖隔离级、不再需要事务、也不必为一次纯读去拿行锁(锁读会让
|
|
171
|
+
* 并发读被写者串起来,而这是会话附着的热路径)。规则少了一条:全店从此只有写方法开事务。
|
|
166
172
|
*/
|
|
167
173
|
private readBinding;
|
|
168
174
|
private readerFor;
|
|
@@ -147,21 +147,23 @@ export class SqlSharedMemoryStore {
|
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
149
|
async readBinding(scopes) {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
150
|
+
const n = scopes.length;
|
|
151
|
+
const kindOrder = "ORDER BY kind, store_id";
|
|
152
|
+
const rows = await this.db.query(this.q(`SELECT 'scope' AS kind, scope_key, state, message, NULL AS store_id, NULL AS description, NULL AS writable
|
|
153
|
+
FROM ${SHARED_MEMORY_SCOPE_TABLE} WHERE scope_key IN (${scopes.map(() => "?").join(", ")})
|
|
154
|
+
UNION ALL
|
|
155
|
+
SELECT 'store' AS kind, scope_key, NULL, NULL, store_id, description, writable
|
|
156
|
+
FROM ${SHARED_MEMORY_STORE_TABLE} WHERE scope_key IN (${scopes.map(() => "?").join(", ")})
|
|
157
|
+
${kindOrder}`, `SELECT 'scope' AS kind, scope_key, state, message, NULL::varchar AS store_id, NULL::varchar AS description, NULL::smallint AS writable
|
|
158
|
+
FROM ${SHARED_MEMORY_SCOPE_TABLE} WHERE scope_key IN (${scopes.map((_, i) => `$${i + 1}`).join(", ")})
|
|
159
|
+
UNION ALL
|
|
160
|
+
SELECT 'store' AS kind, scope_key, NULL::varchar, NULL::varchar, store_id, description, writable
|
|
161
|
+
FROM ${SHARED_MEMORY_STORE_TABLE} WHERE scope_key IN (${scopes.map((_, i) => `$${n + i + 1}`).join(", ")})
|
|
162
|
+
${kindOrder}`), [...scopes, ...scopes]);
|
|
163
|
+
return {
|
|
164
|
+
scopeRows: rows.rows.filter((r) => r.kind === "scope"),
|
|
165
|
+
storeRows: rows.rows.filter((r) => r.kind === "store"),
|
|
166
|
+
};
|
|
165
167
|
}
|
|
166
168
|
readerFor(storeId, scopeKey) {
|
|
167
169
|
return {
|
|
@@ -25,10 +25,43 @@
|
|
|
25
25
|
* `query_timeout`. The counter family (write-behind-counter.ts) needs those and keeps its own
|
|
26
26
|
* exec; this seam stays on the plain (sql, params) form.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
* @contract txn.read-semantics — 本仓「事务里的读看到什么」的**唯一**成文处(S-131 / B-020 / B-022)
|
|
30
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
* 五只店的头注此前各写一套「为什么用这个事务动词」的理由(截断的、漏数的、讲历史沿革的都有)。
|
|
32
|
+
* 判据从今天起只有这一处,店里只留一行 `{@link SqlTxConn.begin}` 指回来。
|
|
33
|
+
*
|
|
34
|
+
* **① 事务动词只有一个** —— `begin()`。曾经那个「悲观」专用动词(MySQL 腿发 TiDB 专有的
|
|
35
|
+
* `BEGIN PESSIMISTIC`)已删除:那条语句在真 MySQL / MariaDB 上是 `ER_PARSE_ERROR`,四店 18 个事务
|
|
36
|
+
* 一个都开不了(**B-020**),而它真正保证的东西现在由 ② 结构性地给出。同理,那个「可重复读」专用
|
|
37
|
+
* 动词也一并并入:隔离级不再由动词决定。
|
|
38
|
+
*
|
|
39
|
+
* **② 隔离级与事务模式是「连接的事实」,不是「动词的参数」** —— 见 {@link SqlDriver.facts}。
|
|
40
|
+
* 每条池连接首次借出时初始化一次(记忆化):
|
|
41
|
+
* · MySQL 协议腿:`SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ`;`SELECT @@tidb_txn_mode`
|
|
42
|
+
* 有值 ⇒ TiDB(值必须是 `pessimistic`,否则会话级设定,**设不上就拒启**——本仓所有 `FOR UPDATE`
|
|
43
|
+
* 判据都以悲观锁读为前提);`ER_UNKNOWN_SYSTEM_VARIABLE` ⇒ InnoDB(MySQL / MariaDB)。
|
|
44
|
+
* · PG:`SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED`,同样回读复核。
|
|
45
|
+
* 两腿钉的**值**不同(RR vs RC)是**引擎事实**不是口味:两边要的都是「加锁读=当前读」,而 PG 的
|
|
46
|
+
* REPEATABLE READ 是快照隔离,`FOR UPDATE` 撞并发提交抛 `40001` 而不是等锁 —— 把 PG 钉成 RR 会把
|
|
47
|
+
* 本仓每一条 `FOR UPDATE` 腿变成需要重试的形。`default_transaction_isolation` 同样是可设变量,
|
|
48
|
+
* 所以 PG 腿也**不问服务器缺省**。
|
|
49
|
+
* 「服务器默认值不是结构保证」这条论证的出处是 [ref]:`transaction_isolation` 是可设变量,一台
|
|
50
|
+
* 跑 READ COMMITTED 的实例会把「一个事务一个快照」悄悄降成「每条语句各自取快照」,而依赖它的调用方
|
|
51
|
+
* 不会收到任何错。所以 MySQL 腿现在**每条连接自己钉**,不问部署。
|
|
52
|
+
*
|
|
53
|
+
* **③ 事务内的集合读一律是锁读**(`… FOR UPDATE`)。普通 `SELECT` 只用于三种情形:读**本事务自己刚写
|
|
54
|
+
* 的行**、读**值/时钟表达式**(不触任何存量行)、以及 PG 语法明禁加锁的形(**只有**聚合 / GROUP BY /
|
|
55
|
+
* DISTINCT / 窗口 / 集合运算;`ORDER BY … LIMIT 1` 尾行读在 PG 上**合法**加锁,不属此类——7.60.0 重扫纠)。
|
|
56
|
+
* 理由:TiDB 悲观事务的普通 SELECT 仍钉 `start_ts`,`lockScope` 的锁等待窗内别副本的提交看不见
|
|
57
|
+
* (**B-022**);InnoDB REPEATABLE READ 下则由本事务的第一条一致性读建 read view —— 两条都是
|
|
58
|
+
* **版本相关的副作用**,不是保证。机器门:`test/sql-txn-semantics-gate.test.ts`(G-B,例外闭集
|
|
59
|
+
* `INTRA_TXN_PLAIN_READS` 每条带理由);真值神谕:`test/sql-txn-semantics-integration.test.ts`(三引擎)。
|
|
60
|
+
*
|
|
61
|
+
* **④ 引擎事实不进店** —— `SqlDriver.facts` 是**运维读面**(`GET /v1/diagnostics/wiring` 的 `sqlEngine`
|
|
62
|
+
* 段与 `GET /v1/capabilities` 的 `sql` 位)的料。店里不许出现 `if (engine === …)`:引擎判别在全仓
|
|
63
|
+
* 只有连接初始化这一处,机器门 G-A 守着。`SqlDialect`(SQL **文本**方言,两值)与 `engine`(运行期
|
|
64
|
+
* 引擎事实,三值)是两条轴,刻意不合成一个三值。
|
|
32
65
|
*
|
|
33
66
|
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
34
67
|
* REPLICATION PLAYBOOK — collapsing the remaining tidb-/pg- twin pairs ([ref] A12)
|
|
@@ -90,38 +123,57 @@ export interface SqlExec {
|
|
|
90
123
|
}
|
|
91
124
|
/** A pooled connection with transaction control (released by the caller in a `finally`). */
|
|
92
125
|
export interface SqlTxConn extends SqlExec {
|
|
93
|
-
begin(): Promise<void>;
|
|
94
|
-
/** TiDB: `BEGIN PESSIMISTIC` (current-read txn). PG: plain `BEGIN`. */
|
|
95
|
-
beginPessimistic(): Promise<void>;
|
|
96
126
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* snapshot" — and the caller that relied on it (`shared-memory-store-sql.ts` readBinding) goes on
|
|
104
|
-
* reading a torn scope/store pair with no error anywhere. The PG arm was already explicit; the
|
|
105
|
-
* MySQL arm was leaning on the default. Both arms now PIN it.
|
|
106
|
-
*
|
|
107
|
-
* The two arms diverge in WHERE the statement goes, and that is not cosmetic:
|
|
108
|
-
* · MySQL/TiDB — `SET TRANSACTION ISOLATION LEVEL …` with no scope keyword applies to the **next**
|
|
109
|
-
* transaction, and issuing it *inside* an open transaction is an ERROR
|
|
110
|
-
* (`ER_CANT_CHANGE_TX_CHARACTERISTICS`). So it must precede `beginTransaction()`.
|
|
111
|
-
* · PG — the same statement must be issued *inside* the transaction, before its first query.
|
|
112
|
-
* Writing one "portable" form would be wrong on one of the two engines; hence one verb, two texts.
|
|
127
|
+
* The ONE transaction verb (S-131). What its reads see is NOT decided here — it is decided once per
|
|
128
|
+
* pooled connection at initialization and reported by {@link SqlDriver.facts}. Read the
|
|
129
|
+
* `@contract txn.read-semantics` block at the top of this file before adding a transaction: the
|
|
130
|
+
* short version is **intra-transaction collection reads are LOCKING reads**, and a plain `SELECT`
|
|
131
|
+
* inside a transaction is only legal for this transaction's own writes, for a value/clock
|
|
132
|
+
* expression, or where PG syntax forbids locking (machine gate: `sql-txn-semantics-gate.test.ts`).
|
|
113
133
|
*/
|
|
114
|
-
|
|
134
|
+
begin(): Promise<void>;
|
|
115
135
|
commit(): Promise<void>;
|
|
116
136
|
rollback(): Promise<void>;
|
|
117
137
|
release(): void;
|
|
118
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* The runtime SQL **engine** — a different axis from {@link SqlDialect} (which is SQL *text*).
|
|
141
|
+
* `"tidb"` and `"innodb"` both speak the MySQL protocol and share every statement in this repo, but
|
|
142
|
+
* they do not share transaction semantics (TiDB has an optimistic mode and no gap locks; InnoDB has
|
|
143
|
+
* neither an optimistic mode nor `BEGIN PESSIMISTIC`), so the two words must stay separable.
|
|
144
|
+
*/
|
|
145
|
+
export type SqlEngine = "tidb" | "innodb" | "pg";
|
|
146
|
+
/**
|
|
147
|
+
* What connection initialization ESTABLISHED and then READ BACK on this driver's pool (S-131 §1
|
|
148
|
+
* 补偿面). Operator read faces: `GET /v1/diagnostics/wiring` → `sqlEngine`, `GET /v1/capabilities`
|
|
149
|
+
* → `sql`. Store code must never read this (machine gate G-A) — engine discrimination exists in
|
|
150
|
+
* exactly one place in this repo, and it is `connect()`.
|
|
151
|
+
*/
|
|
152
|
+
export interface SqlEngineFacts {
|
|
153
|
+
readonly engine: SqlEngine;
|
|
154
|
+
/** `VERSION()` (MySQL protocol) / `server_version` (PG), verbatim. */
|
|
155
|
+
readonly version: string;
|
|
156
|
+
/** The session's transaction isolation, read back AFTER initialization set it. */
|
|
157
|
+
readonly isolation: string;
|
|
158
|
+
/** TiDB's session `tidb_txn_mode`; `null` = this engine has no such indicator (NOT "optimistic"). */
|
|
159
|
+
readonly txnMode: "pessimistic" | null;
|
|
160
|
+
}
|
|
119
161
|
export interface SqlDriver extends SqlExec {
|
|
120
162
|
readonly dialect: SqlDialect;
|
|
163
|
+
/**
|
|
164
|
+
* Engine facts, established the first time ANY driver over the same pool hands out a pooled connection
|
|
165
|
+
* (state is keyed by the pool, so the twenty-odd store drivers sharing one pool report ONE set of facts);
|
|
166
|
+
* `null` until then (a pool that has never connected has nothing to report — it does not GUESS). Every
|
|
167
|
+
* pooled connection re-runs initialization the first time it is borrowed (memo keyed by connection
|
|
168
|
+
* identity), so a pool that reconnects after a fatal error re-establishes the session guarantee on the
|
|
169
|
+
* new connection rather than inheriting it.
|
|
170
|
+
*/
|
|
171
|
+
readonly facts: SqlEngineFacts | null;
|
|
121
172
|
connect(): Promise<SqlTxConn>;
|
|
122
173
|
}
|
|
123
|
-
/** MySQL-protocol (TiDB
|
|
124
|
-
*
|
|
174
|
+
/** MySQL-protocol driver (TiDB / MySQL / MariaDB — `dialect` is the SQL TEXT axis, `facts.engine` the
|
|
175
|
+
* runtime one). Transaction control uses mysql2's NATIVE methods — the historical call shape the
|
|
176
|
+
* fake-pool unit suites script around. */
|
|
125
177
|
export declare function mysqlDriver(pool: MySqlPool): SqlDriver;
|
|
126
178
|
/** PostgreSQL driver. Transaction control is issued as STATEMENTS (node-pg has no native verbs). */
|
|
127
179
|
export declare function pgDriver(pool: PgPool): SqlDriver;
|
|
@@ -9,22 +9,83 @@ async function mysqlQuery(q, sql, params) {
|
|
|
9
9
|
const [r] = (await q.query(sql, params ?? []));
|
|
10
10
|
return mysqlResult(r);
|
|
11
11
|
}
|
|
12
|
+
function isUnknownSystemVariable(err) {
|
|
13
|
+
const e = err;
|
|
14
|
+
return e?.errno === 1193 || e?.code === "ER_UNKNOWN_SYSTEM_VARIABLE";
|
|
15
|
+
}
|
|
16
|
+
const POOL_SESSIONS = new WeakMap();
|
|
17
|
+
function poolSession(pool) {
|
|
18
|
+
let st = POOL_SESSIONS.get(pool);
|
|
19
|
+
if (st === undefined)
|
|
20
|
+
POOL_SESSIONS.set(pool, (st = { initialized: new WeakSet(), facts: null }));
|
|
21
|
+
return st;
|
|
22
|
+
}
|
|
23
|
+
async function mysqlVar(c, sql) {
|
|
24
|
+
try {
|
|
25
|
+
const r = await mysqlQuery(c, sql);
|
|
26
|
+
const v = r.rows[0]?.v;
|
|
27
|
+
return v == null ? "" : String(v);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (isUnknownSystemVariable(err))
|
|
31
|
+
return null;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function refuseSession(what) {
|
|
36
|
+
return new Error(`sql-driver: refusing this connection — ${what}. The repo's read semantics (see @contract txn.read-semantics in src/plugins/sql-driver.ts) ` +
|
|
37
|
+
`are established per pooled connection at initialization: session REPEATABLE READ on the MySQL protocol, plus TiDB's pessimistic transaction mode. ` +
|
|
38
|
+
`Neither is optional: every "SELECT … FOR UPDATE" decision point in this service is a CURRENT read only under them.`);
|
|
39
|
+
}
|
|
40
|
+
async function initMySqlSession(c) {
|
|
41
|
+
await c.query("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
|
|
42
|
+
let txnMode = await mysqlVar(c, "SELECT @@tidb_txn_mode AS v");
|
|
43
|
+
const engine = txnMode === null ? "innodb" : "tidb";
|
|
44
|
+
if (engine === "tidb" && txnMode !== "pessimistic") {
|
|
45
|
+
try {
|
|
46
|
+
await c.query("SET SESSION tidb_txn_mode = 'pessimistic'");
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw refuseSession(`this TiDB session is in "${txnMode}" transaction mode and SET SESSION tidb_txn_mode='pessimistic' was rejected (${err.message})`);
|
|
50
|
+
}
|
|
51
|
+
txnMode = await mysqlVar(c, "SELECT @@tidb_txn_mode AS v");
|
|
52
|
+
if (txnMode !== "pessimistic") {
|
|
53
|
+
throw refuseSession(`this TiDB session reports transaction mode "${txnMode ?? "<absent>"}" after SET SESSION tidb_txn_mode='pessimistic'`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const isolation = (await mysqlVar(c, "SELECT @@transaction_isolation AS v")) ?? (await mysqlVar(c, "SELECT @@tx_isolation AS v"));
|
|
57
|
+
if (isolation === null) {
|
|
58
|
+
throw refuseSession("this server exposes neither @@transaction_isolation (MySQL 8.0+ / TiDB / MariaDB 11.1+) nor @@tx_isolation (MariaDB 10.x), so the isolation level cannot be VERIFIED");
|
|
59
|
+
}
|
|
60
|
+
if (isolation !== "REPEATABLE-READ") {
|
|
61
|
+
throw refuseSession(`this session reports isolation "${isolation}" after SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ`);
|
|
62
|
+
}
|
|
63
|
+
return { engine, version: (await mysqlVar(c, "SELECT VERSION() AS v")) ?? "", isolation, txnMode: engine === "tidb" ? "pessimistic" : null };
|
|
64
|
+
}
|
|
12
65
|
export function mysqlDriver(pool) {
|
|
66
|
+
const session = poolSession(pool);
|
|
13
67
|
return {
|
|
14
68
|
dialect: "tidb",
|
|
69
|
+
get facts() {
|
|
70
|
+
return session.facts;
|
|
71
|
+
},
|
|
15
72
|
query: (sql, params) => mysqlQuery(pool, sql, params),
|
|
16
73
|
async connect() {
|
|
17
74
|
const c = await pool.getConnection();
|
|
75
|
+
const pooled = c.connection ?? c;
|
|
76
|
+
try {
|
|
77
|
+
if (!session.initialized.has(pooled)) {
|
|
78
|
+
session.facts = await initMySqlSession(c);
|
|
79
|
+
session.initialized.add(pooled);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
c.release();
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
18
86
|
return {
|
|
19
87
|
query: (sql, params) => mysqlQuery(c, sql, params),
|
|
20
88
|
begin: () => c.beginTransaction(),
|
|
21
|
-
beginPessimistic: async () => {
|
|
22
|
-
await c.query("BEGIN PESSIMISTIC");
|
|
23
|
-
},
|
|
24
|
-
beginRepeatableRead: async () => {
|
|
25
|
-
await c.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
|
|
26
|
-
await c.beginTransaction();
|
|
27
|
-
},
|
|
28
89
|
commit: () => c.commit(),
|
|
29
90
|
rollback: () => c.rollback(),
|
|
30
91
|
release: () => c.release(),
|
|
@@ -36,24 +97,46 @@ async function pgQuery(q, sql, params) {
|
|
|
36
97
|
const res = await q.query(sql, params ?? []);
|
|
37
98
|
return { rows: (res.rows ?? []), affected: res.rowCount ?? 0 };
|
|
38
99
|
}
|
|
100
|
+
async function initPgSession(c) {
|
|
101
|
+
await c.query("SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED");
|
|
102
|
+
const iso = await pgQuery(c, "SHOW transaction_isolation");
|
|
103
|
+
const isolation = String(iso.rows[0]?.transaction_isolation ?? "");
|
|
104
|
+
if (isolation !== "read committed") {
|
|
105
|
+
throw refuseSession(`this PostgreSQL session reports isolation "${isolation}" after SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED`);
|
|
106
|
+
}
|
|
107
|
+
const ver = await pgQuery(c, "SHOW server_version");
|
|
108
|
+
return {
|
|
109
|
+
engine: "pg",
|
|
110
|
+
version: String(ver.rows[0]?.server_version ?? ""),
|
|
111
|
+
isolation,
|
|
112
|
+
txnMode: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
39
115
|
export function pgDriver(pool) {
|
|
116
|
+
const session = poolSession(pool);
|
|
40
117
|
return {
|
|
41
118
|
dialect: "pg",
|
|
119
|
+
get facts() {
|
|
120
|
+
return session.facts;
|
|
121
|
+
},
|
|
42
122
|
query: (sql, params) => pgQuery(pool, sql, params),
|
|
43
123
|
async connect() {
|
|
44
124
|
const c = await pool.connect();
|
|
125
|
+
try {
|
|
126
|
+
if (!session.initialized.has(c)) {
|
|
127
|
+
session.facts = await initPgSession(c);
|
|
128
|
+
session.initialized.add(c);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
c.release();
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
45
135
|
return {
|
|
46
136
|
query: (sql, params) => pgQuery(c, sql, params),
|
|
47
137
|
begin: async () => {
|
|
48
138
|
await c.query("BEGIN");
|
|
49
139
|
},
|
|
50
|
-
beginPessimistic: async () => {
|
|
51
|
-
await c.query("BEGIN");
|
|
52
|
-
},
|
|
53
|
-
beginRepeatableRead: async () => {
|
|
54
|
-
await c.query("BEGIN");
|
|
55
|
-
await c.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ");
|
|
56
|
-
},
|
|
57
140
|
commit: async () => {
|
|
58
141
|
await c.query("COMMIT");
|
|
59
142
|
},
|
|
@@ -151,10 +151,14 @@ export interface StoreBackend {
|
|
|
151
151
|
* 本身就是 optional,缺席 ⇒ 引擎的 `permissionRules.storeWired` 如实报 `false`,ask 帧不投 `ruleOffers`
|
|
152
152
|
* (发一格按下去无处可兑的「不再询问」= wire 谎言,比缺席更坏)。
|
|
153
153
|
*
|
|
154
|
+
* [ref](core 7.5.0):束里的规则那一格是 **durable 分区后端**(`PermissionRuleStoreBundle.durable`),
|
|
155
|
+
* 引擎接缝上的统一店由 `main.ts` 用 `createPermissionRuleStoreProvider({ durable })` 合成一次 ——
|
|
156
|
+
* 分区构成(durable / org / session)是**部署**决定,backend 只交它自己那一格。
|
|
157
|
+
*
|
|
154
158
|
* `local` 车道**刻意不给 in-memory twin**:一条规则是「人授权过的持久事实」,进程内 Map 形会在重启时
|
|
155
159
|
* 静默丢掉那份授权,而消费端(下一次同命令的 ask)读到的是「没有规则」——那是**放宽面**上的静默降级。
|
|
156
|
-
* ✅ **[ref] §1 已落 File 形**(那条注写下的解除条件逐字是「先落 File 形,core
|
|
157
|
-
*
|
|
160
|
+
* ✅ **[ref] §1 已落 File 形**(那条注写下的解除条件逐字是「先落 File 形,core 现成的
|
|
161
|
+
* File durable 分区 provider 即可当模子」——本车照办):`local` 现在返回一个**跨重启存活**的
|
|
158
162
|
* 三面束,storeWired 在单机车道上真为 true。in-memory twin 的禁令不变,它禁的是「会遗忘的店」,
|
|
159
163
|
* 不是「单机的店」。
|
|
160
164
|
*/
|
|
@@ -8,8 +8,8 @@ export declare const TASK_LIST_ITEM_TABLE = "task_list_item";
|
|
|
8
8
|
*
|
|
9
9
|
* 🔴 为什么不直接把 sessionId 当 listKey:sessionId 是**调用方可自报**的不透明串(提交门
|
|
10
10
|
* `http/server.ts` 只卡 ≤64 **字符** —— [#15] 刻意的宽松提交契约,不卡字节数也不卡字符集)。
|
|
11
|
-
* 而本文件的身份键是字节面严格的:{@link assertListKeyBytes} 对 >190 字节当场拒(
|
|
12
|
-
*
|
|
11
|
+
* 而本文件的身份键是字节面严格的:{@link assertListKeyBytes} 对 >190 字节当场拒(超长键截断案),
|
|
12
|
+
* PG twin 对孤代理/NUL 走 {@link PgUnstorableError} 拒。直接透传
|
|
13
13
|
* 的话,一个 64 字符全四字节 emoji 的会话(256 字节)会让**整条请求**在场景装配处 500 —— 而那条腿
|
|
14
14
|
* 只是待办清单。派生一个恒可存的键把这一整类失败模式消掉(既不是 fail-open 也不是 fail-loud,是让
|
|
15
15
|
* 那条臂不存在)。
|
|
@@ -56,7 +56,7 @@ export async function ensurePgTaskListSchema(q) {
|
|
|
56
56
|
}
|
|
57
57
|
async function ensureMeta(exec, dialect, listKey) {
|
|
58
58
|
await exec(dialect === "tidb"
|
|
59
|
-
? `INSERT
|
|
59
|
+
? `INSERT INTO ${TASK_LIST_META_TABLE} (list_key, next_id, next_sort) VALUES (?, 1, 1) ON DUPLICATE KEY UPDATE list_key = list_key`
|
|
60
60
|
: `INSERT INTO ${TASK_LIST_META_TABLE} (list_key, next_id, next_sort) VALUES ($1, 1, 1) ON CONFLICT (list_key) DO NOTHING`, [listKey]);
|
|
61
61
|
}
|
|
62
62
|
async function allocateOn(exec, dialect, listKey) {
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
* first-use bucket lapsed 即弃)完全由 core 纯函数执行——SQL 层零窗语义。
|
|
8
8
|
*
|
|
9
9
|
* 并发正确性(本 store 的靶心;两副本同 key 并发 charge 绝不能丢账——丢账方向=治理窗漏放):
|
|
10
|
-
* - **ensure-first + 行锁**(mailbox twin 正字先例,[ref] 复审靶1)
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - **ensure-first + 行锁**(mailbox twin 正字先例,[ref] 复审靶1):事务内先幂等建空行
|
|
11
|
+
* (形 A,S-127:MySQL `ON DUPLICATE KEY UPDATE <pk> = <pk>` / PG `ON CONFLICT DO NOTHING`)⇒
|
|
12
|
+
* `SELECT … FOR UPDATE` 时行恒在,「无行 FOR UPDATE + INSERT」的并发死锁/dup 撞车形整个消掉。
|
|
13
|
+
* MySQL 侧动词**不能**是 `INSERT IGNORE`:InnoDB 对重复键的 IGNORE 取 S 锁,随后的 FOR UPDATE 要升 X,
|
|
14
|
+
* 两个并发 charge 各持一把 S 互等 ⇒ ER_LOCK_DEADLOCK(B-013);ON DUPLICATE 的重复键取的就是 X。
|
|
13
15
|
* - charge 频率低(core 只在任务进场/turn 边界/收尾 delta 提交),悲观锁简单正确,不用乐观 CAS。
|
|
14
16
|
* - `read` 无锁单 SELECT(readUsageRecord 纯函数);行缺席 = EMPTY_USAGE_WINDOW_RECORD。
|
|
15
17
|
* - record JSON 坏形(手工污染)= **fail-loud throw**:治理窗算错=计费面撒谎,不 fail-open。
|
|
@@ -69,11 +69,7 @@ export class TiDBUsageWindowStore {
|
|
|
69
69
|
const c = await this.pool.getConnection();
|
|
70
70
|
try {
|
|
71
71
|
await c.beginTransaction();
|
|
72
|
-
await c.query(`INSERT
|
|
73
|
-
key,
|
|
74
|
-
JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
|
|
75
|
-
at,
|
|
76
|
-
]);
|
|
72
|
+
await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE usage_key = usage_key`, [key, JSON.stringify(EMPTY_USAGE_WINDOW_RECORD), at]);
|
|
77
73
|
const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
|
|
78
74
|
if (rows.length === 0) {
|
|
79
75
|
throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
|
package/dist/rules-consent.d.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedAllowRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
|
|
2
|
-
import type { PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
1
|
+
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedAllowRule, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
|
|
2
|
+
import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
3
|
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `InMemoryPermissionRuleStore` / `InMemoryRuleApprovalRecordStore` 驱动**同一段**代码(而不是为了测协议
|
|
10
|
-
* 去拉一个数据库);②将来若真出了 File 形规则店,本文件一个字不用改。SQL 束结构上满足本接口。
|
|
5
|
+
* 规则分区**之外**的两只记录面 —— 它们是 server 自铸的(审批记录的 SQL/File 形、CC 导入票),
|
|
6
|
+
* 与 [ref] 的分区族不是同一件东西,所以在这里单列一次:一个 backend 交出来的束
|
|
7
|
+
* ({@link PermissionRuleStoreBundle})与同意车道消费的束({@link RuleConsentStores})都含这两面,
|
|
8
|
+
* 差的只是规则那一格躺的是 **durable 分区后端**还是**合成好的统一店**。
|
|
11
9
|
*/
|
|
12
|
-
export interface
|
|
13
|
-
provider: PermissionRuleStoreProvider;
|
|
10
|
+
export interface RuleConsentRecordStores {
|
|
14
11
|
/** core 的记录店面 + 一个**可选**的加项:`discardPendingRecord`(超帽拒绝时收掉 `prepareCcImport`
|
|
15
12
|
* 已经落盘的那一条 pending 记录)。没有这个加项的后端只是留一条孤儿行,不影响任何判决
|
|
16
13
|
* (语义与「已确认的记录永不被收」的硬条款见 SQL 侧同名方法)。 */
|
|
@@ -51,6 +48,22 @@ export interface RuleConsentStores {
|
|
|
51
48
|
reclaim(ticketId: string, principal: string, purpose: RuleTicketPurpose, minClaimAgeMs: number): Promise<RuleTicketReclaimResult>;
|
|
52
49
|
};
|
|
53
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 本车道**真正**依赖的三面(**结构**接口,不是 SQL 类)。
|
|
53
|
+
*
|
|
54
|
+
* 🔴 为什么不直接吃 `PermissionRuleStores`(SQL 束):同意车道是**纯协议逻辑**,它对「行躺在 MySQL 还是
|
|
55
|
+
* PG 还是别处」一无所知也不该知道。吃结构接口有两个真收益:①单元测试能用 core 自己的
|
|
56
|
+
* `InMemoryDurableRulePartition` / `InMemoryRuleApprovalRecordStore` 驱动**同一段**代码(而不是为了测协议
|
|
57
|
+
* 去拉一个数据库);②将来若真出了 File 形规则店,本文件一个字不用改。SQL 束结构上满足本接口。
|
|
58
|
+
*
|
|
59
|
+
* 🔴 `provider` 是 [ref] **合成后**的统一店(`createPermissionRuleStoreProvider` 的产物),不是
|
|
60
|
+
* backend 交出来的 durable 分区 —— core 的 `removePersistedRule` / `RuleConsentDeps.provider` 两口收的
|
|
61
|
+
* 都是它,而合成点只有 `main.ts` 一处:引擎接缝(`RunnerDeps.permissionRuleStore`)与本车道读的必须是
|
|
62
|
+
* **同一只**对象,否则「卡上给的候选」与「引擎放行时看到的规则」会分别站在两只店上。
|
|
63
|
+
*/
|
|
64
|
+
export interface RuleConsentStores extends RuleConsentRecordStores {
|
|
65
|
+
provider: PermissionRuleStoreProvider;
|
|
66
|
+
}
|
|
54
67
|
/**
|
|
55
68
|
* 一个 backend 交出来的**完整**规则店束 = 同意车道要的三面 + boot 期审计要的窄读口。
|
|
56
69
|
*
|
|
@@ -59,7 +72,13 @@ export interface RuleConsentStores {
|
|
|
59
72
|
* 结构接口是这条 seam 唯一站得住的形——两个实现各自满足它,消费点(main 装配 / 同意车道 / 撤销面
|
|
60
73
|
* 路由)一个字都不用知道行躺在哪里。
|
|
61
74
|
*/
|
|
62
|
-
export interface PermissionRuleStoreBundle extends
|
|
75
|
+
export interface PermissionRuleStoreBundle extends RuleConsentRecordStores {
|
|
76
|
+
/** [ref] 的 **durable 分区后端**(`user` + `project` 两源)。**不是**引擎接缝本身:
|
|
77
|
+
* `RunnerDeps.permissionRuleStore` 与 {@link RuleConsentStores.provider} 是
|
|
78
|
+
* `createPermissionRuleStoreProvider({ durable })` 合成出来的统一店,合成在 `main.ts` 一处发生一次。
|
|
79
|
+
* 分区构成(接不接 org / session)是**部署**决定,不是 backend 决定 —— 让 backend 各自合成,等于
|
|
80
|
+
* 日后接 org 分区时要挨个改后端,而两个后端合成得不一样时没有任何人看得出来。 */
|
|
81
|
+
durable: DurableRulePartitionProvider;
|
|
63
82
|
/** [ref] §3:库里已有几只规则桶(一 owner 一桶)。boot 期休眠行审计的**唯一**依赖;
|
|
64
83
|
* 为什么数桶不数规则、以及读失败必须响亮,见 `boot/permission-rules-audit.ts` 与两个实现处的注。 */
|
|
65
84
|
countBuckets(): Promise<number>;
|
|
@@ -491,7 +510,30 @@ export interface PersistedRuleWireRow {
|
|
|
491
510
|
/** 去规范化的命令(core 的匹配器信的就是它 + `match`,不是重新解析 `rule`)。 */
|
|
492
511
|
command: string;
|
|
493
512
|
adds: PersistedAllowRule["adds"];
|
|
513
|
+
/** [ref](core 7.5.0):这一行来自哪个源。**由 `scope` 派生**(`global`⟺`user`、`project`⟺
|
|
514
|
+
* `project`、`session`⟺`session`),不是行上另存的第二个字节 —— core 的 `ruleSourceOf` 是整条映射。
|
|
515
|
+
* 今天本部署只接 durable 一格分区 ⇒ 值域实为 `user | project`。
|
|
516
|
+
*
|
|
517
|
+
* 🔴 为什么上 wire 而不是丢掉:统一读把这一格**交到手里了**,治理列举知道而不说 = 一次 wire 谎言的
|
|
518
|
+
* 反面(客户端只能自己去 decode `scope` 猜),而 org 分区接线的那天它会立刻变得承重。 */
|
|
519
|
+
source: EffectivePermissionRule["source"];
|
|
520
|
+
/** [ref]:这一行的**站位** —— `live` 或 `shadowed-by-org`(org deny 盖到它的命令模式上)。
|
|
521
|
+
* 今天无 org 分区 ⇒ 恒 `live`;这一格正是「接了 org 分区之后治理面必须看得见的那件事」。 */
|
|
522
|
+
status: EffectivePermissionRule["status"];
|
|
494
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* 一条**可导入**的规则行 = 列举行**减去两格派生键**。
|
|
526
|
+
*
|
|
527
|
+
* 🔴 为什么导入输入面不收 `source`/`status`:两格都是**派生**的 —— `source` 由 `scope` 派生
|
|
528
|
+
* (core 的 `ruleSourceOf` 是整条映射,d.ts 逐字「A row never carries a second source byte that could
|
|
529
|
+
* drift from its scope」),`status` 由 org 分区的遮蔽判据派生。一个调用方递进来的 `source` 只会与它
|
|
530
|
+
* 自己的 `scope` 漂,而漂的落点是治理面上「这行到底归谁管」。同族纪律 core 自己在卡面投影上也写过
|
|
531
|
+
* (「derived HERE, at projection time … never accepted from a caller」)。
|
|
532
|
+
*
|
|
533
|
+
* 这不是两份平行形状:`Omit` 让「导入面 = 列举面 − 派生键」是一条**机器判据**,列举行加员时导入面
|
|
534
|
+
* 自动跟随、`LOCAL_IMPORT_ROW_SHAPE_PIN`(路由侧)当场编译红。
|
|
535
|
+
*/
|
|
536
|
+
export type ImportableRuleRow = Omit<PersistedRuleWireRow, "source" | "status">;
|
|
495
537
|
/** 列举结果。`rev` = 这只桶的 OCC 版本,游标绑它(见路由侧的游标注)。 */
|
|
496
538
|
export interface PersistedRuleListing {
|
|
497
539
|
rows: PersistedRuleWireRow[];
|
|
@@ -531,7 +573,7 @@ export interface RuleConsentLane {
|
|
|
531
573
|
/** 导入道:原子消费票 → confirm → redeemRuleBatch。 */
|
|
532
574
|
redeemImport(principal: string, ticket: string): Promise<RuleImportRedeemed>;
|
|
533
575
|
/** [ref] A7 直通道:逐行校验 → 预览 + pending 记录 + 票(零可导入行 ⇒ 无票)。 */
|
|
534
|
-
prepareLocalImport(principal: string, rows: readonly
|
|
576
|
+
prepareLocalImport(principal: string, rows: readonly ImportableRuleRow[]): Promise<LocalImportPrepared>;
|
|
535
577
|
/** [ref] A7 直通道:三态兑付(首兑 / 逐字回放 / 崩溃窗重驱),终局落进票行。 */
|
|
536
578
|
redeemLocalImport(principal: string, ticket: string): Promise<LocalImportRedeemed>;
|
|
537
579
|
/**
|
package/dist/rules-consent.js
CHANGED
|
@@ -4,7 +4,7 @@ import { recordFailOpen } from "./observability/fail-open.js";
|
|
|
4
4
|
import { createLogger } from "./observability/logger.js";
|
|
5
5
|
import { MAX_CWD_CHARS } from "./task-cwd.js";
|
|
6
6
|
import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
|
|
7
|
-
import { confirmRuleApproval, parseAllowRuleText, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, } from "@sema-agent/core";
|
|
7
|
+
import { confirmRuleApproval, parseAllowRuleText, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, effectiveOrThrow, } from "@sema-agent/core";
|
|
8
8
|
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
9
9
|
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
10
10
|
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
@@ -682,10 +682,10 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
682
682
|
}
|
|
683
683
|
},
|
|
684
684
|
async listRules(bucket) {
|
|
685
|
-
const
|
|
686
|
-
const rows =
|
|
685
|
+
const view = await effectiveOrThrow(storeForBucket(stores.provider, bucket));
|
|
686
|
+
const rows = view.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds, source: r.source, status: r.status }));
|
|
687
687
|
rows.sort((a, b) => (a.scope < b.scope ? -1 : a.scope > b.scope ? 1 : a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0));
|
|
688
|
-
return { rows, rev:
|
|
688
|
+
return { rows, rev: view.rev };
|
|
689
689
|
},
|
|
690
690
|
async removeRule(input) {
|
|
691
691
|
return await removePersistedRule({ rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
package/dist/run-local.d.ts
CHANGED
|
@@ -82,7 +82,8 @@ export interface LocalGovernanceConfigView extends DeploymentGovernanceConfigVie
|
|
|
82
82
|
* test/run-local.test.ts(该钉已由 🟡 特征化翻成 🔴 正控)。
|
|
83
83
|
* · **禁 memoize**:四个旋钮都是热改字段,每次装配现读活 config(与另两条腿同一姿势)。
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
/** @param engineDataRoot 本腿已铸的引擎记忆根(`memoryEngineBackendFor(config, root)?.root`,S-133 单一写者);`undefined` = 引擎未接。 */
|
|
86
|
+
export declare function applyLocalGovernance(base: TaskSpec, config: LocalGovernanceConfigView, workspaceDir: string, engineDataRoot: string | undefined): TaskSpec;
|
|
86
87
|
/** {@link createLocalApprover} 的 io 面(全部可注入,所以两条臂都测得动)。 */
|
|
87
88
|
export interface LocalApproverIo {
|
|
88
89
|
/** 这次调用是不是接在一个人面前(生产里 = `process.stdin.isTTY`)。 */
|
package/dist/run-local.js
CHANGED
|
@@ -111,8 +111,8 @@ export function missingGatewayRequirements(config) {
|
|
|
111
111
|
}
|
|
112
112
|
return missing;
|
|
113
113
|
}
|
|
114
|
-
export function applyLocalGovernance(base, config, workspaceDir) {
|
|
115
|
-
return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }));
|
|
114
|
+
export function applyLocalGovernance(base, config, workspaceDir, engineDataRoot) {
|
|
115
|
+
return applyRuntimeGovernance({ ...base, toolPolicy: createApprovalBaselinePolicy(config) }, createDeploymentGovernanceInputs(config, { env: new NodeExecutionEnv({ cwd: workspaceDir }), cwd: workspaceDir }, { engineDataRoot }));
|
|
116
116
|
}
|
|
117
117
|
export function createLocalApprover(io) {
|
|
118
118
|
return async (req, signal) => {
|
|
@@ -472,7 +472,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
472
472
|
...(mcp ? { mcp } : {}),
|
|
473
473
|
...(a2a ? { a2a } : {}),
|
|
474
474
|
...(Object.keys(limits).length > 0 ? { limits } : {}),
|
|
475
|
-
}, config, workspaceDir);
|
|
475
|
+
}, config, workspaceDir, memoryEngine?.root);
|
|
476
476
|
logger.info("run_local_start", { scenario: scenarioName, model: spec.model, sessionId, exec: config.remoteExec?.provider ?? "in-process" });
|
|
477
477
|
const settleSessionBackground = async () => {
|
|
478
478
|
const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, scope);
|