@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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import { FileStorageBackend, FileSessionRepo, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
|
|
11
|
+
import { AdoptionError, FileStorageBackend, FileSessionRepo, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
|
|
12
12
|
import { FileWorkflowJournalStore } from "@sema-agent/core";
|
|
13
13
|
import { TiDBOutcomeLedger, PgOutcomeLedger } from "./outcome-ledger-sql.js";
|
|
14
14
|
import { FileOutcomeSink } from "./file-outcome-sink.js";
|
|
@@ -40,6 +40,13 @@ 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
|
+
// #203 §1:local 车道的 File 三面束(core 现成 provider + 本仓 File 形审批记录/导入票)。
|
|
47
|
+
import { createSqlPermissionRuleStores } from "./permission-rule-store-sql.js";
|
|
48
|
+
import { createFilePermissionRuleStores } from "./permission-rule-store-file.js";
|
|
49
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
43
50
|
/** Build the snapshot-BLOB object-store backend from config, or undefined to fall back to the SqlBlobBackend default.
|
|
44
51
|
* When `config.snapshotBlobStore` (MINIO_* env) is set, the E19/2c file-snapshot stores route ONLY the blob BYTES to
|
|
45
52
|
* MinIO; the MANIFEST stays in SQL (the transcript hot read never touches MinIO). The MinioBlobBackend is given the
|
|
@@ -97,6 +104,7 @@ class TiDBBackend {
|
|
|
97
104
|
resumeAnchor() { return new TiDBResumeAnchorStore(this.pool); }
|
|
98
105
|
approvalExemption() { return new TiDBApprovalExemptionStore(this.pool); }
|
|
99
106
|
approvalAsk() { return new TiDBApprovalAskStore(this.pool); } // #151 流内审批协议持久层(无状态 store,每次新建同兄弟)
|
|
107
|
+
permissionRule() { return createSqlPermissionRuleStores(mysqlDriver(this.pool)); } // #154 车二 持久化权限规则店
|
|
100
108
|
sendFileLedger() { return new TiDBSendFileLedger(this.pool); }
|
|
101
109
|
sessionPolicy() { return new TiDBSessionPolicyStore(this.pool); }
|
|
102
110
|
fileSnapshot() { return new TiDBFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "tidb", this.pool), snapshotBoundsFromConfig(this.config)); }
|
|
@@ -105,6 +113,7 @@ class TiDBBackend {
|
|
|
105
113
|
workflowRun(onWarn) { return new TiDBWorkflowRunStore(this.pool, onWarn); } // P1: cross-replica workflow record(onWarn = C5 oversize-slim 留痕,completionInbox 同款先例)
|
|
106
114
|
completionInbox(onWarn) { return new TiDBWorkflowCompletionInbox(this.pool, onWarn); } // P1: cross-replica push half
|
|
107
115
|
notifyJournal() { return new TiDBWorkflowNotifyJournalStore(this.pool); } // 1.108: cross-replica at-least-once notify
|
|
116
|
+
sharedMemoryStore(authorizer) { return new TiDBSharedMemoryStore(this.pool, authorizer); } // design/177 org 共享记忆库
|
|
108
117
|
checkpoint(logger) { return new TiDBCheckpointStore(this.pool, logger); }
|
|
109
118
|
toolResult() { return new TiDBToolResultStore(this.pool); }
|
|
110
119
|
imageIndex() { return new TiDBImageIndex(this.pool); }
|
|
@@ -113,6 +122,7 @@ class TiDBBackend {
|
|
|
113
122
|
costQuota(limitMicroUsd, windowMs, onDegraded) { return new TiDBCostQuota(this.pool, limitMicroUsd, windowMs, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
|
|
114
123
|
rateLimiter(limit, onDegraded) { return new TiDBRateLimiter(this.pool, limit, undefined, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
|
|
115
124
|
session() { return new TiDBSessionStore(this.pool); }
|
|
125
|
+
adoptionLog() { return new TiDBAdoptionLogStore(this.pool); } // design/183 form b 收编日志(无状态 store,每次新建同兄弟)
|
|
116
126
|
mysqlPool() { return this.pool; }
|
|
117
127
|
pgPool() { return undefined; }
|
|
118
128
|
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 +141,7 @@ class PgBackend {
|
|
|
131
141
|
resumeAnchor() { return new PgResumeAnchorStore(this.pool); }
|
|
132
142
|
approvalExemption() { return new PgApprovalExemptionStore(this.pool); }
|
|
133
143
|
approvalAsk() { return new PgApprovalAskStore(this.pool); } // #151 流内审批协议持久层(SQL twin)
|
|
144
|
+
permissionRule() { return createSqlPermissionRuleStores(pgDriver(this.pool)); } // #154 车二 持久化权限规则店(SQL twin)
|
|
134
145
|
sendFileLedger() { return new PgSendFileLedger(this.pool); }
|
|
135
146
|
sessionPolicy() { return new PgSessionPolicyStore(this.pool); }
|
|
136
147
|
fileSnapshot() { return new PgFileSnapshotStore(this.pool, snapshotBlobBackend(this.config, "pg", this.pool), snapshotBoundsFromConfig(this.config)); }
|
|
@@ -139,6 +150,7 @@ class PgBackend {
|
|
|
139
150
|
workflowRun(onWarn) { return new PgWorkflowRunStore(this.pool, onWarn); } // P1: cross-replica workflow record(onWarn = C5 oversize-slim 留痕,completionInbox 同款先例)
|
|
140
151
|
completionInbox(onWarn) { return new PgWorkflowCompletionInbox(this.pool, onWarn); } // P1: cross-replica push half
|
|
141
152
|
notifyJournal() { return new PgWorkflowNotifyJournalStore(this.pool); } // 1.108: cross-replica at-least-once notify
|
|
153
|
+
sharedMemoryStore(authorizer) { return new PgSharedMemoryStore(this.pool, authorizer); } // design/177 org 共享记忆库
|
|
142
154
|
checkpoint(logger) { return new PgCheckpointStore(this.pool, logger); }
|
|
143
155
|
toolResult() { return new PgToolResultStore(this.pool); }
|
|
144
156
|
imageIndex() { return new PgImageIndex(this.pool); }
|
|
@@ -147,6 +159,7 @@ class PgBackend {
|
|
|
147
159
|
costQuota(limitMicroUsd, windowMs, onDegraded) { return new PgCostQuota(this.pool, limitMicroUsd, windowMs, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
|
|
148
160
|
rateLimiter(limit, onDegraded) { return new PgRateLimiter(this.pool, limit, undefined, undefined, onDegraded, this.config.dbQueryTimeoutMs); }
|
|
149
161
|
session() { return new PgSessionStore(this.pool); }
|
|
162
|
+
adoptionLog() { return new PgAdoptionLogStore(this.pool); } // design/183 form b 收编日志(SQL twin)
|
|
150
163
|
mysqlPool() { return undefined; }
|
|
151
164
|
pgPool() { return this.pool; }
|
|
152
165
|
async dbNowMs() { const res = await this.pool.query("SELECT ROUND(EXTRACT(EPOCH FROM now())*1000) AS ms"); return Number(res.rows[0]?.ms); } // S10
|
|
@@ -191,13 +204,29 @@ class LocalBackend {
|
|
|
191
204
|
workflowJournalStore;
|
|
192
205
|
/** design/73 §1 — owner-only JSONL outcome-fact sink (stateless append; no fd held). */
|
|
193
206
|
outcomeSinkInst;
|
|
194
|
-
|
|
207
|
+
/** boot 期的告警口(`openStoreBackendWithFallback` 把它接到 logger.warn 上)。可选:直接 `new` 一只
|
|
208
|
+
* LocalBackend 的测试/工具不必装配日志面 —— 缺席时 core 的披露仍留在 core 那侧,只是本仓不复述。 */
|
|
209
|
+
onWarn;
|
|
210
|
+
constructor(root, config, onWarn) {
|
|
211
|
+
this.onWarn = onWarn;
|
|
195
212
|
// The fileBackend ctor mkdirs `root` + takes the boot pidfile lock — a same-host SECOND instance FAILS FAST.
|
|
196
213
|
// Re-throw with a clear operator message (the raw core error is "another instance owns this data dir").
|
|
214
|
+
//
|
|
215
|
+
// 🔴 core 5.23.0([3372] 提货批件④):这个构造器现在有**第二个**拒绝面 —— design/183 的 I6 adoption
|
|
216
|
+
// boot 门(`dist/stores/file/index.js:45` 的 `assertAdoptionBootGate`):数据根下 `adoption.json` 是
|
|
217
|
+
// 在飞标记 ⇒ `AdoptionError("adoption_in_flight")`,标记读不出 ⇒ `"adoption_marker_corrupt"`。
|
|
218
|
+
// 原来的 catch 是 **catch-all**,会把它一并改写成下面那句 boot-lock 文案 —— 那是**误诊**:它给的两条
|
|
219
|
+
// 出路对 adoption 在飞都不对,而「把 LOCAL_DATA_ROOT 指到一个空目录」更是直接劝运维**丢下一个迁移
|
|
220
|
+
// 到一半的数据根**;`AdoptionError` 的类型身份与 typed `code` 也在改写中被抹掉。
|
|
221
|
+
// ⇒ `AdoptionError` **原样穿过**:core 的拒绝句本就是写给人读的(点名在飞的那条弧,并给出「resume
|
|
222
|
+
// adoptLocalDataRoot 到完成,或修 adoption.json」的出路),我们没有更好的话可说,包一层只会更差。
|
|
223
|
+
// 包装只留给真正的 boot-lock 类失败(那句文案的适用条件)。
|
|
197
224
|
try {
|
|
198
225
|
this.fileBackend = new FileStorageBackend({ root, ...(config.rewindSnapshotMaxMb !== undefined ? { snapshotBounds: { maxBytes: Math.round(config.rewindSnapshotMaxMb * 1024 * 1024) } } : {}) }); // REWIND_SNAPSHOT_MAX_MB
|
|
199
226
|
}
|
|
200
227
|
catch (e) {
|
|
228
|
+
if (e instanceof AdoptionError)
|
|
229
|
+
throw e;
|
|
201
230
|
throw new Error(`DB_BACKEND=local cannot open data dir ${root}: ${e instanceof Error ? e.message : String(e)} ` +
|
|
202
231
|
"(another HTTP service / run-local instance may own it; stop it first or set LOCAL_DATA_ROOT to a free dir)");
|
|
203
232
|
}
|
|
@@ -224,6 +253,10 @@ class LocalBackend {
|
|
|
224
253
|
this.sendFileLedgerStore.dispose(); // release the sendfile-ledger append fd (lazy — may never have opened)
|
|
225
254
|
this.workflowJournalStore.dispose(); // release the per-run journal-ledger fds
|
|
226
255
|
this.checkpointStoreInst?.close(); // release the checkpoint ledger fd (lazy, may never have been built)
|
|
256
|
+
// #203 §1: release core's permission-rule DIRECTORY write lock (lazy — may never have been built).
|
|
257
|
+
// Same discipline as root/LOCK below: an un-released writer lock tells the next boot that a live process
|
|
258
|
+
// owns the rule directory, and core refuses the second holder LOUDLY rather than queueing.
|
|
259
|
+
this.permissionRuleStoresInst?.dispose();
|
|
227
260
|
await this.fileBackend.dispose(); // MUST release root/LOCK so a graceful restart can re-acquire the data dir
|
|
228
261
|
}
|
|
229
262
|
run() { return this.runStore; }
|
|
@@ -233,6 +266,22 @@ class LocalBackend {
|
|
|
233
266
|
* new 一个就等于每个消费者各拿一份互不可见的账(而 SQL twin 天然共享一张表)。易失代价 +
|
|
234
267
|
* 「local 车道默认不上协议」的裁定见 StoreBackend.approvalAsk() 的接口注。 */
|
|
235
268
|
approvalAsk() { return this.approvalAskStore; }
|
|
269
|
+
/** #203 §1:local 车道的 File 三面束。**必须**是 singleton —— core 的 File provider 在第一次取写面时
|
|
270
|
+
* 对规则目录取一把进程级写锁,第二个持有者被**响亮拒绝**(不是排队),所以每次 `new` 一个就等于给
|
|
271
|
+
* 自己造一次「另一个进程占着这个目录」的假象。`close()` 里 `dispose()` 释放它(与数据根 `root/LOCK`
|
|
272
|
+
* 同一条纪律:不释放 ⇒ 优雅重启被自己上一条命留下的锁挡在门外)。惰性建:没开旋钮的部署不该因为
|
|
273
|
+
* 一个从不使用的面而在数据根里长出目录、更不该白占一把写锁。 */
|
|
274
|
+
permissionRule() {
|
|
275
|
+
if (!this.permissionRuleStoresInst) {
|
|
276
|
+
this.permissionRuleStoresInst = createFilePermissionRuleStores(this.fileBackend.root, {
|
|
277
|
+
// core 规则文件的披露面(读不出 / 校验和不符 / 撞上符号链接 ⇒ 答零规则**并说出来**)。
|
|
278
|
+
// 不接住它就是一次静默的 fail-closed:用户突然被反复询问,而日志里一个字都没有。
|
|
279
|
+
onError: (message) => this.onWarn?.("permission_rule_store_disclosure", { message }),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return this.permissionRuleStoresInst;
|
|
283
|
+
}
|
|
284
|
+
permissionRuleStoresInst;
|
|
236
285
|
sendFileLedger() { return this.sendFileLedgerStore; } // singleton — append fd lives in the instance
|
|
237
286
|
/** Durable HITL / plan-review parking on the LOCAL lane. Lazy singleton (the inner core store holds
|
|
238
287
|
* an append fd). taskId join ← FileRunStore; E21 owner guard ← LocalSessionStore.ownerOf (same root). */
|
|
@@ -288,10 +337,14 @@ export function counterStoreLabel(store, kind) {
|
|
|
288
337
|
return store ? `shared(${kind ?? "unknown"})` : "in-process";
|
|
289
338
|
}
|
|
290
339
|
/** Build the durable-store backend for `config.dbBackend`. Does NOT connect (the pool connects lazily; the
|
|
291
|
-
* reachability probe is the caller's first `ensureSchema`). Returns undefined when no DB is configured.
|
|
292
|
-
|
|
340
|
+
* reachability probe is the caller's first `ensureSchema`). Returns undefined when no DB is configured.
|
|
341
|
+
*
|
|
342
|
+
* `onWarn` (additive, optional) is the boot-time warning sink the LOCAL backend needs for core's
|
|
343
|
+
* permission-rule DISCLOSURE face (#203 §1): an unreadable / checksum-mismatched / symlinked rule file
|
|
344
|
+
* answers ZERO rules and says so — swallowing that turns a fail-closed degrade into a silent one. */
|
|
345
|
+
export function createStoreBackend(config, onWarn) {
|
|
293
346
|
if (config.dbBackend === "local")
|
|
294
|
-
return new LocalBackend(config.localDataRoot ?? join(homedir(), ".ai-agent"), config);
|
|
347
|
+
return new LocalBackend(config.localDataRoot ?? join(homedir(), ".ai-agent"), config, onWarn);
|
|
295
348
|
if (config.dbBackend === "pg") {
|
|
296
349
|
if (!config.pg)
|
|
297
350
|
return undefined;
|
|
@@ -321,12 +374,22 @@ export async function openStoreBackendWithFallback(config, logger) {
|
|
|
321
374
|
const mayFallback = config.dbBackend === "local" ? !config.dbBackendExplicit : config.sessionBackend === "auto";
|
|
322
375
|
let backend;
|
|
323
376
|
try {
|
|
324
|
-
|
|
377
|
+
// 包一层箭头而不是直接传 `logger.warn`:后者会把方法从它的接收者上摘下来,一个带 `this` 的
|
|
378
|
+
// logger 实现会在第一次真披露时炸——而那一刻正是最不该炸的时候(本仓 fail-loud 病族先例)。
|
|
379
|
+
backend = createStoreBackend(config, (msg, meta) => logger.warn(msg, meta));
|
|
325
380
|
// connects + creates schema (also the reachability probe).
|
|
326
381
|
if (backend)
|
|
327
382
|
await backend.ensureSchema();
|
|
328
383
|
}
|
|
329
384
|
catch (err) {
|
|
385
|
+
// 🔴 core 5.23.0([3372] 提货批,codex 复审 high 抓获、验真后修):**I6 adoption 门不许被降级臂吃掉**。
|
|
386
|
+
// 1.292 拍的降级裁定说的是「只读 FS / mkdir 失败」那一类——它们的意思是「这台机器存不了盘」,降级到
|
|
387
|
+
// 内存是诚实的等价物。`AdoptionError` 说的是完全不同的一句话:「这份数据**有主、正在搬**」(design/183
|
|
388
|
+
// I6:数据根的 `adoption.json` 在飞,或标记读不出)。对后者降级 = **在搬家途中另开一个写者**,而那正是
|
|
389
|
+
// I6 门存在的全部理由;更坏的是它还会留下一行「DB 不可达」的 warn,把运维支去查连通性。
|
|
390
|
+
// ⇒ 与显式 DB_BACKEND 同待遇:无条件 fail-loud,穿过 `mayFallback`。
|
|
391
|
+
if (err instanceof AdoptionError)
|
|
392
|
+
throw err;
|
|
330
393
|
if (!mayFallback)
|
|
331
394
|
throw err;
|
|
332
395
|
// S5 (SILENT-FALLBACK P0-b): one boot warn was the ONLY trace of "this replica is not persisting".
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import mysql from "mysql2/promise";
|
|
2
2
|
import { TIDB_APPROVAL_ASK_STATEMENTS } from "./approval-ask-store-sql.js";
|
|
3
|
+
import { TIDB_ADOPTION_LOG_STATEMENTS } from "./adoption-log-sql.js";
|
|
4
|
+
import { TIDB_PERMISSION_RULE_STATEMENTS } from "./permission-rule-store-sql.js";
|
|
3
5
|
/**
|
|
4
6
|
* Shared TiDB (MySQL wire-compatible) connection pool for the durable Session center (L1).
|
|
5
7
|
* Owned by `main.ts`; closed on shutdown.
|
|
@@ -648,6 +650,51 @@ export const SCHEMA_STATEMENTS = [
|
|
|
648
650
|
// 跟着中央 `ensureSchema` 在 **named-lock 的那条 conn** 上建 —— 把 pool 级 ensure 函数塞进执行循环
|
|
649
651
|
// 会绕开 DDL 串行化。挂进来同时也是「这张表算生产 schema」的声明(基线导出按定义只收录被执行的语句)。
|
|
650
652
|
...TIDB_APPROVAL_ASK_STATEMENTS,
|
|
653
|
+
// design/177 shared-memory store(#192 单数纪律)—— org 盘的三张表。消费面 = shared-memory-store-sql.ts。
|
|
654
|
+
// 🔴 与 `agent_memory_engine_*`(design/138 本地记忆链)**零关系**:不 join、不共享 scope 登记、不互写
|
|
655
|
+
// (设计 182 §13)。两族同库不同面,合表/建视图会让 harvest 的私人笔记漏进团队库。
|
|
656
|
+
`CREATE TABLE IF NOT EXISTS shared_memory_scope (
|
|
657
|
+
-- 一个 org scope 一行。行缺席 = 该 org 从未开通 ⇒ 语义等价于 connected 且零 store(不是故障)。
|
|
658
|
+
scope_key VARCHAR(190) NOT NULL,
|
|
659
|
+
-- 闭集词:connecting|unavailable|connected。读出时走 planeStateOf 的穷举 switch,未知词整拒快照。
|
|
660
|
+
state VARCHAR(16) NOT NULL,
|
|
661
|
+
-- host 自述原因,仅 unavailable 有意义;core 折成一行喂模型面。
|
|
662
|
+
message VARCHAR(512) NULL,
|
|
663
|
+
updated_at_ms BIGINT NOT NULL,
|
|
664
|
+
PRIMARY KEY (scope_key)
|
|
665
|
+
) COLLATE utf8mb4_bin`,
|
|
666
|
+
`CREATE TABLE IF NOT EXISTS shared_memory_store (
|
|
667
|
+
-- 模型面把手,**全局唯一**:同 id 在两个 org 各一份会在跨 org 成员的快照里撞名,而 core 对撞名 id
|
|
668
|
+
-- 的处置是整行丢弃 —— 那是一次静默的库消失,所以用 PK 在库层堵死。
|
|
669
|
+
store_id VARCHAR(64) NOT NULL,
|
|
670
|
+
scope_key VARCHAR(190) NOT NULL,
|
|
671
|
+
description VARCHAR(512) NOT NULL,
|
|
672
|
+
-- 治理面结论,原样转达;core 对它零动作(模型面根本没有写通道)。
|
|
673
|
+
writable TINYINT(1) NOT NULL DEFAULT 0,
|
|
674
|
+
updated_at_ms BIGINT NOT NULL,
|
|
675
|
+
PRIMARY KEY (store_id),
|
|
676
|
+
KEY idx_shared_memory_store_scope (scope_key, store_id)
|
|
677
|
+
) COLLATE utf8mb4_bin`,
|
|
678
|
+
`CREATE TABLE IF NOT EXISTS shared_memory_entry (
|
|
679
|
+
store_id VARCHAR(64) NOT NULL,
|
|
680
|
+
-- 绝对路径。PK 即 keyset 分页索引:WHERE store_id = ? AND path > ? ORDER BY path。
|
|
681
|
+
path VARCHAR(512) NOT NULL,
|
|
682
|
+
content LONGTEXT NOT NULL,
|
|
683
|
+
-- ISO 8601 原文逐字回放(core 只渲染匹配的 YYYY-MM-DD 前缀)。数值毫秒列是下面那根,两者不同义。
|
|
684
|
+
updated_at_iso VARCHAR(64) NULL,
|
|
685
|
+
size_bytes INT NOT NULL,
|
|
686
|
+
updated_at_ms BIGINT NOT NULL,
|
|
687
|
+
PRIMARY KEY (store_id, path)
|
|
688
|
+
) COLLATE utf8mb4_bin`,
|
|
689
|
+
// design/183 form b(纯身份重绑)的 **SQL 收编日志**。真源 = adoption-log-sql.ts 的
|
|
690
|
+
// `TIDB_ADOPTION_LOG_STATEMENTS`(与 PG twin 并排);展开进来的理由与 approval-ask 同款:
|
|
691
|
+
// 跟着中央 ensureSchema 在 named-lock 的那条 conn 上建,不绕开 DDL 串行化。
|
|
692
|
+
// 🔴 它**必须**与被迁的行同库(183 I2):phase 真源住别处 ⇒ 发起 host 被摧毁时与半迁移状态分家。
|
|
693
|
+
...TIDB_ADOPTION_LOG_STATEMENTS,
|
|
694
|
+
// #154 车二(core 5.18.0 design/179 + 5.22.0 design/182)持久化权限规则店的三张表。真源 =
|
|
695
|
+
// permission-rule-store-sql.ts 的 `TIDB_PERMISSION_RULE_STATEMENTS`(与 PG twin 并排);展开进这个
|
|
696
|
+
// 数组的理由与上面那族逐字相同(named-lock 的那条 conn + 「这张表算生产 schema」的声明)。
|
|
697
|
+
...TIDB_PERMISSION_RULE_STATEMENTS,
|
|
651
698
|
];
|
|
652
699
|
/** Named MySQL/TiDB advisory lock that serializes the whole `ensureSchema` DDL run (see below). */
|
|
653
700
|
export const ENSURE_SCHEMA_LOCK = "sema_ensure_schema";
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { type ImportPreview, type ImportResult, type ImportedSettingsLayer, type PersistedAllowRule, type RemoveResult, type RuleScope } from "@sema-agent/core";
|
|
2
|
+
import type { PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
|
+
import { type RuleImportTicket, type RuleTicketRedeemResult } from "./plugins/permission-rule-store-sql.js";
|
|
4
|
+
/**
|
|
5
|
+
* 本车道**真正**依赖的三面(**结构**接口,不是 SQL 类)。
|
|
6
|
+
*
|
|
7
|
+
* 🔴 为什么不直接吃 `PermissionRuleStores`(SQL 束):同意车道是**纯协议逻辑**,它对「行躺在 MySQL 还是
|
|
8
|
+
* PG 还是别处」一无所知也不该知道。吃结构接口有两个真收益:①单元测试能用 core 自己的
|
|
9
|
+
* `InMemoryPermissionRuleStore` / `InMemoryRuleApprovalRecordStore` 驱动**同一段**代码(而不是为了测协议
|
|
10
|
+
* 去拉一个数据库);②将来若真出了 File 形规则店,本文件一个字不用改。SQL 束结构上满足本接口。
|
|
11
|
+
*/
|
|
12
|
+
export interface RuleConsentStores {
|
|
13
|
+
provider: PermissionRuleStoreProvider;
|
|
14
|
+
/** core 的记录店面 + 一个**可选**的加项:`discardPendingRecord`(超帽拒绝时收掉 `prepareCcImport`
|
|
15
|
+
* 已经落盘的那一条 pending 记录)。没有这个加项的后端只是留一条孤儿行,不影响任何判决
|
|
16
|
+
* (语义与「已确认的记录永不被收」的硬条款见 SQL 侧同名方法)。 */
|
|
17
|
+
approvals: RuleApprovalRecordStore & {
|
|
18
|
+
discardPendingRecord?(recordId: string): Promise<boolean>;
|
|
19
|
+
};
|
|
20
|
+
tickets: {
|
|
21
|
+
mint(input: {
|
|
22
|
+
ticketId: string;
|
|
23
|
+
principal: string;
|
|
24
|
+
approvalId: string;
|
|
25
|
+
candidates: ReadonlyArray<{
|
|
26
|
+
rule: string;
|
|
27
|
+
scope: RuleScope;
|
|
28
|
+
}>;
|
|
29
|
+
ttlMs: number;
|
|
30
|
+
}): Promise<RuleImportTicket>;
|
|
31
|
+
/** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。 */
|
|
32
|
+
consume(ticketId: string, principal: string): Promise<RuleTicketRedeemResult>;
|
|
33
|
+
/** 把认领**放回去**(认领之后遇到不确定/瞬时失败时)。`mustRemainValidMs` = 放回之后这张票至少还要
|
|
34
|
+
* 能用多久 —— 撑不过就**不算放回成功**(理由逐字见 store 侧 `release` 的头注)。 */
|
|
35
|
+
release(ticketId: string, principal: string, mustRemainValidMs?: number): Promise<boolean>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 一个 backend 交出来的**完整**规则店束 = 同意车道要的三面 + boot 期审计要的窄读口。
|
|
40
|
+
*
|
|
41
|
+
* 🔴 为什么它住在这里而不是某个 `plugins/*-sql.ts`:`StoreBackend.permissionRule()` 现在有**两个**
|
|
42
|
+
* 实现(SQL 双方言 + local File),把返回类型钉成其中任何一个的具体类型都会让另一个只能靠断言挤进去。
|
|
43
|
+
* 结构接口是这条 seam 唯一站得住的形——两个实现各自满足它,消费点(main 装配 / 同意车道 / 撤销面
|
|
44
|
+
* 路由)一个字都不用知道行躺在哪里。
|
|
45
|
+
*/
|
|
46
|
+
export interface PermissionRuleStoreBundle extends RuleConsentStores {
|
|
47
|
+
/** #203 §3:库里已有几只规则桶(一 owner 一桶)。boot 期休眠行审计的**唯一**依赖;
|
|
48
|
+
* 为什么数桶不数规则、以及读失败必须响亮,见 `boot/permission-rules-audit.ts` 与两个实现处的注。 */
|
|
49
|
+
countBuckets(): Promise<number>;
|
|
50
|
+
}
|
|
51
|
+
/** 导入票的存活窗。人从「看到预览」到「按下确认」是一次交互,不是一段会话——十分钟宽到不会误伤,
|
|
52
|
+
* 窄到一张泄漏的票不会常驻。运维旋钮暂不开(没有部署形要求它可调;要开时走 config.ts 的既有姿势)。 */
|
|
53
|
+
export declare const RULE_IMPORT_TICKET_TTL_MS: number;
|
|
54
|
+
/** 可重试那一支通告给客户端的等待秒数(`state.rule_import_retry` 的 `retryAfterSec` / `Retry-After`)。
|
|
55
|
+
* 短 —— 它对应的是一次瞬时 store 抖动,不是排队。**单一真源在本文件**:放回认领时要用它判「这张票
|
|
56
|
+
* 还能不能撑过这段窗」,HTTP 层要用它填响应,两处用同一个数才谈得上「承诺兑得出来」。 */
|
|
57
|
+
export declare const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
58
|
+
/**
|
|
59
|
+
* **全部层合计**的候选上限(codex 交叉复审 round7 [high] 二,验真后修)。
|
|
60
|
+
*
|
|
61
|
+
* 单层 16 KiB 只管住了**请求体**,管不住**下游工作量**:紧凑的合法规则(`Bash(ls)` 十个字符)能让三层
|
|
62
|
+
* 塞进上千条候选,而 core 的 `redeemRuleBatch` 是**逐候选**跑一遍「读店 + CAS 写」的串行循环 —— 一个
|
|
63
|
+
* 已鉴权的请求就能把连接池占满、把响应撑到兆字节。全局限流器默认是关的,拦不住它(一次准入就够了)。
|
|
64
|
+
*
|
|
65
|
+
* 帽数的是 **core 已经解析好的候选**(不是我们自己再数一遍 JSON):零重复解析,而且数的正好是那条串行
|
|
66
|
+
* 循环的**真实**长度。位置在**铸票之前** —— 超限就没有票,那条昂贵的循环一次都跑不起来(代价只剩一条
|
|
67
|
+
* 已铸的 pending 记录,与任何一次没去兑付的 prepare 留下的残余同形)。
|
|
68
|
+
* 200 的取值:一份人手写的 CC settings 极少超过几十条;200 给了一个数量级的余量,又把最坏工作量钉在
|
|
69
|
+
* 「几百次 SQL」而不是「几万次」。
|
|
70
|
+
*/
|
|
71
|
+
export declare const MAX_IMPORT_CANDIDATES = 200;
|
|
72
|
+
/** 一次导入 prepare 的产物(HTTP 200 体的素材),或**超帽**的拒绝(见 {@link MAX_IMPORT_CANDIDATES})。 */
|
|
73
|
+
export type RuleImportPrepared = {
|
|
74
|
+
ok: true;
|
|
75
|
+
preview: ImportPreview;
|
|
76
|
+
ticket: string;
|
|
77
|
+
expiresAtMs: number;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
reason: "too-many-candidates";
|
|
81
|
+
candidates: number;
|
|
82
|
+
};
|
|
83
|
+
/** 导入 redeem 的结果。`refused` 的四个成因**在 wire 面折成同一个 404**(见顶注 ①)——这里保留分类
|
|
84
|
+
* 只为服务端日志/诊断。 */
|
|
85
|
+
export type RuleImportRedeemed = {
|
|
86
|
+
ok: true;
|
|
87
|
+
result: ImportResult;
|
|
88
|
+
}
|
|
89
|
+
/** `retryable` = 这次失败**没有裁定任何事**(store 抛错 / CAS 冲突),认领已放回,属主原样重试即可。
|
|
90
|
+
* 它与裁定性拒绝(载荷被篡改、记录不存在)分家是承重的:把两者塌成一格,一次数据库抖动就会被
|
|
91
|
+
* 客户端读成「这张票永远不能用了」。⚠️ **wire 面仍然折成同一个 404**(零存在性 oracle 不因此松动);
|
|
92
|
+
* 这一位只进服务端日志与本层的重试判据。 */
|
|
93
|
+
| {
|
|
94
|
+
ok: false;
|
|
95
|
+
reason: "ticket-unusable" | "record-unusable" | "payload-mismatch";
|
|
96
|
+
detail: string;
|
|
97
|
+
retryable?: true;
|
|
98
|
+
};
|
|
99
|
+
/** 卡道兑付的结果。 */
|
|
100
|
+
export type CardRulePersisted = {
|
|
101
|
+
ok: true;
|
|
102
|
+
rule: string;
|
|
103
|
+
rev: number;
|
|
104
|
+
alreadyRedeemed: boolean;
|
|
105
|
+
} | {
|
|
106
|
+
ok: false;
|
|
107
|
+
reason: "no-candidates" | "unknown-candidate" | "confirm-refused" | "redeem-refused";
|
|
108
|
+
detail: string;
|
|
109
|
+
};
|
|
110
|
+
/** 一层用户交上来的 settings(HTTP 载荷已 zod 校验过的形)。 */
|
|
111
|
+
export interface RuleImportLayerInput {
|
|
112
|
+
layer: ImportedSettingsLayer;
|
|
113
|
+
path: string;
|
|
114
|
+
root: string;
|
|
115
|
+
content: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* scope 的 **wire 形**:`global` | `project:<root>` 的判别式串(design/203 v2 §6 F5「显式成文」)。
|
|
119
|
+
*
|
|
120
|
+
* 🔴 为什么是一个串而不是把 core 的 `{kind, root}` 对象直接上 wire:这一份表示要同时当**列举结果里的
|
|
121
|
+
* 一列**、**查询参数**、**删除请求体的一个字段**和**分页游标的一部分**。四处若各用各的形,「删掉我刚才
|
|
122
|
+
* 列出来的那一行」就变成一次跨形状翻译 —— 而那正是最容易漂的地方。判别式串在四处逐字相同,而且
|
|
123
|
+
* `project:` 前缀让「这条规则是项目内的」在一眼扫日志时就成立。
|
|
124
|
+
*
|
|
125
|
+
* `root` 里可以含冒号(路径合法),所以只切**第一个**冒号 —— 用 `split(":")` 取 [1] 会把
|
|
126
|
+
* `project:/a:b` 悄悄截成 `/a`,那是一条**放宽面**上的静默改写(更短的 root 覆盖更多 cwd)。
|
|
127
|
+
*/
|
|
128
|
+
export declare function serializeRuleScope(scope: RuleScope): string;
|
|
129
|
+
/** {@link serializeRuleScope} 的逆。读不出形 ⇒ `undefined`(调用方 400,绝不猜一个 global 出来 ——
|
|
130
|
+
* 猜 global 会把一次「删项目内规则」的请求变成一次删不掉任何东西的 no-op,或者更糟)。 */
|
|
131
|
+
export declare function parseRuleScope(text: string): RuleScope | undefined;
|
|
132
|
+
/** 一条规则在 wire 上的行 = 一个逻辑 (rule, scope) 对(design/203 v2 §6 F5)。
|
|
133
|
+
* `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
|
|
134
|
+
* 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */
|
|
135
|
+
export interface PersistedRuleWireRow {
|
|
136
|
+
rule: string;
|
|
137
|
+
/** {@link serializeRuleScope} 的判别式串。 */
|
|
138
|
+
scope: string;
|
|
139
|
+
tool: PersistedAllowRule["tool"];
|
|
140
|
+
match: PersistedAllowRule["match"];
|
|
141
|
+
/** 去规范化的命令(core 的匹配器信的就是它 + `match`,不是重新解析 `rule`)。 */
|
|
142
|
+
command: string;
|
|
143
|
+
adds: PersistedAllowRule["adds"];
|
|
144
|
+
}
|
|
145
|
+
/** 列举结果。`rev` = 这只桶的 OCC 版本,游标绑它(见路由侧的游标注)。 */
|
|
146
|
+
export interface PersistedRuleListing {
|
|
147
|
+
rows: PersistedRuleWireRow[];
|
|
148
|
+
rev: number;
|
|
149
|
+
}
|
|
150
|
+
export interface RuleConsentLane {
|
|
151
|
+
/** 卡道兑付口:一次 ask 决议携规则确认 ⇒ prepare→confirm→redeem 一气呵成。 */
|
|
152
|
+
persistCardRule(input: {
|
|
153
|
+
principal: string;
|
|
154
|
+
toolName: string;
|
|
155
|
+
/** 被裁决的命令**原字节**(与 ask 上的那份同源;规则文本由引擎从它铸,调用方不供文本)。 */
|
|
156
|
+
command: string;
|
|
157
|
+
/** 客户端选中的候选**文本**——用来在引擎铸出的候选表里定位下标。文本对不上 ⇒ 拒(不猜)。 */
|
|
158
|
+
ruleText: string;
|
|
159
|
+
toolCallId?: string;
|
|
160
|
+
boundInputHash?: string;
|
|
161
|
+
scope?: RuleScope;
|
|
162
|
+
}): Promise<CardRulePersisted>;
|
|
163
|
+
/** 导入道:读层 → 预览 + pending 记录 + 票。 */
|
|
164
|
+
prepareImport(principal: string, layers: readonly RuleImportLayerInput[]): Promise<RuleImportPrepared>;
|
|
165
|
+
/** 导入道:原子消费票 → confirm → redeemRuleBatch。 */
|
|
166
|
+
redeemImport(principal: string, ticket: string): Promise<RuleImportRedeemed>;
|
|
167
|
+
/**
|
|
168
|
+
* 撤销面读半场:列出一位 principal 名下**活着**的规则(墓碑已折算)。
|
|
169
|
+
*
|
|
170
|
+
* 排序 = (scope, rule) 字典序,**确定性**:分页游标是 keyset 形,而 keyset 的全部前提就是「同一份
|
|
171
|
+
* 数据每次以同一个顺序出现」。core 的 `list()` 不承诺顺序(SQL 形按写入顺序、File 形按文件内顺序),
|
|
172
|
+
* 所以序在这里定,不在店里。
|
|
173
|
+
*/
|
|
174
|
+
listRules(principal: string): Promise<PersistedRuleListing>;
|
|
175
|
+
/**
|
|
176
|
+
* 撤销面写半场。**恒经 core `removePersistedRule`**(design/203 §2 裁定)——它产墓碑,而墓碑是
|
|
177
|
+
* `screenRuleSyncState` 的筛子保证「别的副本不把这条规则回灌回来」的唯一凭据。直接删 SQL 行/文件行
|
|
178
|
+
* 是**无墓碑硬删**:sync 的 join 会让它复活,而运维以为自己收回了权限。
|
|
179
|
+
*
|
|
180
|
+
* 返回值逐字是 core 的 `RemoveResult` 三态(removed / no-op / failed),**不塌**:
|
|
181
|
+
* `removed.stillLive:true` 意思是「墓碑落了,但本次调用期间又落了一次新的批准,规则按 add-wins
|
|
182
|
+
* 仍然活着」——那是一个**有名字的真结果**,不是异常,更不是「撤销完成」。
|
|
183
|
+
*/
|
|
184
|
+
removeRule(input: {
|
|
185
|
+
principal: string;
|
|
186
|
+
rule: string;
|
|
187
|
+
scope: RuleScope;
|
|
188
|
+
}): Promise<RemoveResult>;
|
|
189
|
+
}
|
|
190
|
+
export declare function createRuleConsentLane(stores: RuleConsentStores, opts?: {
|
|
191
|
+
ticketTtlMs?: number;
|
|
192
|
+
newTicketId?: () => string;
|
|
193
|
+
}): RuleConsentLane;
|
|
194
|
+
//# sourceMappingURL=rules-consent.d.ts.map
|