@sema-agent/server 7.75.3 → 7.76.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/USAGE.md +29 -13
- package/dist/boot/reapers.d.ts +5 -1
- package/dist/boot/reapers.js +6 -1
- package/dist/boot/shutdown.js +5 -1
- package/dist/config-catalog.js +1 -1
- package/dist/config-types.d.ts +9 -7
- package/dist/http/routes/approvals-assistant.js +37 -1
- package/dist/http/server.d.ts +5 -1
- package/dist/main.js +3 -1
- package/dist/parent-watch.d.ts +55 -23
- package/dist/parent-watch.js +38 -23
- package/dist/plugins/approval-nonce-store.d.ts +94 -0
- package/dist/plugins/approval-nonce-store.js +126 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +1 -30
- package/dist/plugins/permission-rule-store-sql.js +2 -10
- package/dist/plugins/pg-pool.js +8 -0
- package/dist/plugins/pg-session-storage.js +1 -0
- package/dist/plugins/sql-driver.d.ts +27 -0
- package/dist/plugins/sql-driver.js +12 -0
- package/dist/plugins/store-backend.d.ts +6 -0
- package/dist/plugins/store-backend.js +7 -0
- package/dist/plugins/tidb-pool.js +26 -1
- package/dist/principal-jwt.d.ts +49 -9
- package/dist/principal-jwt.js +16 -8
- package/dist/tool-approval.d.ts +2 -1
- package/dist/tool-approval.js +8 -1
- package/dist/trace/redact.d.ts +7 -4
- package/dist/trace/redact.js +64 -33
- package/package.json +2 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { atomicWriteFile, ensureDir, readJsonlRecords, AppendLog } from "@sema-agent/core";
|
|
3
|
+
import { mysqlDriver, pgDriver, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-driver.js";
|
|
4
|
+
import { isDupKeyError } from "./sql-errors.js";
|
|
5
|
+
export const APPROVAL_NONCE_TABLE = "approval_nonce";
|
|
6
|
+
export const MAX_APPROVAL_NONCE_CHARS = 190;
|
|
7
|
+
export class SqlApprovalNonceStore {
|
|
8
|
+
db;
|
|
9
|
+
constructor(db) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
}
|
|
12
|
+
q(tidb, pg) {
|
|
13
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
14
|
+
}
|
|
15
|
+
async nowMs() {
|
|
16
|
+
return sqlDbNowMs(this.db, APPROVAL_NONCE_TABLE);
|
|
17
|
+
}
|
|
18
|
+
async consume(sessionId, nonce, expiresAtMs) {
|
|
19
|
+
try {
|
|
20
|
+
await this.db.query(this.q(`INSERT INTO ${APPROVAL_NONCE_TABLE} (session_id, nonce, consumed_at_ms, expires_at_ms) VALUES (?,?,${SQL_DB_NOW_MS.tidb},?)`, `INSERT INTO ${APPROVAL_NONCE_TABLE} (session_id, nonce, consumed_at_ms, expires_at_ms) VALUES ($1,$2,${SQL_DB_NOW_MS.pg},$3)`), [sessionId, nonce, expiresAtMs]);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (isDupKeyError(this.db.dialect, err))
|
|
25
|
+
return false;
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async reapExpired() {
|
|
30
|
+
const res = await this.db.query(this.q(`DELETE FROM ${APPROVAL_NONCE_TABLE} WHERE expires_at_ms <= ${SQL_DB_NOW_MS.tidb}`, `DELETE FROM ${APPROVAL_NONCE_TABLE} WHERE expires_at_ms <= ${SQL_DB_NOW_MS.pg}`), []);
|
|
31
|
+
return res.affected;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class TiDBApprovalNonceStore extends SqlApprovalNonceStore {
|
|
35
|
+
constructor(pool) {
|
|
36
|
+
super(mysqlDriver(pool));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class PgApprovalNonceStore extends SqlApprovalNonceStore {
|
|
40
|
+
constructor(pool) {
|
|
41
|
+
super(pgDriver(pool));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const keyOf = (sessionId, nonce) => JSON.stringify([sessionId, nonce]);
|
|
45
|
+
export class MemoryApprovalNonceStore {
|
|
46
|
+
rows = new Map();
|
|
47
|
+
async nowMs() {
|
|
48
|
+
return Date.now();
|
|
49
|
+
}
|
|
50
|
+
async consume(sessionId, nonce, expiresAtMs) {
|
|
51
|
+
const k = keyOf(sessionId, nonce);
|
|
52
|
+
if (this.rows.has(k))
|
|
53
|
+
return false;
|
|
54
|
+
this.rows.set(k, expiresAtMs);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
async reapExpired() {
|
|
58
|
+
const now = await this.nowMs();
|
|
59
|
+
let n = 0;
|
|
60
|
+
for (const [k, exp] of this.rows) {
|
|
61
|
+
if (exp <= now) {
|
|
62
|
+
this.rows.delete(k);
|
|
63
|
+
n++;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return n;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export class FileApprovalNonceStore {
|
|
70
|
+
path;
|
|
71
|
+
tmpDir;
|
|
72
|
+
rows = new Map();
|
|
73
|
+
log;
|
|
74
|
+
constructor(root) {
|
|
75
|
+
const dir = join(root, "approval-nonces");
|
|
76
|
+
ensureDir(dir);
|
|
77
|
+
this.tmpDir = join(dir, ".tmp");
|
|
78
|
+
ensureDir(this.tmpDir);
|
|
79
|
+
this.path = join(dir, "consumed.jsonl");
|
|
80
|
+
for (const r of readJsonlRecords(this.path)) {
|
|
81
|
+
if (!r || typeof r.s !== "string" || typeof r.n !== "string" || typeof r.e !== "number")
|
|
82
|
+
continue;
|
|
83
|
+
this.rows.set(keyOf(r.s, r.n), { s: r.s, n: r.n, e: r.e });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
logFor() {
|
|
87
|
+
return (this.log ??= new AppendLog(this.path));
|
|
88
|
+
}
|
|
89
|
+
async nowMs() {
|
|
90
|
+
return Date.now();
|
|
91
|
+
}
|
|
92
|
+
async consume(sessionId, nonce, expiresAtMs) {
|
|
93
|
+
const k = keyOf(sessionId, nonce);
|
|
94
|
+
if (this.rows.has(k))
|
|
95
|
+
return false;
|
|
96
|
+
const record = { s: sessionId, n: nonce, e: expiresAtMs };
|
|
97
|
+
this.logFor().append(record, true);
|
|
98
|
+
this.rows.set(k, record);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
async reapExpired() {
|
|
102
|
+
const nowMs = await this.nowMs();
|
|
103
|
+
const survivors = [];
|
|
104
|
+
let removed = 0;
|
|
105
|
+
for (const record of this.rows.values()) {
|
|
106
|
+
if (record.e <= nowMs) {
|
|
107
|
+
removed++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
survivors.push(record);
|
|
111
|
+
}
|
|
112
|
+
if (removed === 0)
|
|
113
|
+
return 0;
|
|
114
|
+
this.log?.closeForSwap();
|
|
115
|
+
atomicWriteFile(this.tmpDir, this.path, survivors.map((r) => `${JSON.stringify(r)}\n`).join(""));
|
|
116
|
+
for (const [k, record] of [...this.rows])
|
|
117
|
+
if (record.e <= nowMs)
|
|
118
|
+
this.rows.delete(k);
|
|
119
|
+
return removed;
|
|
120
|
+
}
|
|
121
|
+
dispose() {
|
|
122
|
+
this.log?.close();
|
|
123
|
+
this.log = undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=approval-nonce-store.js.map
|
|
@@ -250,30 +250,6 @@ export type RuleTicketReclaimResult = {
|
|
|
250
250
|
* 廉价,重铸一张即可(core [ref] 建议的姿态)。
|
|
251
251
|
*/
|
|
252
252
|
export declare const RULE_IMPORT_TICKET_GC_GRACE_MS: number;
|
|
253
|
-
/**
|
|
254
|
-
* [ref] A7 —— **数据库自己的钟**(毫秒),两方言各一句(codex 交叉轮 R5-F1,验真后修)。
|
|
255
|
-
*
|
|
256
|
-
* 用处:`settled_at_ms` 的写入与清扫「已落定」那一支的截止**必须在同一个钟域**。它们此前各拿各副本的
|
|
257
|
-
* 墙钟:落定那台若比跑清扫那台慢过一个宽限(本仓 [ref]③ 记过一次真实的 11 分钟 NTP 纠正),一份**刚刚**
|
|
258
|
-
* 生出来的权威回执下一拍就被删,而"落定之后必有一个完整的回放窗"是本批新许下的承诺 —— 它不能建立在
|
|
259
|
-
* 「两台机器的钟差不多」这条没人声明过的假设上。数据库的钟是这两台副本**唯一**共享的那口钟。
|
|
260
|
-
*
|
|
261
|
-
* 🔴 射程收官([ref] / [ref],2026-09-02):**这张票上的每一个时刻量都走本钟**,一个不剩 ——
|
|
262
|
-
* `consumed_at_ms` / `settled_at_ms`(R8-F2 起)、`expires_at_ms` / `created_at_ms`(本批)。
|
|
263
|
-
*
|
|
264
|
-
* 本批补上的是 **TTL 轴**,也是最后一条:铸票 `expires_at_ms = <本钟> + ttl`、判定
|
|
265
|
-
* `expires_at_ms > <本钟>`、清扫 `expires_at_ms <= <本钟> − 宽限`。修前那一形是**两台副本各出一半**的
|
|
266
|
-
* 比较(铸票副本的墙钟定到期时刻,兑付副本的墙钟做判定),于是 TTL 在集群上**不单调**:同一张票在钟慢
|
|
267
|
-
* 的那台上还活着、在钟快的那台上已经死了;极端形是「出生即过期」(铸票副本慢过一个 TTL ⇒ 票铸出来就
|
|
268
|
-
* 没人兑得动)。这不需要故障就能发作 —— 负载均衡把 prepare 与 redeem 分到两台副本是**常态**。
|
|
269
|
-
* 上一批的进程内单调锚只覆盖「同一副本铸、同一副本兑」那一格,连同它的台账在本批一并删除。
|
|
270
|
-
*
|
|
271
|
-
* 判定读不到本钟 ⇒ 语句抛错 ⇒ 调用方按不确定失败处置(fail-closed:绝不退回本地墙钟放行)。
|
|
272
|
-
*/
|
|
273
|
-
export declare const SQL_DB_NOW_MS: {
|
|
274
|
-
readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
|
|
275
|
-
readonly pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
|
|
276
|
-
};
|
|
277
253
|
/** prepare 时刻候选集的规范摘要(载荷绑定 F1 ④ 的判据)。纯数据 ⇒ `build*`。
|
|
278
254
|
*
|
|
279
255
|
* 🔴 入参型收到 core 的 `RuleCandidate`(core 7.9.0 [ref] 起含 `behavior`)—— 规则的身份是
|
|
@@ -294,12 +270,7 @@ export declare class SqlRuleImportTicketStore {
|
|
|
294
270
|
private readonly db;
|
|
295
271
|
constructor(db: SqlDriver);
|
|
296
272
|
private q;
|
|
297
|
-
/**
|
|
298
|
-
* 问一次**数据库自己的钟**(毫秒)。铸票用它当基准,于是到期时刻与后续每一次判定同域。
|
|
299
|
-
*
|
|
300
|
-
* 读不出数 ⇒ **响亮抛**([ref] fail-closed):静默兜个本地 `Date.now()` 正是本批要删掉的那条腿,
|
|
301
|
-
* 而一次「钟读不出来」在授权面上是运维事件,不是一个可以猜的默认值。
|
|
302
|
-
*/
|
|
273
|
+
/** 问一次**数据库自己的钟**(毫秒,{@link sqlDbNowMs})。铸票用它当基准,于是到期时刻与后续每一次判定同域。 */
|
|
303
274
|
private dbNowMs;
|
|
304
275
|
/**
|
|
305
276
|
* 铸一张票。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
|
|
4
|
-
import { dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
4
|
+
import { dialectProtocolJsonEncoder, sqlDbNowMs, SQL_DB_NOW_MS } from "./sql-driver.js";
|
|
5
5
|
import { isMissingColumnError } from "./sql-errors.js";
|
|
6
6
|
import { canonicalJson } from "../canonical-json.js";
|
|
7
7
|
import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, isPersistedRuleTool, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
@@ -624,10 +624,6 @@ export class SqlRuleApprovalRecordStore {
|
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
export const RULE_IMPORT_TICKET_GC_GRACE_MS = 10 * 60_000;
|
|
627
|
-
export const SQL_DB_NOW_MS = {
|
|
628
|
-
tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)",
|
|
629
|
-
pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint",
|
|
630
|
-
};
|
|
631
627
|
export function buildRulePayloadHash(candidates) {
|
|
632
628
|
return createHash("sha256").update(canonicalJson(candidates), "utf8").digest("hex");
|
|
633
629
|
}
|
|
@@ -640,11 +636,7 @@ export class SqlRuleImportTicketStore {
|
|
|
640
636
|
return this.db.dialect === "tidb" ? tidbSql : pgSql;
|
|
641
637
|
}
|
|
642
638
|
async dbNowMs() {
|
|
643
|
-
|
|
644
|
-
const n = Number(rows[0]?.["db_now_ms"] ?? Number.NaN);
|
|
645
|
-
if (!Number.isFinite(n))
|
|
646
|
-
throw new Error(`${PERMISSION_RULE_TICKET_TABLE}: the database clock came back unreadable (${String(rows[0]?.["db_now_ms"])})`);
|
|
647
|
-
return n;
|
|
639
|
+
return sqlDbNowMs(this.db, PERMISSION_RULE_TICKET_TABLE);
|
|
648
640
|
}
|
|
649
641
|
async mint(input) {
|
|
650
642
|
const nowMs = await this.dbNowMs();
|
package/dist/plugins/pg-pool.js
CHANGED
|
@@ -125,6 +125,14 @@ export const PG_SCHEMA_STATEMENTS = [
|
|
|
125
125
|
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
126
126
|
PRIMARY KEY (session_id, tool_name)
|
|
127
127
|
)`,
|
|
128
|
+
`CREATE TABLE IF NOT EXISTS approval_nonce (
|
|
129
|
+
session_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
130
|
+
nonce VARCHAR(190) COLLATE "C" NOT NULL,
|
|
131
|
+
consumed_at_ms BIGINT NOT NULL,
|
|
132
|
+
expires_at_ms BIGINT NOT NULL,
|
|
133
|
+
PRIMARY KEY (session_id, nonce)
|
|
134
|
+
)`,
|
|
135
|
+
`CREATE INDEX IF NOT EXISTS idx_approval_nonce_expires ON approval_nonce (expires_at_ms)`,
|
|
128
136
|
`CREATE TABLE IF NOT EXISTS sendfile_link (
|
|
129
137
|
id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
130
138
|
scope VARCHAR(190) COLLATE "C" NOT NULL,
|
|
@@ -49,6 +49,7 @@ export const PG_SESSION_SCHEMA = [
|
|
|
49
49
|
PRIMARY KEY (session_id, seq)
|
|
50
50
|
)`,
|
|
51
51
|
`CREATE UNIQUE INDEX IF NOT EXISTS uq_session_event_entry ON session_event (session_id, entry_id)`,
|
|
52
|
+
`CREATE INDEX IF NOT EXISTS idx_session_event_type ON session_event (session_id, type, seq)`,
|
|
52
53
|
];
|
|
53
54
|
export async function ensureSchema(pool) {
|
|
54
55
|
for (const stmt of PG_SESSION_SCHEMA)
|
|
@@ -158,6 +158,33 @@ export interface SqlEngineFacts {
|
|
|
158
158
|
/** TiDB's session `tidb_txn_mode`; `null` = this engine has no such indicator (NOT "optimistic"). */
|
|
159
159
|
readonly txnMode: "pessimistic" | null;
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* **数据库自己的钟**(毫秒),两方言各一句 —— 本仓这口钟在 SQL 面的**唯一**铸点([ref] A7 / [ref] /
|
|
163
|
+
* 合并复审 R2 把 S-280 的 nonce 店并进来)。
|
|
164
|
+
*
|
|
165
|
+
* 用处永远是同一条:**一个时刻量的铸造点与它的判定点必须在同一个钟域**。副本各拿各的墙钟时,两台机器
|
|
166
|
+
* 之间的偏差没有任何一条腿强制过上界(本仓 [ref]③ 记过一次真实的 11 分钟 NTP 纠正),于是
|
|
167
|
+
* 「A 铸的东西 B 判着还活着」这类承诺就建立在一条没人声明过的假设上。数据库的钟是一组副本**唯一**
|
|
168
|
+
* 共享的那口钟,所以每一条「谁生谁死」的判决都落到它身上:
|
|
169
|
+
* · 规则导入票(`permission-rule-store-sql.ts`):`expires_at_ms` 的铸造 / 判定 / 清扫三处;
|
|
170
|
+
* · 直连门 plan_review 的消费记录(`approval-nonce-store.ts`):受理窗的判定与记录的清扫。
|
|
171
|
+
*
|
|
172
|
+
* 读不到本钟 ⇒ 语句抛错 ⇒ 调用方按不确定失败处置([ref] fail-closed:**绝不**退回本地墙钟放行)。
|
|
173
|
+
*
|
|
174
|
+
* 它住在 driver 这只叶子模块里,是因为「这个引擎怎么说『现在几点』」是**方言事实**,与任何一只店无关;
|
|
175
|
+
* 放在某一只店里会让下一个需要它的店要么 import 一整只无关的店、要么自己抄一份(抄出来的两份会漂)。
|
|
176
|
+
*/
|
|
177
|
+
export declare const SQL_DB_NOW_MS: {
|
|
178
|
+
readonly tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)";
|
|
179
|
+
readonly pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint";
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* 问一次 {@link SQL_DB_NOW_MS} —— 所有需要「库钟此刻」的店共用这**一份**读法。
|
|
183
|
+
*
|
|
184
|
+
* `subject` 只进错误文案(出问题时运维要知道是哪一张表的判决卡住了)。读不出数 ⇒ **响亮抛**:
|
|
185
|
+
* 一次「钟读不出来」在授权面上是运维事件,不是一个可以猜的默认值,更不是掉回 `Date.now()` 的理由。
|
|
186
|
+
*/
|
|
187
|
+
export declare function sqlDbNowMs(db: SqlDriver, subject: string): Promise<number>;
|
|
161
188
|
export interface SqlDriver extends SqlExec {
|
|
162
189
|
readonly dialect: SqlDialect;
|
|
163
190
|
/**
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { pgSafeJsonStringify, pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
2
|
+
export const SQL_DB_NOW_MS = {
|
|
3
|
+
tidb: "CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS SIGNED)",
|
|
4
|
+
pg: "(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint",
|
|
5
|
+
};
|
|
6
|
+
export async function sqlDbNowMs(db, subject) {
|
|
7
|
+
const sql = db.dialect === "tidb" ? `SELECT ${SQL_DB_NOW_MS.tidb} AS db_now_ms` : `SELECT ${SQL_DB_NOW_MS.pg} AS db_now_ms`;
|
|
8
|
+
const { rows } = await db.query(sql, []);
|
|
9
|
+
const n = Number(rows[0]?.["db_now_ms"] ?? Number.NaN);
|
|
10
|
+
if (!Number.isFinite(n))
|
|
11
|
+
throw new Error(`${subject}: the database clock came back unreadable (${String(rows[0]?.["db_now_ms"])})`);
|
|
12
|
+
return n;
|
|
13
|
+
}
|
|
2
14
|
function mysqlResult(r) {
|
|
3
15
|
if (Array.isArray(r))
|
|
4
16
|
return { rows: r, affected: 0 };
|
|
@@ -8,6 +8,7 @@ import { type OneTimeMigrationReport } from "./one-time-migrations.js";
|
|
|
8
8
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
9
9
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
10
10
|
import { type ApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
11
|
+
import { type ApprovalNonceStore } from "./approval-nonce-store.js";
|
|
11
12
|
import { type ApprovalAskStore as ApprovalAskStoreType } from "./approval-ask-store-sql.js";
|
|
12
13
|
import { type MemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
|
|
13
14
|
import { type SendFileLedger } from "./send-file-ledger.js";
|
|
@@ -39,6 +40,7 @@ export type RunStore = TiDBRunStore | PgRunStore | FileRunStore;
|
|
|
39
40
|
* FileResumeAnchorStore is the FILE-backed local twin (P0.5 variant-1; survives restart). */
|
|
40
41
|
export type ResumeAnchorStore = TiDBResumeAnchorStore | PgResumeAnchorStore | FileResumeAnchorStore;
|
|
41
42
|
export type { ApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
43
|
+
export type { ApprovalNonceStore } from "./approval-nonce-store.js";
|
|
42
44
|
/** [ref]([ref] v3.1 §3.0-§3.2 流内审批协议持久层):`ApprovalAskStore` 接口 + 行/补丁类型。
|
|
43
45
|
* 车3 刀 3a 起 `StoreBackend.approvalAsk()` 已在(见接口体的那条注)。 */
|
|
44
46
|
export type { ApprovalAskStore, AskRow, NewAskRow, AskTransitionPatch, DecideAskInput, DecideResult, ExpireResult, BindGateInput, BatchRow, AskDecision } from "./approval-ask-store-sql.js";
|
|
@@ -147,6 +149,10 @@ export interface StoreBackend {
|
|
|
147
149
|
/** Per-session per-toolName approval exemption ("本会话不再询问") — an approval MEMORY the F4
|
|
148
150
|
* ask-gate consults after deny/neverAuto. REQUIRED on all backends (works local, like resumeAnchor). */
|
|
149
151
|
approvalExemption(): ApprovalExemptionStore;
|
|
152
|
+
/** S-280 直连门审批证明的一次性消费记录(`plan_review` 的 nonce)。REQUIRED on all backends —— 与
|
|
153
|
+
* `approvalExemption` 同姿势:tidb/pg = SQL 双方言(单类双语句),local = File 追加日志 + 压实。
|
|
154
|
+
* **不能**退化成进程内易失形:一次重启若把记录抹掉,JWT TTL 窗内的重放就复活了,而那正是本店要关的洞。 */
|
|
155
|
+
approvalNonce(): ApprovalNonceStore;
|
|
150
156
|
/** [ref]([ref] §3.0-§3.2)流内审批协议持久层:每个流内工具调用一行的 ask 状态机 + 批行。
|
|
151
157
|
* REQUIRED on all backends — tidb/pg = SQL 双方言 twin;local = {@link InMemoryApprovalAskStore}。
|
|
152
158
|
*
|
|
@@ -12,6 +12,7 @@ import { LocalSessionStore } from "./local-session-store.js";
|
|
|
12
12
|
import { LocalCheckpointStore } from "./local-checkpoint-store.js";
|
|
13
13
|
import { FileResumeAnchorStore } from "./file-resume-anchor-store.js";
|
|
14
14
|
import { TiDBApprovalExemptionStore, PgApprovalExemptionStore, FileApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
15
|
+
import { TiDBApprovalNonceStore, PgApprovalNonceStore, FileApprovalNonceStore } from "./approval-nonce-store.js";
|
|
15
16
|
import { TiDBApprovalAskStore, PgApprovalAskStore } from "./approval-ask-store-sql.js";
|
|
16
17
|
import { InMemoryApprovalAskStore } from "./approval-ask-store-memory.js";
|
|
17
18
|
import { TiDBMemoryOptOutGrantStore, PgMemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
|
|
@@ -112,6 +113,7 @@ class TiDBBackend {
|
|
|
112
113
|
run() { return new TiDBRunStore(this.pool); }
|
|
113
114
|
resumeAnchor() { return new TiDBResumeAnchorStore(this.pool); }
|
|
114
115
|
approvalExemption() { return new TiDBApprovalExemptionStore(this.pool); }
|
|
116
|
+
approvalNonce() { return new TiDBApprovalNonceStore(this.pool); }
|
|
115
117
|
approvalAsk() { return new TiDBApprovalAskStore(this.pool); }
|
|
116
118
|
permissionRule() { return createSqlPermissionRuleStores(mysqlDriver(this.pool)); }
|
|
117
119
|
sendFileLedger() { return new TiDBSendFileLedger(this.pool); }
|
|
@@ -159,6 +161,7 @@ class PgBackend {
|
|
|
159
161
|
run() { return new PgRunStore(this.pool); }
|
|
160
162
|
resumeAnchor() { return new PgResumeAnchorStore(this.pool); }
|
|
161
163
|
approvalExemption() { return new PgApprovalExemptionStore(this.pool); }
|
|
164
|
+
approvalNonce() { return new PgApprovalNonceStore(this.pool); }
|
|
162
165
|
approvalAsk() { return new PgApprovalAskStore(this.pool); }
|
|
163
166
|
permissionRule() { return createSqlPermissionRuleStores(pgDriver(this.pool)); }
|
|
164
167
|
sendFileLedger() { return new PgSendFileLedger(this.pool); }
|
|
@@ -200,6 +203,7 @@ class LocalBackend {
|
|
|
200
203
|
sessionStore;
|
|
201
204
|
resumeAnchorStore;
|
|
202
205
|
approvalExemptionStore;
|
|
206
|
+
approvalNonceStore;
|
|
203
207
|
approvalAskStore = new InMemoryApprovalAskStore();
|
|
204
208
|
sendFileLedgerStore;
|
|
205
209
|
workflowJournalStore;
|
|
@@ -238,6 +242,7 @@ class LocalBackend {
|
|
|
238
242
|
});
|
|
239
243
|
this.resumeAnchorStore = new FileResumeAnchorStore(this.fileBackend.root);
|
|
240
244
|
this.approvalExemptionStore = new FileApprovalExemptionStore(this.fileBackend.root);
|
|
245
|
+
this.approvalNonceStore = new FileApprovalNonceStore(this.fileBackend.root);
|
|
241
246
|
this.sendFileLedgerStore = new FileSendFileLedger(this.fileBackend.root);
|
|
242
247
|
this.workflowJournalStore = new FileWorkflowJournalStore(this.fileBackend.root);
|
|
243
248
|
this.outcomeSinkInst = new FileOutcomeSink(this.fileBackend.root);
|
|
@@ -255,6 +260,7 @@ class LocalBackend {
|
|
|
255
260
|
this.runStore.dispose();
|
|
256
261
|
this.resumeAnchorStore.dispose();
|
|
257
262
|
this.approvalExemptionStore.dispose();
|
|
263
|
+
this.approvalNonceStore.dispose();
|
|
258
264
|
this.sendFileLedgerStore.dispose();
|
|
259
265
|
this.workflowJournalStore.dispose();
|
|
260
266
|
this.checkpointStoreInst?.close();
|
|
@@ -264,6 +270,7 @@ class LocalBackend {
|
|
|
264
270
|
run() { return this.runStore; }
|
|
265
271
|
resumeAnchor() { return this.resumeAnchorStore; }
|
|
266
272
|
approvalExemption() { return this.approvalExemptionStore; }
|
|
273
|
+
approvalNonce() { return this.approvalNonceStore; }
|
|
267
274
|
approvalAsk() { return this.approvalAskStore; }
|
|
268
275
|
permissionRule() {
|
|
269
276
|
if (!this.permissionRuleStoresInst) {
|
|
@@ -69,7 +69,24 @@ export const SCHEMA_STATEMENTS = [
|
|
|
69
69
|
payload JSON NOT NULL,
|
|
70
70
|
ts DATETIME(3) NOT NULL,
|
|
71
71
|
PRIMARY KEY (session_id, seq),
|
|
72
|
-
UNIQUE KEY uq_session_event_entry (session_id, entry_id)
|
|
72
|
+
UNIQUE KEY uq_session_event_entry (session_id, entry_id),
|
|
73
|
+
-- S-284(7.76.0):「本会话按 seq 倒序的最新一条某 type 事件」这一**类**读法的访问路径。
|
|
74
|
+
-- 列序 = 等值前缀 (session_id, type) + 排序列 seq。
|
|
75
|
+
-- · **热路径(本索引的存在理由)**:computeFloor 的 type = 'compaction' ORDER BY seq DESC LIMIT 1 ——
|
|
76
|
+
-- 每次 wake 的第一条 SQL。缺这条路径时两库都退化成**整会话扫**:TiDB 按 PK 取整 session 区间
|
|
77
|
+
-- (keep order:false)把 N 行 ~1KB payload 全读进 cop 侧 TopN(实测 N=100 → 10000:0.91ms → 30.91ms);
|
|
78
|
+
-- PG 的规划器随表变大从 Index Scan Backward 翻成 Bitmap Heap Scan、再翻成 Seq Scan + Sort
|
|
79
|
+
-- (0.23ms → 1.42ms)。于是「每轮热读 = floor 之上那 ~30 行」这条 §1.5 承诺被第一条 SQL 单方面毁掉。
|
|
80
|
+
-- 补上之后两库都是一次索引定位 + 一行回表:TiDB IndexRangeScan keep order:true, desc + cop Limit 1
|
|
81
|
+
-- (1.04 / 0.90 / 0.87ms),PG Index Scan Backward(0.23 / 0.21 / 0.21ms)—— 与 N 无关。
|
|
82
|
+
-- · **冷路径(**只有 PG 实测吃到**,如实登记)**:getEpochAnchor 的
|
|
83
|
+
-- seq < ? AND type IN ('prompt_epoch','compaction') ORDER BY seq DESC LIMIT 100。实测(N=10058、
|
|
84
|
+
-- 58 条候选):PG 走 Index Scan + 小 Sort,2.04ms → 0.49ms;**TiDB 在 pseudo 统计下不选这条索引**,
|
|
85
|
+
-- 计划仍是整会话 TableRangeScan keep order:true, desc + cop Selection —— 与无索引时同一个计划。
|
|
86
|
+
-- 不在本版加方言索引提示(那是一条特判,且该路径是 rewind 切在 kept tail 内才走的冷回退,不是每轮热读)。
|
|
87
|
+
-- 写路径代价(实测,非估计):多一条二级索引项 ⇒ TiDB 单行 append p50 1.34 → 1.58ms、10058 行批量装载
|
|
88
|
+
-- 845 → 952ms;PG 单行 append p50 0.82 → 0.80ms(噪声内)、批量装载 375 → 405ms。
|
|
89
|
+
KEY idx_session_event_type (session_id, type, seq)
|
|
73
90
|
) COLLATE utf8mb4_bin`,
|
|
74
91
|
`CREATE TABLE IF NOT EXISTS task_run (
|
|
75
92
|
-- ⚠️ 表级 COLLATE 的**非键列**影响(#192 A11,codex 复审 2026-08-08 round2 点名——先前"只有
|
|
@@ -298,6 +315,14 @@ export const SCHEMA_STATEMENTS = [
|
|
|
298
315
|
granted_by VARCHAR(190) NULL,
|
|
299
316
|
created_at DATETIME(3) NOT NULL,
|
|
300
317
|
PRIMARY KEY (session_id, tool_name)
|
|
318
|
+
) COLLATE utf8mb4_bin`,
|
|
319
|
+
`CREATE TABLE IF NOT EXISTS approval_nonce (
|
|
320
|
+
session_id VARCHAR(64) NOT NULL,
|
|
321
|
+
nonce VARCHAR(190) NOT NULL,
|
|
322
|
+
consumed_at_ms BIGINT NOT NULL,
|
|
323
|
+
expires_at_ms BIGINT NOT NULL,
|
|
324
|
+
PRIMARY KEY (session_id, nonce),
|
|
325
|
+
KEY idx_approval_nonce_expires (expires_at_ms)
|
|
301
326
|
) COLLATE utf8mb4_bin`,
|
|
302
327
|
`CREATE TABLE IF NOT EXISTS sendfile_link (
|
|
303
328
|
id VARCHAR(64) NOT NULL,
|
package/dist/principal-jwt.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
|
|
2
|
+
/**
|
|
3
|
+
* 验签允许的**时钟不一致容差**(秒)—— 本仓这条轴上的单一来源。
|
|
4
|
+
*
|
|
5
|
+
* 它回答的是**一跳**:签发方(issuer)与判定这份 token 的那口钟可以差多少(exp/nbf 门的宽限)。
|
|
6
|
+
*
|
|
7
|
+
* 🔴 它**不**回答「两台副本之间差多少」:S-280 的消费记录不再靠任何钟差上界活着 —— 受理窗的判定、
|
|
8
|
+
* 落地后的复核、到期清扫三处读的是**同一口钟**(消费记录店自己的钟,见
|
|
9
|
+
* `plugins/approval-nonce-store.ts` 的 `nowMs()`)。曾经由本常量推导出的那个「副本间钟差」常量已
|
|
10
|
+
* 整条删除:它是一条**猜出来的**上界,没有任何一条腿强制它,而正确的修法是根本不需要这个数。
|
|
11
|
+
*/
|
|
12
|
+
export declare const PRINCIPAL_JWT_CLOCK_SKEW_SEC = 30;
|
|
2
13
|
/**
|
|
3
14
|
* [ref] D-G: verify a compact JWS principal token. Returns the crypto-verified principal (`sub`) + the
|
|
4
15
|
* channel-binding (`bnd`) on success, or `null` on ANY failure (caller fail-closes — NEVER falls back to a header).
|
|
@@ -14,6 +25,7 @@ export declare function verifyPrincipalJwt(token: string, keys: PrincipalJwtKey[
|
|
|
14
25
|
}): {
|
|
15
26
|
sub: string;
|
|
16
27
|
bnd?: string;
|
|
28
|
+
acceptedBeforeMs: number;
|
|
17
29
|
} | null;
|
|
18
30
|
/** The channel-binding value a direct client's principal JWT must carry in `cnf.bnd`: the hex SHA-256 of the
|
|
19
31
|
* CLIENT-VISIBLE action identity (sessionId + boundCallId + boundInputHash — NOT the unexposed checkpointToken).
|
|
@@ -23,6 +35,35 @@ export declare function approvalBnd(env: {
|
|
|
23
35
|
boundCallId?: string | null;
|
|
24
36
|
boundInputHash?: string | null;
|
|
25
37
|
}): string;
|
|
38
|
+
/**
|
|
39
|
+
* 一次性凭据:这份证明**必须被消费掉一次**才算数,以及它的消费记录该活到什么时候。
|
|
40
|
+
* 只有非动作门(plan_review)的证明带它 —— 动作门的一次性由 checkpointToken 给(见 {@link verifyDirectDoorProof})。
|
|
41
|
+
*/
|
|
42
|
+
export interface SingleUseProof {
|
|
43
|
+
/** 签名器为这一次决定新铸的随机值(= 信封第 2 位 `boundCallId`,同时进 `cnf.bnd` 与 HMAC)。 */
|
|
44
|
+
readonly nonce: string;
|
|
45
|
+
/**
|
|
46
|
+
* 这份证明**不再被受理**的时刻(epoch ms,**开区间上界**:`店钟 >= 它` ⇒ 验签已经不收它)。
|
|
47
|
+
* 一个值、两处用,所以它们不可能漂:
|
|
48
|
+
* · 消费记录的到期列逐字落它 —— 清理腿的删除线就是**店钟**,没有任何余量、也不需要;
|
|
49
|
+
* · 消费**落地之后**的复核线(再问一次店钟)—— 消费是一次跨网络的写,验签发生在它之前。
|
|
50
|
+
*
|
|
51
|
+
* 🔴 「它」与「店钟」必须是同一口钟:本值由 `verifyPrincipalJwt` 从 `exp + skew` 算出,而那一关的
|
|
52
|
+
* `nowSec` 由调用方从**店钟**喂进来(`routes/approvals-assistant.ts`)。
|
|
53
|
+
*/
|
|
54
|
+
readonly acceptedBeforeMs: number;
|
|
55
|
+
}
|
|
56
|
+
/** {@link verifyDirectDoorProof} 的判决。`singleUse` 在场 = 调用方必须在改任何状态**之前**消费它一次。 */
|
|
57
|
+
export type DirectDoorProofResult = {
|
|
58
|
+
ok: true;
|
|
59
|
+
principal: string;
|
|
60
|
+
singleUse?: SingleUseProof;
|
|
61
|
+
} | {
|
|
62
|
+
ok: false;
|
|
63
|
+
status: number;
|
|
64
|
+
errorCode: string;
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
26
67
|
/**
|
|
27
68
|
* [ref] D-G: verify the WHOLE direct-door proof for one /decide — the signed principal JWT (identity) + its
|
|
28
69
|
* cnf.bnd channel-binding + the HMAC envelope (integrity) — and return the CRYPTO-VERIFIED principal, or a typed
|
|
@@ -30,6 +71,12 @@ export declare function approvalBnd(env: {
|
|
|
30
71
|
* verify principal → bind → mac-present → mac-valid (so a non-operator never gets a MAC oracle, an unverified
|
|
31
72
|
* caller never reaches the operator list).
|
|
32
73
|
*
|
|
74
|
+
* 🔴 `opts.nowSec`(S-280 / 合并复审 R2):受理窗要按**哪口钟**判。非动作门(plan_review)的反重放是
|
|
75
|
+
* 一条**消费记录**,而那条记录的生死由**消费记录店的钟**决定 —— 所以那条腿必须把店钟喂进来
|
|
76
|
+
* (`floor(店钟/1000)`),否则「还受理 ⇒ 记录还在」就退化成「两台副本的钟差不多」这条没人声明过的假设。
|
|
77
|
+
* 缺席 = 本机墙钟,这对动作门(`/decide`)是对的:它的反重放是 checkpointToken 的一次性,不牵扯第二台
|
|
78
|
+
* 机器上的一张行。
|
|
79
|
+
*
|
|
33
80
|
* 🔴 `opts.actionBinding`: the boundCallId/boundInputHash action-binding is `policy_ask`-
|
|
34
81
|
* SPECIFIC (a pending TOOL call). A NON-ACTION gate — `plan_review` (and any review-pause) — `binds NO action`
|
|
35
82
|
* (core checkpoint-store.ts: pendingAction `{kind:"plan_review"}`, editedPlan is a typed sibling that binds no
|
|
@@ -56,13 +103,6 @@ export declare function verifyDirectDoorProof(proof: {
|
|
|
56
103
|
approvalHmacKeys: ApprovalHmacKey[];
|
|
57
104
|
}, opts?: {
|
|
58
105
|
actionBinding?: boolean;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
principal: string;
|
|
62
|
-
} | {
|
|
63
|
-
ok: false;
|
|
64
|
-
status: number;
|
|
65
|
-
errorCode: string;
|
|
66
|
-
error: string;
|
|
67
|
-
};
|
|
106
|
+
nowSec?: number;
|
|
107
|
+
}): DirectDoorProofResult;
|
|
68
108
|
//# sourceMappingURL=principal-jwt.d.ts.map
|
package/dist/principal-jwt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
2
2
|
import { verifyApprovalHmac } from "./approval-hmac.js";
|
|
3
|
+
export const PRINCIPAL_JWT_CLOCK_SKEW_SEC = 30;
|
|
3
4
|
function b64urlToBuf(s) {
|
|
4
5
|
return Buffer.from(s, "base64url");
|
|
5
6
|
}
|
|
@@ -61,15 +62,15 @@ export function verifyPrincipalJwt(token, keys, expect) {
|
|
|
61
62
|
if (typeof payload.sub !== "string" || payload.sub.length === 0)
|
|
62
63
|
return null;
|
|
63
64
|
const now = expect.nowSec ?? Math.floor(Date.now() / 1000);
|
|
64
|
-
const skew = expect.skewSec ??
|
|
65
|
-
if (typeof payload.exp !== "number" || payload.exp + skew < now)
|
|
65
|
+
const skew = expect.skewSec ?? PRINCIPAL_JWT_CLOCK_SKEW_SEC;
|
|
66
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp) || payload.exp + skew < now)
|
|
66
67
|
return null;
|
|
67
|
-
if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || payload.nbf - skew > now))
|
|
68
|
+
if (payload.nbf !== undefined && (typeof payload.nbf !== "number" || !Number.isFinite(payload.nbf) || payload.nbf - skew > now))
|
|
68
69
|
return null;
|
|
69
70
|
if (expect.maxTtlSec && payload.exp - now > expect.maxTtlSec + skew)
|
|
70
71
|
return null;
|
|
71
72
|
const bnd = typeof payload.cnf?.bnd === "string" ? payload.cnf.bnd : undefined;
|
|
72
|
-
return { sub: payload.sub, bnd };
|
|
73
|
+
return { sub: payload.sub, bnd, acceptedBeforeMs: (Math.floor(payload.exp + skew) + 1) * 1000 };
|
|
73
74
|
}
|
|
74
75
|
export function approvalBnd(env) {
|
|
75
76
|
return createHash("sha256").update(JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null])).digest("hex");
|
|
@@ -78,11 +79,16 @@ export function verifyDirectDoorProof(proof, envelope, config, opts) {
|
|
|
78
79
|
const actionBinding = opts?.actionBinding ?? true;
|
|
79
80
|
if (!proof.jwt)
|
|
80
81
|
return { ok: false, status: 401, errorCode: "principal_unverified", error: "direct door requires a signed principal token (X-Approval-Principal-Token)" };
|
|
81
|
-
const verified = verifyPrincipalJwt(proof.jwt, config.principalJwtPubkeys, { iss: config.principalJwtIss ?? "\0", aud: config.principalJwtAud ?? "\0", maxTtlSec: config.principalJwtMaxTtlSec });
|
|
82
|
+
const verified = verifyPrincipalJwt(proof.jwt, config.principalJwtPubkeys, { iss: config.principalJwtIss ?? "\0", aud: config.principalJwtAud ?? "\0", maxTtlSec: config.principalJwtMaxTtlSec, nowSec: opts?.nowSec });
|
|
82
83
|
if (!verified)
|
|
83
84
|
return { ok: false, status: 401, errorCode: "principal_unverified", error: "principal token failed verification" };
|
|
84
|
-
if (actionBinding
|
|
85
|
-
|
|
85
|
+
if (actionBinding) {
|
|
86
|
+
if (!envelope.boundInputHash)
|
|
87
|
+
return { ok: false, status: 400, errorCode: "approval_mac_required", error: "direct door requires boundInputHash (the bound action) in the body" };
|
|
88
|
+
}
|
|
89
|
+
else if (!envelope.boundCallId) {
|
|
90
|
+
return { ok: false, status: 400, errorCode: "approval_nonce_required", error: "direct door requires a single-use nonce in boundCallId for a non-action gate (sign it into cnf.bnd and the HMAC envelope; see ASSISTANT-WIRE-CONTRACT §6.1/§6.2)" };
|
|
91
|
+
}
|
|
86
92
|
if (verified.bnd !== approvalBnd(envelope))
|
|
87
93
|
return { ok: false, status: 401, errorCode: "principal_unbound", error: "principal token is not bound to this decision (cnf.bnd mismatch)" };
|
|
88
94
|
if (!proof.mac)
|
|
@@ -90,6 +96,8 @@ export function verifyDirectDoorProof(proof, envelope, config, opts) {
|
|
|
90
96
|
const ok = verifyApprovalHmac({ sessionId: envelope.sessionId, boundCallId: envelope.boundCallId ?? null, boundInputHash: envelope.boundInputHash ?? null, decision: envelope.decision, reason: envelope.reason ?? null }, proof.mac, proof.kid, config.approvalHmacKeys);
|
|
91
97
|
if (!ok)
|
|
92
98
|
return { ok: false, status: 401, errorCode: "approval_mac_invalid", error: "approval envelope MAC invalid" };
|
|
93
|
-
return
|
|
99
|
+
return actionBinding
|
|
100
|
+
? { ok: true, principal: verified.sub }
|
|
101
|
+
: { ok: true, principal: verified.sub, singleUse: { nonce: envelope.boundCallId, acceptedBeforeMs: verified.acceptedBeforeMs } };
|
|
94
102
|
}
|
|
95
103
|
//# sourceMappingURL=principal-jwt.js.map
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -468,7 +468,8 @@ export type RuleRefusalReason = Extract<CardRulePersisted, {
|
|
|
468
468
|
* 部署整条车道判死。 */
|
|
469
469
|
| "rule_governance_forced"
|
|
470
470
|
/**
|
|
471
|
-
* [ref]
|
|
471
|
+
* [ref]:**这一行**没有可铸规则的素材。原为 durable(PARKED)迟到决议腿专属;自 live 腿也接上素材闸起**两腿同词**
|
|
472
|
+
* (第四条成因:命令含脱敏器认得的凭据形 ⇒ 素材整只扣下,两腿同谓词)。
|
|
472
473
|
*
|
|
473
474
|
* 成因闭集(三条,处置相同):①这只 ask 从没进过规则车道(治理档 / 无属主 / 引擎没铸候选 / args 里
|
|
474
475
|
* 读不出命令原字节)⇒ `rule_command` 列 NULL;②行由**加这两列之前**的构建落下;③行上的卡读不回来
|
package/dist/tool-approval.js
CHANGED
|
@@ -31,10 +31,13 @@ const DEFAULT_PARK_TOMBSTONE_MAX = 2000;
|
|
|
31
31
|
const DEFAULT_PARK_TOMBSTONE_TTL_MS = 60 * 60_000;
|
|
32
32
|
const DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX = 128;
|
|
33
33
|
const PARKING_RETRY_AFTER_MS = 250;
|
|
34
|
+
function ruleMaterialCarriesCredentialShape(command) {
|
|
35
|
+
return redactSecrets(command, { observe: false }) !== normalizeForRedaction(command);
|
|
36
|
+
}
|
|
34
37
|
function durableRuleMaterial(material) {
|
|
35
38
|
if (material === undefined)
|
|
36
39
|
return { ruleCommand: null, ruleScopeRoot: null };
|
|
37
|
-
if (
|
|
40
|
+
if (ruleMaterialCarriesCredentialShape(material.command)) {
|
|
38
41
|
defaultLogger.warn("approval rule material withheld: the adjudicated command carries credential-shaped bytes", {
|
|
39
42
|
commandChars: material.command.length,
|
|
40
43
|
});
|
|
@@ -1799,6 +1802,10 @@ export class ToolApprovalCoordinator {
|
|
|
1799
1802
|
return withFlag(false, "rule_store_error");
|
|
1800
1803
|
if (inputWasEdited)
|
|
1801
1804
|
return withFlag(false, "rule_input_edited");
|
|
1805
|
+
if (ruleMaterialCarriesCredentialShape(material.command)) {
|
|
1806
|
+
defaultLogger.warn("approval rule material withheld on the live leg: the adjudicated command carries credential-shaped bytes", { commandChars: material.command.length });
|
|
1807
|
+
return withFlag(false, "rule_material_absent");
|
|
1808
|
+
}
|
|
1802
1809
|
if (persistRule.kind === "batch") {
|
|
1803
1810
|
const offer = material.offers[persistRule.batchOfferIndex];
|
|
1804
1811
|
if (offer === undefined || offer.kind !== "batch")
|
package/dist/trace/redact.d.ts
CHANGED
|
@@ -28,10 +28,13 @@
|
|
|
28
28
|
* `redactModelText`(四座同调:结果两叶经 `redactTerminalResult`,账本两行经 `redactLedgerEventData`);
|
|
29
29
|
* 它定的是**写回**判据(认出凭据形才改字节、抛则占位),不是第二张规则表。
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
* (
|
|
33
|
-
* ⚠️
|
|
34
|
-
* (
|
|
31
|
+
* 臂分**三段 + 一个候选池**,段之间的先后是契约(上下文越多越靠前):结构段(PEM/URL/JSON + 三条体扫描器)
|
|
32
|
+
* → 词法关键字段(`Bearer`/`Basic`/`token`)→ **候选池** → URL 收尾归一化。
|
|
33
|
+
* ⚠️ **池内没有优先序,自 7.76.0**:{@link SHAPE_SECRET_PATTERNS}(裸形八条)与 {@link FRAGMENT_SECRET_PATTERNS}
|
|
34
|
+
* ([ref] 前缀残片八条,上游先截后脱的产物 ⇒ 带 `:partial` 后缀的 token)并进**同一个**候选池
|
|
35
|
+
* ({@link armPoolRule}):全臂看同一份未改写文本收候选跨度 → 不动点 → 并集律解重叠 → 一次替换,
|
|
36
|
+
* **池内臂序对输出无语义**(7.75.2 只做到裸形一段,残片段仍逐臂改写 ⇒ S-282 的两条漏形)。
|
|
37
|
+
* 左边界律因此有**三种**边界:非词内字符 ∨ 转义分隔单元尾 ∨ **一枚已收候选跨度的尾**(= 凭据尾)。
|
|
35
38
|
*/
|
|
36
39
|
/** 本模块替换 token 的**闭形**正则源(`«redacted»` / `«redacted:aws-key»` / `«redacted:gh-token:partial»` …):
|
|
37
40
|
* URL-userinfo 臂用它把「用户名位上已经是本模块 token」整个当一个用户名单元吃掉。模块加载时对
|