@sema-agent/server 7.13.0 → 7.15.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/MIGRATION.md +16 -1
- package/USAGE.md +21 -0
- package/dist/approval-ask-machine.d.ts +10 -0
- package/dist/approval-ask-machine.js +10 -0
- package/dist/boot/coordinators.js +2 -1
- package/dist/boot/memory-boundary.d.ts +90 -0
- package/dist/boot/memory-boundary.js +118 -0
- package/dist/boot/resolve-spec.js +31 -0
- package/dist/boot/runner-deps.js +19 -0
- package/dist/boot/stores.js +101 -10
- package/dist/capabilities/memory-notice.d.ts +87 -0
- package/dist/capabilities/memory-notice.js +110 -0
- package/dist/config-types.d.ts +86 -9
- package/dist/config.d.ts +1 -1
- package/dist/config.js +111 -1
- package/dist/governance-ask-marks.js +2 -1
- package/dist/http/routes/capabilities.js +22 -4
- package/dist/http/routes/runs.js +23 -2
- package/dist/http/routes/trace-usage.js +49 -19
- package/dist/http/server.d.ts +9 -1
- package/dist/http/server.js +9 -1
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +45 -0
- package/dist/observability/metrics.js +4 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +2 -1
- package/dist/plugins/approval-ask-store-sql.js +2 -1
- package/dist/plugins/memory-embedder.d.ts +44 -0
- package/dist/plugins/memory-embedder.js +173 -0
- package/dist/plugins/store-backend.d.ts +3 -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/security.js +3 -1
- package/dist/tool-approval.d.ts +1 -0
- package/dist/tool-approval.js +88 -6
- package/package.json +3 -3
|
@@ -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/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
package/dist/tool-approval.js
CHANGED
|
@@ -47,7 +47,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
47
47
|
import { uuidv7, MAX_RULE_TEXT_CHARS } from "@sema-agent/core";
|
|
48
48
|
import { redactDeep, redactSecrets } from "./trace/redact.js";
|
|
49
49
|
import { createLogger } from "./observability/logger.js";
|
|
50
|
-
import { deriveAskId, deriveBatchId } from "./approval-ask-machine.js";
|
|
50
|
+
import { deriveAskId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
|
|
51
51
|
import { ApprovalCardEnvelopeSchema, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, } from "./approval-card.js";
|
|
52
52
|
import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
|
|
53
53
|
/** #151 车2:本模块自有的日志出口——同 config-provider.ts/runtime-caps-resolver.ts 先例(协调器不走
|
|
@@ -159,7 +159,27 @@ export function parseToolApprovalResponse(body) {
|
|
|
159
159
|
}
|
|
160
160
|
persistRule = rule;
|
|
161
161
|
}
|
|
162
|
-
|
|
162
|
+
// #229(设计稿 233 稿B):回决**理由**。与 durable 腿(`http/routes/runs.ts` 的 `AskDecisionBodySchema.note`)
|
|
163
|
+
// **同词同源同上限**({@link MAX_DECISION_NOTE_CHARS}),落的也是同一列 `decision_note` —— 不造第三口径。
|
|
164
|
+
// 🔴 **任何 decision 都可带**(deny 也算),与 durable 腿的无条件记账形对齐:「为什么拒」正是审计面上
|
|
165
|
+
// 最值钱的那一条,做成 allow-only 等于把它扔掉。⚠️ `allow_session` 是 wire 上的三值之一,落到行上是
|
|
166
|
+
// `approve`(三值映二值)—— 它的 note 因此记在**那条 approve 行**上,不另开一行、也不丢。
|
|
167
|
+
// 坏形(非串 / 超上限)是**响亮 400**,不静默截断:一条被悄悄砍半的审计理由比没有理由更坏。
|
|
168
|
+
const rawNote = body.note;
|
|
169
|
+
let note;
|
|
170
|
+
if (rawNote !== undefined) {
|
|
171
|
+
if (typeof rawNote !== "string" || rawNote.length > MAX_DECISION_NOTE_CHARS) {
|
|
172
|
+
return { ok: false, error: `note must be a string of at most ${MAX_DECISION_NOTE_CHARS} characters` };
|
|
173
|
+
}
|
|
174
|
+
note = rawNote;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
value: d,
|
|
179
|
+
...(d !== "deny" && u !== undefined ? { updatedInput: u } : {}),
|
|
180
|
+
...(persistRule !== undefined ? { persistRule } : {}),
|
|
181
|
+
...(note !== undefined ? { note } : {}),
|
|
182
|
+
};
|
|
163
183
|
}
|
|
164
184
|
return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
|
|
165
185
|
}
|
|
@@ -782,6 +802,25 @@ export class ToolApprovalCoordinator {
|
|
|
782
802
|
// ③ 引擎真铸了候选(`req.ruleSuggestions` 非空)—— 命令不可匹配(复合/重定向/替换)时 core 交空数组;
|
|
783
803
|
// ④ 命令原字节读得出(回决时**引擎**要拿它重铸候选;读不出就没有可兑付的路,投候选等于骗人)。
|
|
784
804
|
// 读 `req.args.command` 走窄读:`args` 是 `unknown`,禁裸 as-cast(宪法 [2704])。
|
|
805
|
+
// 🔴 **core 5.27.0 起引擎自己也收窄了产源**(#231 提货,[3612]):mandated(`shellGate:"always"` /
|
|
806
|
+
// 工具自带 egress / irreversibility `always|maybe` 标记;⚠️ **不含 org 规则**,亲读 `persistedRuleMandateOf`
|
|
807
|
+
// 只收 {egress, irreversibility, shellGated} 三位)、`requiresRealApproval`、shadowed、hook 产、inherited-unresolved、ancestor-resolved、
|
|
808
|
+
// anonymous(无 principal 且未声明 local-owner)六族 ask **一律不带** `ruleSuggestions`(亲读
|
|
809
|
+
// `dist/core/runner/prepare-task.js` 的 `ruleSuggestionsOf`,不是读 CHANGELOG)。这与本仓那句
|
|
810
|
+
// 「发一格按下去无处可兑的『不再询问』= wire 谎言」是**同一条判据**,只是这次由引擎在源头执行。
|
|
811
|
+
// 上面四个合取项**一个都不删**:①③④ 是我方独有的前置(店装配 / 空数组不铸键 / 命令可读),
|
|
812
|
+
// ② 的 `governanceForced` 是**我方**的治理标(部署侧 AUTONOMY/commandPolicy/SENSITIVE_WRITE_PATTERNS,
|
|
813
|
+
// 引擎的 mandated 词表里没有它),六族里的另外四族只有引擎判得出(消音谓词 / hook 出处 / 继承未决 /
|
|
814
|
+
// 祖先决议),所以两侧不是重复,是各管各的那一段。
|
|
815
|
+
// 唯一**重叠**的是 anonymous 那一族:引擎的判据是 `spec.principal` 空且 `deps.localOwnerRules !== true`
|
|
816
|
+
// (我方从不设后者,亲验 `grep -rn localOwnerRules src` 只有 adoption 计划表里的一行声明位),
|
|
817
|
+
// 而我方的判据是 ask 的 `owner === null` —— 两者同源(`askOwner = gatedPrincipal(req) ?? null` 与
|
|
818
|
+
// `spec.principal = auth?.principal` 取自同一次鉴权)⇒ **本批对单机匿名部署零行为变化**(那种卡在
|
|
819
|
+
// #203 codex R1-F1 之后本来就不带候选)。⚠️ 记档:若将来要让单机形也能「不再询问」,正路是声明
|
|
820
|
+
// `RunnerDeps.localOwnerRules`(core 会在 provider 缺 `forLocalOwner` 时**抛**,不静默),而不是把
|
|
821
|
+
// 我方这道 owner 门放宽 —— 那会造出一张按下去无处可兑的卡。
|
|
822
|
+
// 候选**基数**同批从 ≤1 变成 ≤2(reviewed 前缀候选,`exact` 恒 index 0)——本层逐字透传整只数组,
|
|
823
|
+
// 不截长不重排;兑付口按**人报的文本**定位(`rules-consent.ts` 的 `findIndex`,不是 `[0]`)。
|
|
785
824
|
// `let`:上面那条并集裁定可能在对账后把它撤回 undefined(行说这是治理门)。
|
|
786
825
|
let ruleLaneMaterial = this.buildRuleLaneMaterial(req, primary.owner, governanceForced);
|
|
787
826
|
const frame = {
|
|
@@ -1645,14 +1684,28 @@ export class ToolApprovalCoordinator {
|
|
|
1645
1684
|
* 时仍是纯同步函数(D1)。`askStore`/`askId`/`batchId` 由调用方在已窄化的分支里传入(避免非空断言)。 */
|
|
1646
1685
|
async respondWithCas(askStore, askId, batchId, id, entry, parsed) {
|
|
1647
1686
|
let decidedWon = false;
|
|
1687
|
+
// 🔴 #229 + codex 对抗复审 round1 [high](验真后修):`noteRecorded` 的判据比 `decidedWon` **更严**,
|
|
1688
|
+
// 所以是**两个**变量而不是一个。`decidedWon` 回答「这条决议是不是终局」(单赢者 CAS 的语义,足够支撑
|
|
1689
|
+
// 200/404 的分派与 `notifyExternalDecision`);`noteLanded` 回答「**这一次请求**的那段文本在不在行上」。
|
|
1690
|
+
// 两者在**提交歧义**臂上会分岔(见下方 catch 里的理由),而回执上那句「你的理由记上了」只能由后者答。
|
|
1691
|
+
let noteLanded = false;
|
|
1648
1692
|
try {
|
|
1649
|
-
const decided = await this.withStoreDeadline(
|
|
1693
|
+
const decided = await this.withStoreDeadline(
|
|
1694
|
+
// #229:`decisionNote` 是**店缝上既有**的键(`DecideAskInput.decisionNote`,SQL twin 与 memory twin
|
|
1695
|
+
// 都已实装)⇒ 零新 SQL、零新列,live 腿与 durable 腿写同一条 UPDATE 的同一列。
|
|
1696
|
+
askStore.decideAsk(askId, batchId, {
|
|
1697
|
+
decision: parsed.value === "deny" ? "deny" : "approve",
|
|
1698
|
+
...(parsed.note !== undefined ? { decisionNote: parsed.note } : {}),
|
|
1699
|
+
}), "decideAsk", DURABLE_CALL_TIMEOUT_MS, (late) => {
|
|
1650
1700
|
// R3-1 迟到成功:HTTP 响应早已发出(这次调用报的是超时),但**决议真落了盘** —— 同 askId 下
|
|
1651
1701
|
// 还在悬挂的本地条目必须按真决议结清,否则它们只能等自己的窗/取消再绕一圈。
|
|
1652
1702
|
if (late.ok)
|
|
1653
1703
|
this.notifyExternalDecision(askId, parsed.value !== "deny", parsed.updatedInput);
|
|
1654
1704
|
});
|
|
1655
1705
|
decidedWon = decided.ok;
|
|
1706
|
+
// 干净赢下 CAS ⇒ note 与决议是**同一条 UPDATE** 写的,赢即落,不必再读一次。
|
|
1707
|
+
if (decided.ok)
|
|
1708
|
+
noteLanded = true;
|
|
1656
1709
|
if (!decided.ok) {
|
|
1657
1710
|
// CAS 输——respond 绝不覆盖赢家(D2)。逐字复用现行 404 形(409/410 分化留给车4)。
|
|
1658
1711
|
return { status: 404, body: { error: "no pending tool approval for this id (settled, expired, or not on this replica)", errorCode: "tool_approval.not_pending" } };
|
|
@@ -1668,8 +1721,18 @@ export class ToolApprovalCoordinator {
|
|
|
1668
1721
|
// 那本就该回放成功)。复核读本身失败/超时 ⇒ 维持「不知道」,守卫照旧 404(不许把未知说成成功)。
|
|
1669
1722
|
try {
|
|
1670
1723
|
const row = await this.withStoreDeadline(askStore.getAsk(askId), "getAsk(decideAsk-confirm)", DURABLE_CONVERGE_READ_TIMEOUT_MS);
|
|
1671
|
-
if (row?.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve"))
|
|
1724
|
+
if (row?.state === "DECIDED" && row.decision === (parsed.value === "deny" ? "deny" : "approve")) {
|
|
1672
1725
|
decidedWon = true;
|
|
1726
|
+
// 🔴 codex 对抗复审 round1 [high](真 finding,红先复现):**note 的判据不能沿用决议的判据**。
|
|
1727
|
+
// 上一段那句「别人不可能写出同一条决议再让我们看见」对**决议**成立(单赢者 CAS),对 `note`
|
|
1728
|
+
// **不成立** —— 两路并发 respond 完全可以带**同决议、不同 note**:一路干净赢下 CAS(写进去的是
|
|
1729
|
+
// 它的 note),另一路撞上提交歧义,在这次确认读里看见那条 DECIDED 行。只比决议的话,输的那一路
|
|
1730
|
+
// 会把别人的胜利认成自己的,回一句「你的理由记上了」,而行上是**别人**的理由 —— 恰好是本布尔
|
|
1731
|
+
// 存在的意义被反过来用。所以这里逐字比**行上的 note**:相等才算落地(文本真的相同就不是谎,
|
|
1732
|
+
// 哪怕是别人写的);行上没有 note、或与本次提交不同 ⇒ 如实 `false`。
|
|
1733
|
+
// 决议侧的 `decidedWon` 保持原样(200/404 的分派与 `notifyExternalDecision` 的判据不动)。
|
|
1734
|
+
noteLanded = parsed.note === undefined || row.decisionNote === parsed.note;
|
|
1735
|
+
}
|
|
1673
1736
|
}
|
|
1674
1737
|
catch (confirmErr) {
|
|
1675
1738
|
this.noteStoreError(confirmErr, "getAsk(decideAsk-confirm)");
|
|
@@ -1707,9 +1770,12 @@ export class ToolApprovalCoordinator {
|
|
|
1707
1770
|
// session 记忆照记(grant 谈的是**将来**的 ask,与这次投递是否由我完成无关)。
|
|
1708
1771
|
// 但 `updatedInput` **没有**随那次结算送达闭包(R2-4)⇒ 显式声明未投递,回显里不许出现
|
|
1709
1772
|
// `updatedInputForwarded`。
|
|
1710
|
-
|
|
1773
|
+
// #229:`noteRecorded` 取的是**店的真实结果**(`noteLanded`,比 `decidedWon` 更严——见其声明处),
|
|
1774
|
+
// 与 `updatedInputForwarded` 的判据刻意分家:那一格问「闭包收到编辑没有」(本支恒否),
|
|
1775
|
+
// 这一格问「行上记下的是不是**这次**的理由」。两件事,不共用一个布尔。
|
|
1776
|
+
return this.finishRespond(id, entry, parsed, { updatedInputDelivered: false, noteRecorded: noteLanded });
|
|
1711
1777
|
}
|
|
1712
|
-
const result = this.finishRespond(id, entry, parsed);
|
|
1778
|
+
const result = this.finishRespond(id, entry, parsed, { noteRecorded: noteLanded });
|
|
1713
1779
|
// round5(pendingByAskId 顶注):回决赢下 CAS 时同样要收尾「同 askId 重复本地注册」那一支——与
|
|
1714
1780
|
// windowExpired/runCancel/emitAllP 三条竞争者的赢家路径同精神(那三处已经这么做)。此处 entry 已经
|
|
1715
1781
|
// 经 `finishRespond` 自行 settle 并从 pendingByAskId 的 Set 里摘除自己,故这里天然只会清算真正
|
|
@@ -1749,6 +1815,21 @@ export class ToolApprovalCoordinator {
|
|
|
1749
1815
|
// 命令行);此时还回 `updatedInputForwarded: true` 等于告诉壳「你的编辑生效了」,是最不该撒的那种谎。
|
|
1750
1816
|
// 调用方在 stale-entry 分支传 `updatedInputDelivered: false`,回显里这个键就整个缺席(壳按未透传处理)。
|
|
1751
1817
|
const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
|
|
1818
|
+
// #229(设计稿 233 稿B v2 §1):`noteRecorded` = 这次回决的**理由到底有没有落进持久行**。
|
|
1819
|
+
//
|
|
1820
|
+
// 🔴 形照 `rulePersisted`(always-emit 布尔,只在请求真带了 `note` 时在场),值照**店的真实结果**
|
|
1821
|
+
// (调用方传进来的 `decidedWon`,含 CAS 抛错后那次确认读的改判),不是「askStore 在不在」:
|
|
1822
|
+
// · 本方法被 `respond()` **直接**调到 = store 缺席,或 store 在场但 `ensureAsk` 失败已清掉坐标 ——
|
|
1823
|
+
// 两种都是「压根没打过那条 UPDATE」,缺省 `false` 正是这一支的真相;
|
|
1824
|
+
// · `respondWithCas` 的 D5 fail-open 支(店抖动、确认读也没读到赢)同样 `false` —— **不知道不许
|
|
1825
|
+
// 说成成功**(与 `updatedInputForwarded` 从不发 `false`、只在真投递时发 `true` 是同一条纪律的两面:
|
|
1826
|
+
// 那一格靠缺席表达否定,这一格是三态里的一态,必须显式说 `false`,否则壳分不出「这台不支持」);
|
|
1827
|
+
// · 提交歧义臂上**赢了决议但行上是别人的 note**(同决议不同 note 的并发)同样 `false` —— 判据是
|
|
1828
|
+
// 「行上那段文本是不是这次提交的」,不是「这条决议是不是终局」(codex round1 [high],见调用方
|
|
1829
|
+
// `respondWithCas` 里 `noteLanded` 的推导)。
|
|
1830
|
+
// 能力位 `capabilities.approvalDecisionNote` 与本格**必须同车**:老服务对未知键静默丢 + 200,少了
|
|
1831
|
+
// 能力位,「记上了」与「这台不认识 note」在 wire 上不可判别。
|
|
1832
|
+
const noteRecorded = opts?.noteRecorded ?? false;
|
|
1752
1833
|
return {
|
|
1753
1834
|
status: 200,
|
|
1754
1835
|
body: {
|
|
@@ -1757,6 +1838,7 @@ export class ToolApprovalCoordinator {
|
|
|
1757
1838
|
decision: parsed.value,
|
|
1758
1839
|
...(rememberApplied !== undefined ? { rememberApplied } : {}),
|
|
1759
1840
|
...(parsed.updatedInput !== undefined && allowed && updatedInputDelivered ? { updatedInputForwarded: true } : {}),
|
|
1841
|
+
...(parsed.note !== undefined ? { noteRecorded } : {}),
|
|
1760
1842
|
},
|
|
1761
1843
|
};
|
|
1762
1844
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.15.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
57
|
+
"@sema-agent/core": "^5.27.0",
|
|
58
58
|
"@sema-agent/registry-core": "^0.16.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"sharp": "^0.35.3"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@sema-agent/sdk": "^6.
|
|
72
|
+
"@sema-agent/sdk": "^6.16.0",
|
|
73
73
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
74
74
|
"@types/node": "22.10.2",
|
|
75
75
|
"@types/pg": "^8.20.0",
|