@sema-agent/server 7.12.0 → 7.14.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 +51 -2
- package/dist/adoption/plan.d.ts +38 -4
- package/dist/adoption/plan.js +72 -0
- package/dist/adoption/quiesce.d.ts +70 -0
- package/dist/adoption/quiesce.js +148 -0
- package/dist/adoption/runner.js +63 -5
- package/dist/adoption/sql.d.ts +15 -0
- package/dist/adoption/sql.js +18 -0
- package/dist/adoption/wire.d.ts +7 -1
- package/dist/adoption/wire.js +6 -0
- package/dist/approval-card.d.ts +5 -0
- package/dist/approval-card.js +22 -0
- package/dist/boot/coordinators.js +2 -1
- package/dist/boot/memory-boundary.d.ts +84 -0
- package/dist/boot/memory-boundary.js +110 -0
- package/dist/boot/permission-rules-audit.js +29 -1
- package/dist/boot/reapers.d.ts +15 -0
- package/dist/boot/reapers.js +101 -44
- package/dist/boot/resolve-spec.js +31 -0
- package/dist/boot/runner-deps.d.ts +16 -2
- package/dist/boot/runner-deps.js +24 -4
- package/dist/boot/stores.js +93 -3
- package/dist/capabilities/memory-notice.d.ts +83 -0
- package/dist/capabilities/memory-notice.js +90 -0
- package/dist/config-types.d.ts +100 -11
- package/dist/config.d.ts +1 -1
- package/dist/config.js +111 -1
- package/dist/governance-ask-marks.js +2 -1
- package/dist/http/active-run-conflict.d.ts +33 -8
- package/dist/http/active-run-conflict.js +37 -2
- package/dist/http/routes/adoption.js +25 -2
- package/dist/http/routes/approvals-assistant.js +33 -3
- package/dist/http/routes/capabilities.js +35 -5
- package/dist/http/routes/images.js +18 -0
- package/dist/http/routes/runs.js +21 -5
- package/dist/http/routes/tasks.js +18 -6
- package/dist/http/routes/trace-usage.js +43 -14
- package/dist/http/server.d.ts +35 -9
- package/dist/http/server.js +111 -17
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/main.js +46 -6
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/plugins/adoption-log-sql.d.ts +40 -0
- package/dist/plugins/adoption-log-sql.js +69 -2
- package/dist/plugins/approval-ask-store-sql.d.ts +2 -1
- package/dist/plugins/approval-ask-store-sql.js +2 -1
- package/dist/plugins/file-run-store.d.ts +85 -1
- package/dist/plugins/file-run-store.js +450 -17
- package/dist/plugins/memory-embedder.d.ts +44 -0
- package/dist/plugins/memory-embedder.js +173 -0
- package/dist/plugins/permission-rule-store-sql.d.ts +45 -0
- package/dist/plugins/permission-rule-store-sql.js +60 -2
- package/dist/plugins/shared-memory-store-sql.d.ts +23 -9
- package/dist/plugins/shared-memory-store-sql.js +55 -18
- package/dist/plugins/sql-driver.d.ts +19 -0
- package/dist/plugins/sql-driver.js +12 -0
- package/dist/plugins/store-backend.d.ts +3 -1
- package/dist/plugins/store-backend.js +24 -1
- package/dist/plugins/tidb-pool.js +11 -4
- package/dist/plugins/tool-result-store-sql.d.ts +35 -2
- package/dist/plugins/tool-result-store-sql.js +127 -11
- package/dist/plugins/web-search.d.ts +3 -1
- package/dist/plugins/web-search.js +3 -1
- package/dist/rules-consent.d.ts +33 -4
- package/dist/rules-consent.js +43 -2
- package/dist/run-local.js +6 -2
- package/dist/runtime-governance.d.ts +33 -0
- package/dist/runtime-governance.js +32 -0
- package/dist/security.js +3 -1
- package/dist/tool-approval.d.ts +32 -0
- package/dist/tool-approval.js +39 -0
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/package.json +3 -3
|
@@ -187,13 +187,20 @@ export const SCHEMA_STATEMENTS = [
|
|
|
187
187
|
KEY idx_rate_limit_window (window_bucket)
|
|
188
188
|
) COLLATE utf8mb4_bin`,
|
|
189
189
|
// Durable backing for offloaded large tool results (core 1.47/1.49). One row per offload ref
|
|
190
|
-
// (`tr_<sessionId
|
|
190
|
+
// (`tr_<sessionId>~<toolCallId>~<contentSeg>` since core 5.26.0 #119, globally unique → PK); content is the full tool output moved out of
|
|
191
191
|
// context. Write-once (INSERT IGNORE); TTL-reaped by created_at (idx_created). LONGTEXT holds large
|
|
192
192
|
// file/diff reads. Lets an async run wake on any replica and still read_tool_result the full text.
|
|
193
|
+
// #119(core 5.26.0 提货):`ref` 从 VARCHAR(190) 抬到 518 = core 导出的 `MAX_MINTED_TOOL_RESULT_REF_CHARS`
|
|
194
|
+
// (`"tr_"` + 四段 × 128 + 三个 `~` 分隔),并新增 #119 出处两列(NULL = 该行无属主,读面 fail-closed)。
|
|
195
|
+
// 抬宽不是保守裕度:core 的 d.ts 逐字点名过旧宽度(「TiDB's was VARCHAR(191), which a long three-segment
|
|
196
|
+
// ref overruns — a truncating key aliases distinct refs」)—— 截断的主键会把两枚不同的 ref 折成同一行,
|
|
197
|
+
// 于是 A 的产物能从 B 的 ref 读出来。utf8mb4 下 518 字符 = 2072 字节,仍在 InnoDB/TiDB 3072 字节键长内。
|
|
193
198
|
`CREATE TABLE IF NOT EXISTS tool_result (
|
|
194
|
-
ref
|
|
195
|
-
content
|
|
196
|
-
|
|
199
|
+
ref VARCHAR(518) NOT NULL,
|
|
200
|
+
content LONGTEXT NOT NULL,
|
|
201
|
+
owner_session_id VARCHAR(190) NULL,
|
|
202
|
+
owner_task_id VARCHAR(190) NULL,
|
|
203
|
+
created_at DATETIME(3) NOT NULL,
|
|
197
204
|
PRIMARY KEY (ref),
|
|
198
205
|
KEY idx_tool_result_created (created_at)
|
|
199
206
|
) COLLATE utf8mb4_bin`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolResultStore, ToolResultSlice } from "@sema-agent/core";
|
|
1
|
+
import type { ToolResultProvenance, ToolResultStore, ToolResultSlice } from "@sema-agent/core";
|
|
2
2
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
3
3
|
import type { Pool as PgPool, PoolClient as PgPoolClient } from "pg";
|
|
4
4
|
import { type SqlDriver } from "./sql-driver.js";
|
|
@@ -8,6 +8,21 @@ import { type SqlDriver } from "./sql-driver.js";
|
|
|
8
8
|
export declare const PG_TOOL_RESULT_SCHEMA: string[];
|
|
9
9
|
/** Idempotent schema apply for the tool_result table (for the integration test to call). */
|
|
10
10
|
export declare function ensureSchema(pool: PgPool | PgPoolClient): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* #119 升级前置断言 —— **拒启**,不是 warn(codex 复审 [high],已核真)。
|
|
13
|
+
*
|
|
14
|
+
* 病:本仓不发 `ALTER TABLE` 迁移(标准裁定:改列就改 CREATE + 删库重建)。于是一台**没删表**就升上来的
|
|
15
|
+
* 部署,`CREATE TABLE IF NOT EXISTS` 对它是空操作,旧表既没有出处两列也只有 `VARCHAR(190)` 的 ref 列。
|
|
16
|
+
* 那样跑起来的后果不是「少个功能」:每一次 offload 写都会撞 unknown column 报错,而 core 的 offload
|
|
17
|
+
* 失败臂会把错误吞成一条内联占位("[offload LOST at write time…]")—— 服务照跑,工具产物全丢,日志里只有
|
|
18
|
+
* 一句看不出根因的话。这正是「响亮或 fail-closed,禁静默降级」那条要挡的形态。
|
|
19
|
+
*
|
|
20
|
+
* 判据用**能力探测**而不是版本号/information_schema:发一条恒空的 `WHERE 1=0` 读,列不在就报错。
|
|
21
|
+
* 探不通即拒启,错误文案直接给出两条方言的动作(删表重建),不让运维去猜。
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertToolResultProvenanceSchema(query: (sql: string) => Promise<{
|
|
24
|
+
rows: Record<string, unknown>[];
|
|
25
|
+
}>, dialect: "tidb" | "pg"): Promise<void>;
|
|
11
26
|
/** Dual-dialect durable ToolResultStore. See the file header for the dialect-delta ledger. */
|
|
12
27
|
export declare class SqlToolResultStore implements ToolResultStore {
|
|
13
28
|
protected readonly db: SqlDriver;
|
|
@@ -24,7 +39,25 @@ export declare class SqlToolResultStore implements ToolResultStore {
|
|
|
24
39
|
* 抛错文案与 core 逐字相同,所以 wire 与既有钉都不变。 */
|
|
25
40
|
private isUnsafeRef;
|
|
26
41
|
private assertSafeRef;
|
|
27
|
-
|
|
42
|
+
/**
|
|
43
|
+
* #119(core 5.26.0)—— 出处的**写面**。语义一律取 core 单源(`assertToolResultProvenanceMatch`),
|
|
44
|
+
* 本方法只负责把它落到 SQL 上:
|
|
45
|
+
* · **写一次选举同时定属主** —— 内容与属主是同一条 INSERT,不存在「内容写进去了属主还没跟上」的窗口
|
|
46
|
+
* (core 头注允许 file 双对象后端出现这个窗口,单行后端没有,别自造);
|
|
47
|
+
* · 抢输的那一方(IGNORE / DO NOTHING ⇒ affected=0)回读已存属主再判:同属主(或本次无出处)=
|
|
48
|
+
* 幂等空转;异属主 = `ToolResultRefConflictError` typed 拒。**绝不静默 keep-first**:ref 是主键,
|
|
49
|
+
* 静默空转会让第二位写者拿着自己的 ref 读回第一位的字节;
|
|
50
|
+
* · 无出处的行永久 unowned —— 后续带出处的 put 不回填(没有证据的收养),`ownerOf` 继续答 undefined。
|
|
51
|
+
*/
|
|
52
|
+
put(ref: string, content: string, provenance?: ToolResultProvenance): Promise<void>;
|
|
53
|
+
/** 一次回读同时回答两件事:**行在不在**(reap/purge 窗口的判别位)与**属主是谁**。两件事必须来自同一
|
|
54
|
+
* 条 SELECT —— 分两次问会重新引入它要消灭的那个窗口。 */
|
|
55
|
+
private readOwner;
|
|
56
|
+
/**
|
|
57
|
+
* #119 —— 出处的**读面**。未知 ref 与「存了但无属主」两种情况都答 `undefined`:读面对二者一视同仁
|
|
58
|
+
* (fail-closed,谁都没被授权),所以这里也不必把它们分开报。
|
|
59
|
+
*/
|
|
60
|
+
ownerOf(ref: string): Promise<ToolResultProvenance | undefined>;
|
|
28
61
|
get(ref: string, opts?: {
|
|
29
62
|
offset?: number;
|
|
30
63
|
limit?: number;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* offloaded result (the ref misses → the model is told it's unavailable; the preview still stands, so it
|
|
10
10
|
* degrades, never crashes). A durable store survives wake/resume across the stateless fleet.
|
|
11
11
|
*
|
|
12
|
-
* core namespaces the ref as `tr_<sessionId
|
|
12
|
+
* core namespaces the ref as `tr_<sessionId>~<toolCallId>~<contentCoordinate>` (1.49, re-minted injectively in
|
|
13
|
+
* 5.26.0 #119 — `~`-separated, four segments max, content-addressed) → globally unique → usable directly as
|
|
13
14
|
* the PRIMARY KEY (no composite key needed). `put` is write-once (idempotent on replay/retry: a retry is
|
|
14
15
|
* a NEW toolCallId → new ref, so a given ref never changes content). Retention is anchored to a run's
|
|
15
16
|
* RECOVERABLE window via TTL (`reapOlderThan`): a resumable run may re-fetch an old ref after wake, but a
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
* sessions' rows); PG spells `ESCAPE '\'` (its default escape char already, kept for lock-step intent
|
|
36
37
|
* with the TiDB twin rather than out of necessity).
|
|
37
38
|
*/
|
|
38
|
-
import { assertSafeToolResultRef } from "@sema-agent/core";
|
|
39
|
+
import { MAX_MINTED_TOOL_RESULT_REF_CHARS, assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance } from "@sema-agent/core";
|
|
39
40
|
import { escapeLike } from "./sql-escape.js";
|
|
40
41
|
import { pgSanitizeText } from "./pg-safe-json.js";
|
|
41
42
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
@@ -43,10 +44,14 @@ import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
|
43
44
|
* (LONGTEXT→TEXT, DATETIME(3)→TIMESTAMPTZ(3), inline KEY→separate CREATE INDEX). Disjoint from other stores'
|
|
44
45
|
* tables, so a self-contained ensureSchema is safe (central pg-pool aggregation is done separately). */
|
|
45
46
|
export const PG_TOOL_RESULT_SCHEMA = [
|
|
47
|
+
// #119(core 5.26.0):宽度与出处两列与 TiDB 双生逐字对齐 —— 理由写在 tidb-pool.ts 的同一张 DDL 上
|
|
48
|
+
// (`MAX_MINTED_TOOL_RESULT_REF_CHARS` = 518;截断主键 = 两枚 ref 折成一行 = 身份故障)。
|
|
46
49
|
`CREATE TABLE IF NOT EXISTS tool_result (
|
|
47
|
-
ref
|
|
48
|
-
content
|
|
49
|
-
|
|
50
|
+
ref VARCHAR(518) COLLATE "C" NOT NULL,
|
|
51
|
+
content TEXT COLLATE "C" NOT NULL,
|
|
52
|
+
owner_session_id VARCHAR(190) COLLATE "C" NULL,
|
|
53
|
+
owner_task_id VARCHAR(190) COLLATE "C" NULL,
|
|
54
|
+
created_at TIMESTAMPTZ(3) NOT NULL,
|
|
50
55
|
PRIMARY KEY (ref)
|
|
51
56
|
)`,
|
|
52
57
|
`CREATE INDEX IF NOT EXISTS idx_tool_result_created ON tool_result (created_at)`,
|
|
@@ -56,6 +61,49 @@ export async function ensureSchema(pool) {
|
|
|
56
61
|
for (const stmt of PG_TOOL_RESULT_SCHEMA)
|
|
57
62
|
await pool.query(stmt);
|
|
58
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* #119 升级前置断言 —— **拒启**,不是 warn(codex 复审 [high],已核真)。
|
|
66
|
+
*
|
|
67
|
+
* 病:本仓不发 `ALTER TABLE` 迁移(标准裁定:改列就改 CREATE + 删库重建)。于是一台**没删表**就升上来的
|
|
68
|
+
* 部署,`CREATE TABLE IF NOT EXISTS` 对它是空操作,旧表既没有出处两列也只有 `VARCHAR(190)` 的 ref 列。
|
|
69
|
+
* 那样跑起来的后果不是「少个功能」:每一次 offload 写都会撞 unknown column 报错,而 core 的 offload
|
|
70
|
+
* 失败臂会把错误吞成一条内联占位("[offload LOST at write time…]")—— 服务照跑,工具产物全丢,日志里只有
|
|
71
|
+
* 一句看不出根因的话。这正是「响亮或 fail-closed,禁静默降级」那条要挡的形态。
|
|
72
|
+
*
|
|
73
|
+
* 判据用**能力探测**而不是版本号/information_schema:发一条恒空的 `WHERE 1=0` 读,列不在就报错。
|
|
74
|
+
* 探不通即拒启,错误文案直接给出两条方言的动作(删表重建),不让运维去猜。
|
|
75
|
+
*/
|
|
76
|
+
export async function assertToolResultProvenanceSchema(query, dialect) {
|
|
77
|
+
// ② 键宽(codex 复审 round2 [high],已核真):只探两列会放过「有出处列、但 ref 仍是 VARCHAR(190)」的
|
|
78
|
+
// 半迁移表 —— 而这道断言的文案与 CHANGELOG 都在宣称它护着加宽后的键。截断的主键把两枚不同的 ref 折成
|
|
79
|
+
// 同一行 = 身份故障,正是本次升级要消灭的东西,所以宽度必须**真查**,不能靠「列在 ⇒ 表是新的」推断。
|
|
80
|
+
const widthRow = await query(dialect === "tidb"
|
|
81
|
+
? "SELECT CHARACTER_MAXIMUM_LENGTH AS len FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'tool_result' AND column_name = 'ref'"
|
|
82
|
+
: "SELECT character_maximum_length AS len FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'tool_result' AND column_name = 'ref'").catch(() => ({ rows: [] }));
|
|
83
|
+
const width = Number(widthRow.rows[0]?.len ?? NaN);
|
|
84
|
+
// 查不到宽度(权限受限的 information_schema / 意外的列类型)⇒ **不**据此拒启:那是「没看见」,不是
|
|
85
|
+
// 「看见了不合格」。看见了才判,判就判死。
|
|
86
|
+
if (Number.isFinite(width) && width < MAX_MINTED_TOOL_RESULT_REF_CHARS) {
|
|
87
|
+
throw new Error(`tool_result.ref is VARCHAR(${width}) but core 5.26.0 mints refs up to ${MAX_MINTED_TOOL_RESULT_REF_CHARS} characters ` +
|
|
88
|
+
`(#119 made the ref injective: four \`~\`-separated segments). A truncating primary key aliases two distinct refs onto ` +
|
|
89
|
+
`one row — one run's tool output reads back from another run's ref. This repository ships no ALTER TABLE migrations: ` +
|
|
90
|
+
`run \`DROP TABLE tool_result;\` on the ${dialect === "pg" ? "PostgreSQL" : "MySQL-protocol"} backend and restart ` +
|
|
91
|
+
`(the schema is recreated at boot; offloaded results are a recoverable-window cache, inline previews are unaffected).`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
await query("SELECT owner_session_id, owner_task_id FROM tool_result WHERE 1=0");
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
throw new Error(`tool_result is missing the #119 provenance columns (owner_session_id / owner_task_id) — refusing to start. ` +
|
|
98
|
+
`This release picks up @sema-agent/core 5.26.0, which changed how an offloaded tool result is addressed ` +
|
|
99
|
+
`(the ref grew past the old VARCHAR(190) key) and made the store record who wrote each row. This repository ` +
|
|
100
|
+
`ships no ALTER TABLE migrations, so the table must be recreated: run \`DROP TABLE tool_result;\` on the ` +
|
|
101
|
+
`${dialect === "pg" ? "PostgreSQL" : "MySQL-protocol"} backend and restart — the schema is recreated at boot. ` +
|
|
102
|
+
`Offloaded tool results are a recoverable-window cache (the inline previews in transcripts are unaffected), ` +
|
|
103
|
+
`so the cost is at most the full text of results from runs still in flight. ` +
|
|
104
|
+
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
59
107
|
/** Dual-dialect durable ToolResultStore. See the file header for the dialect-delta ledger. */
|
|
60
108
|
export class SqlToolResultStore {
|
|
61
109
|
db;
|
|
@@ -87,7 +135,17 @@ export class SqlToolResultStore {
|
|
|
87
135
|
assertSafeRef(ref) {
|
|
88
136
|
assertSafeToolResultRef(ref);
|
|
89
137
|
}
|
|
90
|
-
|
|
138
|
+
/**
|
|
139
|
+
* #119(core 5.26.0)—— 出处的**写面**。语义一律取 core 单源(`assertToolResultProvenanceMatch`),
|
|
140
|
+
* 本方法只负责把它落到 SQL 上:
|
|
141
|
+
* · **写一次选举同时定属主** —— 内容与属主是同一条 INSERT,不存在「内容写进去了属主还没跟上」的窗口
|
|
142
|
+
* (core 头注允许 file 双对象后端出现这个窗口,单行后端没有,别自造);
|
|
143
|
+
* · 抢输的那一方(IGNORE / DO NOTHING ⇒ affected=0)回读已存属主再判:同属主(或本次无出处)=
|
|
144
|
+
* 幂等空转;异属主 = `ToolResultRefConflictError` typed 拒。**绝不静默 keep-first**:ref 是主键,
|
|
145
|
+
* 静默空转会让第二位写者拿着自己的 ref 读回第一位的字节;
|
|
146
|
+
* · 无出处的行永久 unowned —— 后续带出处的 put 不回填(没有证据的收养),`ownerOf` 继续答 undefined。
|
|
147
|
+
*/
|
|
148
|
+
async put(ref, content, provenance) {
|
|
91
149
|
this.assertSafeRef(ref);
|
|
92
150
|
// Sanitize invalid UTF-16 (lone/half surrogates — a JS string CAN hold them, e.g. a binary-ish tool
|
|
93
151
|
// output) to U+FFFD so the column accepts the FULL value via a Buffer utf8 round-trip (valid content is
|
|
@@ -102,7 +160,58 @@ export class SqlToolResultStore {
|
|
|
102
160
|
if (this.db.dialect === "pg")
|
|
103
161
|
safe = pgSanitizeText(safe);
|
|
104
162
|
// write-once / keep-first: a replay re-puts the SAME ref+content → IGNORE / DO NOTHING keeps the original row.
|
|
105
|
-
|
|
163
|
+
const owner = provenance !== undefined ? normalizeToolResultProvenance(provenance) : undefined;
|
|
164
|
+
const insert = async () => (await this.db.query(this.q("INSERT IGNORE INTO tool_result (ref, content, owner_session_id, owner_task_id, created_at) VALUES (?,?,?,?,?)", "INSERT INTO tool_result (ref, content, owner_session_id, owner_task_id, created_at) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (ref) DO NOTHING"), [ref, safe, owner?.sessionId ?? null, owner?.taskId ?? null, new Date()])).affected;
|
|
165
|
+
// 🔴 codex 复审(medium,已核真)——**抢输之后那一行可能已经不在了**。选举(INSERT)与观察(SELECT)
|
|
166
|
+
// 是两条语句,中间可以插进 TTL reap 或 §0.5 会话清除。那时 `readOwner` 回 `absent`,而 core 的比较器
|
|
167
|
+
// 对「缺一侧」是**不下判决**的(设计如此:无出处的行不许被收养)—— 于是 put 会**成功返回**,而库里
|
|
168
|
+
// 既没有内容也没有属主,调用方拿着一枚当场读不出来的 ref。
|
|
169
|
+
//
|
|
170
|
+
// 🔴 处置是**抛**,不是重投(codex 复审两轮的合取解;第一版写的重投被第二轮当场驳回,理由成立):
|
|
171
|
+
// 重投会让**删除输**。§0.5 会话清除 DELETE 掉这一行之后,一次重投就把它原地复活 —— 而 core 的部分
|
|
172
|
+
// offload 写点是不 await 的,能活过 active-run 围栏,于是被删会话的产物重新落库、留到 TTL 才消失,
|
|
173
|
+
// 直接违背 E21 的删除权保证。「少存一份可恢复窗口内的缓存」与「删了又回来」不是同一个量级的代价。
|
|
174
|
+
// 抛出去会走到 core 的 offload 失败臂 —— 它把这次卸载记成 "[offload LOST at write time: …]" 并保住
|
|
175
|
+
// 内联预览,模型看到的是实话。
|
|
176
|
+
if ((await insert()) > 0)
|
|
177
|
+
return; // 本次赢下选举:内容与属主同一条语句落地
|
|
178
|
+
const stored = await this.readOwner(ref);
|
|
179
|
+
if (stored.present) {
|
|
180
|
+
// 抢输(或纯重放):属主判定归 core 的单源比较器 —— 「同属主」在两个 store 里不许有两种含义。
|
|
181
|
+
assertToolResultProvenanceMatch(ref, stored.owner, owner);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`tool-result store: ref ${JSON.stringify(ref)} lost the write-once election and the elected row was already gone ` +
|
|
185
|
+
`(a concurrent TTL reap or session purge deleted it). Nothing was stored — reporting the loss rather than ` +
|
|
186
|
+
`re-inserting (which would resurrect a purged session's content) or returning a ref that reads back empty.`);
|
|
187
|
+
}
|
|
188
|
+
/** 一次回读同时回答两件事:**行在不在**(reap/purge 窗口的判别位)与**属主是谁**。两件事必须来自同一
|
|
189
|
+
* 条 SELECT —— 分两次问会重新引入它要消灭的那个窗口。 */
|
|
190
|
+
async readOwner(ref) {
|
|
191
|
+
const { rows } = await this.db.query(this.q("SELECT owner_session_id, owner_task_id FROM tool_result WHERE ref = ?", "SELECT owner_session_id, owner_task_id FROM tool_result WHERE ref = $1"), [ref]);
|
|
192
|
+
const row = rows[0];
|
|
193
|
+
if (!row)
|
|
194
|
+
return { present: false, owner: undefined };
|
|
195
|
+
// 🔴 缺席的判别位**只有 SQL NULL**(codex 复审 round2 [medium],已核真):曾经写成
|
|
196
|
+
// `sessionId.length === 0` 也算无属主 —— 那会把一个 `sessionId: ""` 的合法(core 的
|
|
197
|
+
// `normalizeToolResultProvenance` 不拒空串)出处在往返中悄悄折成 unowned,与内存后端的逐字比较分家;
|
|
198
|
+
// `taskId: ""` 折成缺席更糟 —— 授权面会从「task 级」放宽成「整条 session」。字符串一律原样还原。
|
|
199
|
+
const sessionId = row.owner_session_id;
|
|
200
|
+
if (typeof sessionId !== "string")
|
|
201
|
+
return { present: true, owner: undefined }; // NULL 列 = 行在但无属主
|
|
202
|
+
const taskId = row.owner_task_id;
|
|
203
|
+
// 键**缺席**而不是 present-as-undefined:core 的比较器把「缺 taskId」与「显式 undefined」当同一个值,
|
|
204
|
+
// 但调用方(和本仓的 toEqual 钉)看得见键在不在,落库的 NULL 只能还原成缺席形。
|
|
205
|
+
return { present: true, owner: typeof taskId === "string" ? { sessionId, taskId } : { sessionId } };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* #119 —— 出处的**读面**。未知 ref 与「存了但无属主」两种情况都答 `undefined`:读面对二者一视同仁
|
|
209
|
+
* (fail-closed,谁都没被授权),所以这里也不必把它们分开报。
|
|
210
|
+
*/
|
|
211
|
+
async ownerOf(ref) {
|
|
212
|
+
if (this.isUnsafeRef(ref))
|
|
213
|
+
return undefined; // 与 get 同尺:写面拒过的 ref 必不在库,不把驱动层错误漏出去
|
|
214
|
+
return (await this.readOwner(ref)).owner;
|
|
106
215
|
}
|
|
107
216
|
async get(ref, opts = {}) {
|
|
108
217
|
// D1 读面半条(RB-266「the read face degrades instead」):unsafe ref 在读面**降级**返回 undefined
|
|
@@ -147,11 +256,18 @@ export class SqlToolResultStore {
|
|
|
147
256
|
* data-layer protection that matters (a crafted id can't widen the prefix across tenants).
|
|
148
257
|
*/
|
|
149
258
|
async deleteBySession(sessionId) {
|
|
150
|
-
// Escape the WHOLE literal prefix `tr_<sessionId
|
|
151
|
-
//
|
|
152
|
-
|
|
259
|
+
// Escape the WHOLE literal prefix `tr_<sessionId><sep>` (incl. the literal underscore in `tr_` and, on the
|
|
260
|
+
// legacy arm, the separator underscore) so the only LIKE wildcard is the trailing `%` — a `_` left
|
|
261
|
+
// un-escaped is a single-char wildcard that could over-match across tenants.
|
|
262
|
+
//
|
|
263
|
+
// 🔴 #119(core 5.26.0):**两个前缀**,因为铸法在 5.26.0 换了分隔符。5.25.0 及以前铸 `tr_<sid>_<callId>`,
|
|
264
|
+
// 5.26.0 起铸 `tr_<sid>~<callId>~<contentSeg>`(单射 ref)。只留旧前缀 = 升级后一行也命不中,§0.5 会话
|
|
265
|
+
// 删除**静默清零**;只留新前缀 = 升级前写下的存量行永远删不掉。删除面漏删比多跑一次 LIKE 贵得多,
|
|
266
|
+
// 所以两条都发,而且都保持「整段字面量转义 + 只有末尾 % 是通配」的老纪律。
|
|
267
|
+
const legacyPrefix = `${escapeLike(`tr_${sessionId}_`)}%`;
|
|
268
|
+
const modernPrefix = `${escapeLike(`tr_${sessionId}~`)}%`;
|
|
153
269
|
// Explicit `ESCAPE` clause — see the file-header dialect-delta note for why each dialect spells it as it does.
|
|
154
|
-
const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE ref LIKE ? ESCAPE '\\\\'", "DELETE FROM tool_result WHERE ref LIKE $1 ESCAPE '\\'"), [
|
|
270
|
+
const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE ref LIKE ? ESCAPE '\\\\' OR ref LIKE ? ESCAPE '\\\\'", "DELETE FROM tool_result WHERE ref LIKE $1 ESCAPE '\\' OR ref LIKE $2 ESCAPE '\\'"), [legacyPrefix, modernPrefix]);
|
|
155
271
|
return affected;
|
|
156
272
|
}
|
|
157
273
|
}
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ── 立案:装配层四条缺口([2176] 我方认领,2026-08-01 调研,**尚未实施**)────────────────────
|
|
17
17
|
*
|
|
18
|
-
* ① **`opts`
|
|
18
|
+
* ① **`opts` 仍有 brave 腿不消费**(只剩 `brave` 的 switch 分支不传 `opts`;`searxng` 腿已随 ② 的
|
|
19
|
+
* 还债改动接住 `opts` 并透传给 core 的 adapter,由它把 `allowedDomains` 原生下推成 `site:` 前缀。
|
|
20
|
+
* 立案时的原话是「`opts` 只有 tavily 腿在消费(`brave`/`searxng` 都不传)」,② 落地后已部分还清)。
|
|
19
21
|
* 定性:**优化缺失,不是正确性缺口** —— 上面那句「core 再执行一次 FLOOR」经亲验属实
|
|
20
22
|
* (core `dist/tools/web.js` 的 `webSearchResultAllowed(url, allowed, blocked)`,按 hostname
|
|
21
23
|
* 逐条过滤 blocked/allowed)。所以模型请求的域限制**不会**被静默丢弃。
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*
|
|
16
16
|
* ── 立案:装配层四条缺口([2176] 我方认领,2026-08-01 调研,**尚未实施**)────────────────────
|
|
17
17
|
*
|
|
18
|
-
* ① **`opts`
|
|
18
|
+
* ① **`opts` 仍有 brave 腿不消费**(只剩 `brave` 的 switch 分支不传 `opts`;`searxng` 腿已随 ② 的
|
|
19
|
+
* 还债改动接住 `opts` 并透传给 core 的 adapter,由它把 `allowedDomains` 原生下推成 `site:` 前缀。
|
|
20
|
+
* 立案时的原话是「`opts` 只有 tavily 腿在消费(`brave`/`searxng` 都不传)」,② 落地后已部分还清)。
|
|
19
21
|
* 定性:**优化缺失,不是正确性缺口** —— 上面那句「core 再执行一次 FLOOR」经亲验属实
|
|
20
22
|
* (core `dist/tools/web.js` 的 `webSearchResultAllowed(url, allowed, blocked)`,按 hostname
|
|
21
23
|
* 逐条过滤 blocked/allowed)。所以模型请求的域限制**不会**被静默丢弃。
|
package/dist/rules-consent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ImportPreview, type ImportResult, type ImportedSettingsLayer, type PersistedAllowRule, type RemoveResult, type RuleScope } from "@sema-agent/core";
|
|
1
|
+
import { type RuleOwner, type ImportPreview, type ImportResult, type ImportedSettingsLayer, type PersistedAllowRule, type RemoveResult, type RuleScope } from "@sema-agent/core";
|
|
2
2
|
import type { PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
3
|
import { type RuleImportTicket, type RuleTicketRedeemResult } from "./plugins/permission-rule-store-sql.js";
|
|
4
4
|
/**
|
|
@@ -47,6 +47,16 @@ export interface PermissionRuleStoreBundle extends RuleConsentStores {
|
|
|
47
47
|
/** #203 §3:库里已有几只规则桶(一 owner 一桶)。boot 期休眠行审计的**唯一**依赖;
|
|
48
48
|
* 为什么数桶不数规则、以及读失败必须响亮,见 `boot/permission-rules-audit.ts` 与两个实现处的注。 */
|
|
49
49
|
countBuckets(): Promise<number>;
|
|
50
|
+
/**
|
|
51
|
+
* A-010.17 保留期腿:清掉**可证已死**的导入票与孤儿 pending 审批记录,返回删掉的总行数。
|
|
52
|
+
* 谓词与「为什么不需要旋钮」逐字见 `plugins/permission-rule-store-sql.ts` 的同名成员。
|
|
53
|
+
*
|
|
54
|
+
* 🔴 **可选**,而缺席是**如实登记**过的:File 车道(`permission-rule-store-file.ts`)的审批/票两面是
|
|
55
|
+
* **追加日志**,删一行等于重写整个日志 —— 那是一次压实(compaction)设计,与 SQL 的一条 DELETE 不是
|
|
56
|
+
* 同一件事,不在本条射程内。缺席的实际后果有界:File 车道 = 单用户本地形,两只日志的增长由**一个人**
|
|
57
|
+
* 的导入次数封顶,不是多租户共享库那种无界增长。要做压实时在这里补实现,消费点(reaper)零改动。
|
|
58
|
+
*/
|
|
59
|
+
reapExpired?(nowMs: number): Promise<number>;
|
|
50
60
|
}
|
|
51
61
|
/** 导入票的存活窗。人从「看到预览」到「按下确认」是一次交互,不是一段会话——十分钟宽到不会误伤,
|
|
52
62
|
* 窄到一张泄漏的票不会常驻。运维旋钮暂不开(没有部署形要求它可调;要开时走 config.ts 的既有姿势)。 */
|
|
@@ -147,6 +157,20 @@ export interface PersistedRuleListing {
|
|
|
147
157
|
rows: PersistedRuleWireRow[];
|
|
148
158
|
rev: number;
|
|
149
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* 撤销面两口寻址的**桶**(#203 收官件,core 5.24.0 提货批 [3427])。
|
|
162
|
+
*
|
|
163
|
+
* 裸串 = principal 简写(既有调用方逐字不变);结构形 {@link RuleOwner} 多出 `{kind:"local-owner"}`
|
|
164
|
+
* —— 身份缺席的单机形自己那只桶。这不是 server 发明的第二种寻址:core 的
|
|
165
|
+
* `PermissionRuleStoreProvider.forLocalOwner()`(design/182 §4.5)一直就有这只桶,缺的是**撤销入口
|
|
166
|
+
* 叫不出它的名字** —— 我方 [3399]① 请托、core 5.24.0 把 `removePersistedRule` 的 `principal` 入参扩成
|
|
167
|
+
* `string | RuleOwner` 兑现(d.ts 那段注逐字写着「downstream request, 2026-08-10」)。
|
|
168
|
+
*
|
|
169
|
+
* 🔴 **绝不用哨兵串**表示身份缺席(core 的 `RuleOwner` 头注就是这条纪律的出处):`forPrincipal("")` /
|
|
170
|
+
* `forPrincipal("local-owner")` 都会让一个**编出来的名字**流进身份面,而身份面上编的名字与真名在下游
|
|
171
|
+
* 不可分。判别式联合是唯一诚实的形。
|
|
172
|
+
*/
|
|
173
|
+
export type RuleBucketRef = string | RuleOwner;
|
|
150
174
|
export interface RuleConsentLane {
|
|
151
175
|
/** 卡道兑付口:一次 ask 决议携规则确认 ⇒ prepare→confirm→redeem 一气呵成。 */
|
|
152
176
|
persistCardRule(input: {
|
|
@@ -165,13 +189,15 @@ export interface RuleConsentLane {
|
|
|
165
189
|
/** 导入道:原子消费票 → confirm → redeemRuleBatch。 */
|
|
166
190
|
redeemImport(principal: string, ticket: string): Promise<RuleImportRedeemed>;
|
|
167
191
|
/**
|
|
168
|
-
*
|
|
192
|
+
* 撤销面读半场:列出一只桶名下**活着**的规则(墓碑已折算)。
|
|
169
193
|
*
|
|
170
194
|
* 排序 = (scope, rule) 字典序,**确定性**:分页游标是 keyset 形,而 keyset 的全部前提就是「同一份
|
|
171
195
|
* 数据每次以同一个顺序出现」。core 的 `list()` 不承诺顺序(SQL 形按写入顺序、File 形按文件内顺序),
|
|
172
196
|
* 所以序在这里定,不在店里。
|
|
197
|
+
*
|
|
198
|
+
* 入参裸串 = principal 简写(既有调用方逐字不变);{@link RuleBucketRef} 的结构形另开 local-owner 桶。
|
|
173
199
|
*/
|
|
174
|
-
listRules(
|
|
200
|
+
listRules(bucket: RuleBucketRef): Promise<PersistedRuleListing>;
|
|
175
201
|
/**
|
|
176
202
|
* 撤销面写半场。**恒经 core `removePersistedRule`**(design/203 §2 裁定)——它产墓碑,而墓碑是
|
|
177
203
|
* `screenRuleSyncState` 的筛子保证「别的副本不把这条规则回灌回来」的唯一凭据。直接删 SQL 行/文件行
|
|
@@ -180,9 +206,12 @@ export interface RuleConsentLane {
|
|
|
180
206
|
* 返回值逐字是 core 的 `RemoveResult` 三态(removed / no-op / failed),**不塌**:
|
|
181
207
|
* `removed.stillLive:true` 意思是「墓碑落了,但本次调用期间又落了一次新的批准,规则按 add-wins
|
|
182
208
|
* 仍然活着」——那是一个**有名字的真结果**,不是异常,更不是「撤销完成」。
|
|
209
|
+
*
|
|
210
|
+
* `principal` 裸串 = principal 简写(既有调用方逐字不变);{@link RuleBucketRef} 的结构形让
|
|
211
|
+
* local-owner 桶**真可撤**(core 5.24.0 起 `removePersistedRule` 的入参 union 扩,#203 收官件)。
|
|
183
212
|
*/
|
|
184
213
|
removeRule(input: {
|
|
185
|
-
principal:
|
|
214
|
+
principal: RuleBucketRef;
|
|
186
215
|
rule: string;
|
|
187
216
|
scope: RuleScope;
|
|
188
217
|
}): Promise<RemoveResult>;
|
package/dist/rules-consent.js
CHANGED
|
@@ -87,6 +87,42 @@ export function parseRuleScope(text) {
|
|
|
87
87
|
// (空前缀包含一切)——一条项目内规则就此悄悄升成全局规则(cc-import 那侧同判据,同一条 codex 发现)。
|
|
88
88
|
return root === "" ? undefined : { kind: "project", root };
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* 桶引用 → 具体的规则店。
|
|
92
|
+
*
|
|
93
|
+
* 🔴 **穷举判别,未知形一律抛**(CLAUDE.md 词表闭集律 + codex 对抗复审 R3-[medium],验真后改)。
|
|
94
|
+
* 第一稿写的是 `kind === "principal" ? 甲 : 乙` 的二分:TS 那侧是穷举的,**运行期不是** —— 本车道是
|
|
95
|
+
* 一个**导出**的束,调用方可以是任何一层(HTTP 路由今天只递裸串,但这条口正是为了将来递结构形才开的)。
|
|
96
|
+
* 于是 `{kind:"principle"}`(拼错)或 `{}` 会静默落进 local-owner 那支 = 一次**跨桶读**;更坏的是
|
|
97
|
+
* 同一个坏值递给 core 的 `removePersistedRule` 时它按 `kind === "local-owner"` 反着判,落到
|
|
98
|
+
* `forPrincipal(undefined)`(空店)—— 列举和撤销**指向两只不同的桶**,而两边都不报错。
|
|
99
|
+
* 身份面上「不认识的形」只有一个正确答案:响亮拒。
|
|
100
|
+
*
|
|
101
|
+
* 🔴 local-owner 桶而 provider 没有 `forLocalOwner` ⇒ 同样**抛**,不静默回落到别的桶。回落到
|
|
102
|
+
* `forPrincipal(…)` 会把「读/删身份缺席桶」变成「读/删某个 principal 的桶」——那是一次跨桶的读或写,
|
|
103
|
+
* 是本车道最不该出的错。报文与 core 的 `removePersistedRule` 同支同措辞(那侧回 `failed` 带
|
|
104
|
+
* `forLocalOwner` 字样),两侧一起读日志时对得上。
|
|
105
|
+
*/
|
|
106
|
+
function storeForBucket(provider, ref) {
|
|
107
|
+
// 裸串 = principal 简写(core `removePersistedRule` 的同款归一,两侧对同一个输入判同一只桶)。
|
|
108
|
+
if (typeof ref === "string")
|
|
109
|
+
return provider.forPrincipal(ref);
|
|
110
|
+
// `ref.kind` 在类型上是闭集;`switch` 的 default 是**运行期**的那道门(不是死码)。
|
|
111
|
+
switch (ref.kind) {
|
|
112
|
+
case "principal":
|
|
113
|
+
return provider.forPrincipal(ref.principal);
|
|
114
|
+
case "local-owner":
|
|
115
|
+
if (provider.forLocalOwner === undefined) {
|
|
116
|
+
throw new Error("this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be listed or removed through it");
|
|
117
|
+
}
|
|
118
|
+
return provider.forLocalOwner();
|
|
119
|
+
default: {
|
|
120
|
+
// core 给 `RuleOwner` 加员时这里是**编译**红(`never` 赋值失败),不是静默继承某一支。
|
|
121
|
+
const unknown = ref;
|
|
122
|
+
throw new Error(`unrecognised permission-rule bucket reference (${JSON.stringify(unknown)}) — refusing to guess which bucket it names`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
90
126
|
export function createRuleConsentLane(stores, opts) {
|
|
91
127
|
const deps = { provider: stores.provider, approvals: stores.approvals };
|
|
92
128
|
const ticketTtlMs = opts?.ticketTtlMs ?? RULE_IMPORT_TICKET_TTL_MS;
|
|
@@ -222,10 +258,10 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
222
258
|
return await indeterminate(err instanceof Error ? err.message : String(err));
|
|
223
259
|
}
|
|
224
260
|
},
|
|
225
|
-
async listRules(
|
|
261
|
+
async listRules(bucket) {
|
|
226
262
|
// `list()` 已经把墓碑折算掉(`applyTombstones`)⇒ 这里拿到的就是**活着**的规则,治理面看到的
|
|
227
263
|
// 与引擎放行时看到的是同一份。空桶 = 空表 + rev 0,不是错误(「这个人没有规则」是合法状态)。
|
|
228
|
-
const stored = await stores.provider
|
|
264
|
+
const stored = await storeForBucket(stores.provider, bucket).list();
|
|
229
265
|
const rows = stored.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds }));
|
|
230
266
|
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));
|
|
231
267
|
return { rows, rev: stored.rev };
|
|
@@ -233,6 +269,11 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
233
269
|
async removeRule(input) {
|
|
234
270
|
// 🔴 一行也不自己写:OCC 重试、墓碑铸造、`stillLive` 的读回全在 core 那条原语里,而它的语义
|
|
235
271
|
// (add-wins、observed-remove、失败必须由读回确认「什么都没写」)是 core 的单一属主。
|
|
272
|
+
// 桶引用**原样**递下去(裸串走 core 的 principal 简写臂,结构形走 owner 臂)——我方不在这里
|
|
273
|
+
// 归一成 owner 对象:core 5.24.0 的 `removePersistedRule` 自己就做那次归一(`typeof === "string"`),
|
|
274
|
+
// 多归一一次等于把上游的简写语义抄一份到下游,抄件会漂。local-owner 而 provider 无 `forLocalOwner`
|
|
275
|
+
// 的那支也归 core:它回的是 `failed` + 报文含 `forLocalOwner`(C13 判据),比抛错更适合这条
|
|
276
|
+
// 三态返回的口。
|
|
236
277
|
return await removePersistedRule({ rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
|
237
278
|
},
|
|
238
279
|
};
|
package/dist/run-local.js
CHANGED
|
@@ -582,6 +582,11 @@ export async function runLocal(argv, deps = {}) {
|
|
|
582
582
|
*/
|
|
583
583
|
const sharedRunnerDeps = createSharedRunnerDeps({
|
|
584
584
|
config,
|
|
585
|
+
// 交接件⑤:commit 尾注署名座进**共享基座** ⇒ 主 runner 与 subRunner 同源。
|
|
586
|
+
// [931]① clay 拍:run-local = Sema 品牌本地形态,commit 尾注接 Sema 署名(core 1.300 缺省已翻转不署)。
|
|
587
|
+
// 修前它只写在下面主 runner 的差异键里 ⇒ **委派出去的子代提交不带署名**,而它提交进的是同一个仓、
|
|
588
|
+
// 代表的是同一个部署。那不是一次有理由的分歧(差异键注释块逐条列了理由,唯独没提它),是漏配。
|
|
589
|
+
hands: { commitCoAuthor: "Sema <noreply@vivi-ai.com>" },
|
|
585
590
|
brain,
|
|
586
591
|
pricing,
|
|
587
592
|
tracer: createTracer(metrics),
|
|
@@ -601,8 +606,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
601
606
|
const runnerDeps = {
|
|
602
607
|
...sharedRunnerDeps,
|
|
603
608
|
// ── 以下为主 runner 的差异键(不在共享基座;逐个有因)──────────────────────────────────────
|
|
604
|
-
//
|
|
605
|
-
hands: { commitCoAuthor: "Sema <noreply@vivi-ai.com>" },
|
|
609
|
+
// (`hands` 已随共享基座展开 —— 交接件⑤;这里手写同名键会 override 展开、把 subRunner 重新甩开。)
|
|
606
610
|
sessionStore,
|
|
607
611
|
...(memoryEngine ? { memoryBackend: memoryEngine.backend, memoryEngineDir: memoryEngine.root } : {}),
|
|
608
612
|
checkpointStore: fileBackend.checkpointStore,
|
|
@@ -93,6 +93,39 @@ export declare function compileCommandPolicy(rules: CommandRule[] | undefined):
|
|
|
93
93
|
* - `auto` / `undefined` → `{}` (no extra tightening; still subject to the deployment baseline + commandPolicy).
|
|
94
94
|
*/
|
|
95
95
|
export declare function autonomyOverrides(autonomy: Autonomy | undefined): Partial<TaskSpec>;
|
|
96
|
+
/**
|
|
97
|
+
* #220:**部署级**判据 —— 本部署的治理层自己是否把 shellGate 抬到 `"always"`。
|
|
98
|
+
*
|
|
99
|
+
* 用途 = 持久门(park 行)的出身归因。活卡腿判「这只 ask 是治理层产的」靠 ALS 标记表(见本文件
|
|
100
|
+
* `createGovernanceAskMarkingPolicy` / `createGovernanceShellGateMarkPolicy`),而 park 腿天然跨副本、
|
|
101
|
+
* 跨重启,进程内的表在那里结构上够不着;行上唯一的取证格是 core 写的
|
|
102
|
+
* `gate.riskDescriptor.shellGateDoctrine`。
|
|
103
|
+
*
|
|
104
|
+
* 🔴 **为什么单看行上那一格不够**:有效档 `"always"` **不止治理层一个产地** —— `resolve-spec` 的 SUP 路由
|
|
105
|
+
* 姿态(`supPostureOverrides`)在 governance **之后**叠,同样产 `"always"`(本文件 `manualModeShellGate`
|
|
106
|
+
* 合成段的注释里已如实记着这条时序)。只看行 ⇒ 一条 SUP 路由出来的门会被谎报成「运维治理层强制」,
|
|
107
|
+
* 而那恰是这个信号要回答的那个问题。所以归因取**合取**:行上是 `always` **∧** 本部署的治理层本来就
|
|
108
|
+
* 要求 `always`(后者成立时,治理层就是一个真成因,SUP 是否也抬过不改变这句话的真假)。
|
|
109
|
+
*
|
|
110
|
+
* 判据逐字对着两个产地(与 `applyRuntimeGovernance` 里 `overrides.shellGate` 的取值同源):
|
|
111
|
+
* `autonomyOverrides(autonomy).shellGate`(`AUTONOMY=ask`)与 `MANUAL_MODE_SHELL_GATE` 旋钮,取「有一个
|
|
112
|
+
* 是 always」。`commandPolicy` 不入判据:它产的是 `toolPolicy` 的 ask,不抬 shellGate 档,与本格无关。
|
|
113
|
+
*
|
|
114
|
+
* ⚠️ **已知残留(codex 对抗复审 2026-08-11 逮到,如实登记而不是掩盖)**:本判据读的是**当下**的
|
|
115
|
+
* config,而 `config.autonomy` 是**热改**的(`config-center/apply-effective.ts` 就地写 `config.autonomy`)。
|
|
116
|
+
* 于是失真是**双向**的,不是我原先写的「只往缺席方向」:
|
|
117
|
+
* · 缺席向(常见):mint 之后运维把 `AUTONOMY=ask` 撤了 ⇒ 老 park 行归不出治理出身,少标一个;
|
|
118
|
+
* · **在场向**(窄):一条**纯 SUP 路由**产的门(`ROUTER_ENABLED` 开 ∧ 路由判 supervisor ∧ 当时治理层
|
|
119
|
+
* 并不要求 always),之后运维把 autonomy 热改成 `ask`,那条老行会被标成治理出身 —— 五个前提要**同时**
|
|
120
|
+
* 成立,且该行仍未决。
|
|
121
|
+
* 根治要**在 mint 那一刻把出身写进行里**(core 的 `CheckpointGate` 属主面,不是本仓能单方面做的);
|
|
122
|
+
* 在那之前这一位的定位是**分诊提示**,不是裁决输入 —— 它不参与任何门/CAS/resume 判定,消费端也只拿它
|
|
123
|
+
* 渲染徽标。登记在此,不加特征化测试(把已知残留钉成契约是另一种病)。
|
|
124
|
+
*/
|
|
125
|
+
export declare function governanceMandatesShellGateAlways(governance: {
|
|
126
|
+
autonomy?: Autonomy;
|
|
127
|
+
manualModeShellGate?: "always" | "classify";
|
|
128
|
+
}): boolean;
|
|
96
129
|
/**
|
|
97
130
|
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
|
|
98
131
|
* `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
@@ -219,6 +219,38 @@ export function autonomyOverrides(autonomy) {
|
|
|
219
219
|
return {};
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* #220:**部署级**判据 —— 本部署的治理层自己是否把 shellGate 抬到 `"always"`。
|
|
224
|
+
*
|
|
225
|
+
* 用途 = 持久门(park 行)的出身归因。活卡腿判「这只 ask 是治理层产的」靠 ALS 标记表(见本文件
|
|
226
|
+
* `createGovernanceAskMarkingPolicy` / `createGovernanceShellGateMarkPolicy`),而 park 腿天然跨副本、
|
|
227
|
+
* 跨重启,进程内的表在那里结构上够不着;行上唯一的取证格是 core 写的
|
|
228
|
+
* `gate.riskDescriptor.shellGateDoctrine`。
|
|
229
|
+
*
|
|
230
|
+
* 🔴 **为什么单看行上那一格不够**:有效档 `"always"` **不止治理层一个产地** —— `resolve-spec` 的 SUP 路由
|
|
231
|
+
* 姿态(`supPostureOverrides`)在 governance **之后**叠,同样产 `"always"`(本文件 `manualModeShellGate`
|
|
232
|
+
* 合成段的注释里已如实记着这条时序)。只看行 ⇒ 一条 SUP 路由出来的门会被谎报成「运维治理层强制」,
|
|
233
|
+
* 而那恰是这个信号要回答的那个问题。所以归因取**合取**:行上是 `always` **∧** 本部署的治理层本来就
|
|
234
|
+
* 要求 `always`(后者成立时,治理层就是一个真成因,SUP 是否也抬过不改变这句话的真假)。
|
|
235
|
+
*
|
|
236
|
+
* 判据逐字对着两个产地(与 `applyRuntimeGovernance` 里 `overrides.shellGate` 的取值同源):
|
|
237
|
+
* `autonomyOverrides(autonomy).shellGate`(`AUTONOMY=ask`)与 `MANUAL_MODE_SHELL_GATE` 旋钮,取「有一个
|
|
238
|
+
* 是 always」。`commandPolicy` 不入判据:它产的是 `toolPolicy` 的 ask,不抬 shellGate 档,与本格无关。
|
|
239
|
+
*
|
|
240
|
+
* ⚠️ **已知残留(codex 对抗复审 2026-08-11 逮到,如实登记而不是掩盖)**:本判据读的是**当下**的
|
|
241
|
+
* config,而 `config.autonomy` 是**热改**的(`config-center/apply-effective.ts` 就地写 `config.autonomy`)。
|
|
242
|
+
* 于是失真是**双向**的,不是我原先写的「只往缺席方向」:
|
|
243
|
+
* · 缺席向(常见):mint 之后运维把 `AUTONOMY=ask` 撤了 ⇒ 老 park 行归不出治理出身,少标一个;
|
|
244
|
+
* · **在场向**(窄):一条**纯 SUP 路由**产的门(`ROUTER_ENABLED` 开 ∧ 路由判 supervisor ∧ 当时治理层
|
|
245
|
+
* 并不要求 always),之后运维把 autonomy 热改成 `ask`,那条老行会被标成治理出身 —— 五个前提要**同时**
|
|
246
|
+
* 成立,且该行仍未决。
|
|
247
|
+
* 根治要**在 mint 那一刻把出身写进行里**(core 的 `CheckpointGate` 属主面,不是本仓能单方面做的);
|
|
248
|
+
* 在那之前这一位的定位是**分诊提示**,不是裁决输入 —— 它不参与任何门/CAS/resume 判定,消费端也只拿它
|
|
249
|
+
* 渲染徽标。登记在此,不加特征化测试(把已知残留钉成契约是另一种病)。
|
|
250
|
+
*/
|
|
251
|
+
export function governanceMandatesShellGateAlways(governance) {
|
|
252
|
+
return autonomyOverrides(governance.autonomy).shellGate === "always" || governance.manualModeShellGate === "always";
|
|
253
|
+
}
|
|
222
254
|
/**
|
|
223
255
|
* [2942]/[2943] `governanceForced` 的**写侧** —— 把内层策略(治理层自己合成的那些)产的 `ask` 记进标记表。
|
|
224
256
|
*
|
package/dist/security.js
CHANGED
|
@@ -171,7 +171,9 @@ export function createAuthorizer(config, sessionStore) {
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
// Enforce ownership whenever the session HAS an owner — including against callers that present
|
|
174
|
-
// no principal (audit B
|
|
174
|
+
// no principal (audit B — the fix IS this guard, right here in `createAuthorizer`; no line-number
|
|
175
|
+
// anchor on purpose, the old `security.ts:77` pointer had drifted onto an unrelated JSDoc):
|
|
176
|
+
// with the old `principal &&` guard, a caller that
|
|
175
177
|
// omitted the header under requirePrincipal=false could attach to ANY owned session by id.
|
|
176
178
|
// Anonymous callers may still attach to anonymous (owner=null) sessions, so single-tenant dev
|
|
177
179
|
// (no principals anywhere) is unaffected; flipping the requirePrincipal default is NOT needed.
|
package/dist/tool-approval.d.ts
CHANGED
|
@@ -61,6 +61,38 @@ export interface ToolApprovalFrame {
|
|
|
61
61
|
* `governance-ask-marks.ts` 顶注。
|
|
62
62
|
*/
|
|
63
63
|
governanceForced?: true;
|
|
64
|
+
/**
|
|
65
|
+
* #144(core 5.25.0,[3438] 接力契约 / [3443] 主件;**ADDITIVE**,`"tool_approval"` only)——
|
|
66
|
+
* 这只 ask **命中了**调用方的一条持久 allow 规则,而那条规则**没能清掉它**。值 = 被越级的那条规则
|
|
67
|
+
* **原文**(core `AskRequest.persistedRuleShadowed`,四个 mint 点全带;core 侧已过 `inlineUntrusted`)。
|
|
68
|
+
*
|
|
69
|
+
* 🔴 **它存在的理由是一次真实的用户面回归**:core 5.25.0 收窄了消音边界(裁定逐字:「Allow rules
|
|
70
|
+
* silence the CLASSIFIER's questions, never a MANDATED one」)—— `shellGate:"always"` 的部署、工具自带
|
|
71
|
+
* egress/irreversible mark、以及既有的 governance 三类门下,用户此前被规则消掉的 ask **重新出现**。
|
|
72
|
+
* 没有这个键,人看到的是「我明明点过『不再询问』,它怎么又问」,唯一合理的结论是「我的规则坏了/没存上」。
|
|
73
|
+
* 有了它,壳能渲「你的规则仍在,只是这次调用被(运维令 / 工具本性)要求逐次确认」。
|
|
74
|
+
*
|
|
75
|
+
* 🔴 **缺席 ≠「你没有规则」**:本键只在「有规则命中 ∧ 规则清不掉这只 ask」时在场。绝大多数 ask
|
|
76
|
+
* 压根没有规则命中(缺席),而**命中且清掉了**的那些根本不会变成 ask(它们被消音了,没有卡)。
|
|
77
|
+
* 消费端禁把缺席读成「你在这条命令上没有规则」。
|
|
78
|
+
*
|
|
79
|
+
* 与 {@link ToolApprovalFrame.governanceForced} **刻意分列、不合并**:那个键回答「门是谁下的」
|
|
80
|
+
* (运维治理层),本键回答「你那条规则怎么了」。governance 只是不可消音的三个来源之一,另两个
|
|
81
|
+
* (doctrine `always` / 工具自带 mark)不打 governance 标 —— 合并会让后两类的 ask 要么谎报治理出身、
|
|
82
|
+
* 要么丢掉规则解释。出身的完整推法见 [3438]:`gate.safetyAxis` + `riskDescriptor.shellGateDoctrine` +
|
|
83
|
+
* `realApproval.origin` 组合读,server 不新铸出身键。
|
|
84
|
+
*
|
|
85
|
+
* 值是**用户内容族**(规则原文是人写的文本)⇒ 与 `message`/`sourceAgentName` 同待遇:`redactSecrets`
|
|
86
|
+
* 后上帧,UNTRUSTED-for-display。空串**不铸键**(core 只在真有命中时带它,空串是坏值不是「空规则」)。
|
|
87
|
+
*
|
|
88
|
+
* 耐久路(park 行)的对偶是 `gate.riskDescriptor.shadowedRule` —— 我方对 `riskDescriptor` 是**整体透传**
|
|
89
|
+
* (SQL 双生落整只 JSON 列、`listPending` 整只回读、两条 durable 读面整行上 wire),所以那一路**键集**零施工;
|
|
90
|
+
* 唯一的施工是**内容**:那两条读面(`GET /v1/approvals` + `/v1/approvals/stream`)在读边界对这一格补
|
|
91
|
+
* `redactSecrets`(core 只中和不脱敏,而运维队列是跨租户可见的那一条 —— 见
|
|
92
|
+
* `http/routes/approvals-assistant.ts` 的 `redactPendingDisclosures` 顶注)。
|
|
93
|
+
* 两路的钉见 `test/approval-shadowed-rule-wire.test.ts` 与 db-integration 的「#209 件4」格。
|
|
94
|
+
*/
|
|
95
|
+
persistedRuleShadowed?: string;
|
|
64
96
|
/**
|
|
65
97
|
* #154 车二(core 5.18.0 design/179):`"tool_approval"` only —— 引擎为这次 ask 铸的**规则候选**
|
|
66
98
|
* (`AskRequest.ruleSuggestions`,逐字透传:闭词表 `match` + 引擎铸的文本,server 不重铸不重排)。
|