@sema-agent/server 7.115.0 → 7.116.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 +167 -4
- package/USAGE.md +6 -3
- package/dist/approval-ask-audit-store.d.ts +25 -6
- package/dist/approval-ask-audit-store.js +6 -1
- package/dist/approval-card.d.ts +8 -4
- package/dist/approval-card.js +2 -0
- package/dist/approval-crash-convergence.d.ts +60 -0
- package/dist/approval-crash-convergence.js +75 -0
- package/dist/approval-deny-reasons.d.ts +13 -0
- package/dist/approval-deny-reasons.js +2 -0
- package/dist/approval.js +2 -1
- package/dist/boot/coordinators.js +0 -12
- package/dist/boot/execution-env.js +2 -1
- package/dist/boot/resolve-spec.js +1 -0
- package/dist/boot/stage-07-capability-layer.js +21 -15
- package/dist/boot/stores.js +4 -4
- package/dist/capabilities/hands-lane.d.ts +0 -23
- package/dist/capabilities/hands-lane.js +3 -2
- package/dist/capabilities/send-user-file-tool.d.ts +10 -22
- package/dist/capabilities/send-user-file-tool.js +1 -8
- package/dist/capability-facts.d.ts +296 -0
- package/dist/capability-facts.js +354 -0
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config.d.ts +16 -13
- package/dist/config.js +12 -12
- package/dist/http/admission.d.ts +6 -0
- package/dist/http/admission.js +22 -5
- package/dist/http/principal-gate.d.ts +3 -0
- package/dist/http/principal-gate.js +3 -0
- package/dist/http/resume-legs.js +10 -1
- package/dist/http/retired-skill-manifest.d.ts +23 -0
- package/dist/http/retired-skill-manifest.js +22 -0
- package/dist/http/route-table.d.ts +3 -2
- package/dist/http/route-table.js +5 -5
- package/dist/http/routes/a2a-serve.js +14 -21
- package/dist/http/routes/admin-config-refresh.js +7 -2
- package/dist/http/routes/admin-drain.js +7 -2
- package/dist/http/routes/admin-memory-optout.js +20 -6
- package/dist/http/routes/adoption.js +19 -5
- package/dist/http/routes/agents-roster.js +4 -2
- package/dist/http/routes/approvals-assistant.js +41 -18
- package/dist/http/routes/background.d.ts +0 -2
- package/dist/http/routes/background.js +4 -5
- package/dist/http/routes/capabilities.d.ts +2 -1
- package/dist/http/routes/capabilities.js +62 -65
- package/dist/http/routes/config-catalog.d.ts +4 -5
- package/dist/http/routes/config-catalog.js +16 -2
- package/dist/http/routes/devices.js +17 -11
- package/dist/http/routes/diagnostics.js +6 -1
- package/dist/http/routes/fleet.js +4 -2
- package/dist/http/routes/images.js +30 -3
- package/dist/http/routes/leader.js +7 -1
- package/dist/http/routes/mcp-reconnect.js +17 -10
- package/dist/http/routes/memory-bundle.js +20 -6
- package/dist/http/routes/memory-compliance.js +19 -5
- package/dist/http/routes/memory-consolidation.d.ts +9 -12
- package/dist/http/routes/memory-consolidation.js +17 -3
- package/dist/http/routes/memory-origin.js +19 -5
- package/dist/http/routes/notify-wake.js +5 -3
- package/dist/http/routes/observability.js +19 -5
- package/dist/http/routes/retention-ops.js +6 -1
- package/dist/http/routes/rules.js +49 -21
- package/dist/http/routes/run-memory-capture.js +5 -2
- package/dist/http/routes/runs.js +90 -60
- package/dist/http/routes/session-sync.js +64 -53
- package/dist/http/routes/sessions-list.js +8 -5
- package/dist/http/routes/sessions.js +39 -24
- package/dist/http/routes/shared-memory.js +5 -3
- package/dist/http/routes/tasks.js +2 -1
- package/dist/http/routes/trace-usage.js +18 -11
- package/dist/http/routes/workflows.js +36 -13
- package/dist/http/server.js +4 -1
- package/dist/http/wire-types.d.ts +6 -0
- package/dist/namespaced-rule-name.d.ts +54 -0
- package/dist/namespaced-rule-name.js +33 -0
- package/dist/observability/corrupt-read-seat.d.ts +4 -3
- package/dist/observability/corrupt-read-seat.js +4 -3
- package/dist/observability/fail-open.d.ts +5 -1
- package/dist/observability/fail-open.js +5 -1
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/plugins/file-run-store.d.ts +7 -1
- package/dist/plugins/file-run-store.js +20 -7
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/one-time-migrations.d.ts +4 -2
- package/dist/plugins/one-time-migrations.js +26 -3
- package/dist/plugins/pg-session-storage.js +1 -1
- package/dist/plugins/run-store-sql.js +4 -4
- package/dist/plugins/session-policy-store-sql.js +4 -2
- package/dist/plugins/store-backend.d.ts +6 -1
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/store-contracts.d.ts +27 -8
- package/dist/plugins/store-contracts.js +12 -3
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/request-key-closure.js +1 -1
- package/dist/run-local.d.ts +37 -3
- package/dist/run-local.js +48 -5
- package/dist/runs.d.ts +0 -58
- package/dist/runs.js +15 -20
- package/dist/session-policy-wire.d.ts +40 -60
- package/dist/session-policy-wire.js +60 -32
- package/dist/spec-fields.d.ts +32 -0
- package/dist/spec-fields.js +31 -16
- package/dist/standing-answer-grant.d.ts +8 -0
- package/dist/standing-answer-grant.js +5 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/tool-approval.d.ts +16 -0
- package/dist/tool-approval.js +29 -7
- package/dist/trace/core-keyset-guard.d.ts +4 -4
- package/dist/trace/self-minted-wire.d.ts +42 -0
- package/dist/trace/self-minted-wire.js +13 -0
- package/dist/user-input-ledger.d.ts +8 -5
- package/package.json +4 -2
- package/scripts/drain-check-local.mjs +115 -25
- package/scripts/drain-check.mysql.sql +67 -0
- package/scripts/drain-check.pg.sql +71 -0
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* 换钉前的**排空检查 —— `DB_BACKEND=local`(File 店)车道**,只读。随 npm 包出(`scripts/drain-check-local.mjs`)。
|
|
4
|
-
*
|
|
4
|
+
* 一次跑**四条判据**(判据表 {@link DRAIN_CRITERIA},每条一行末行、各自的命中行;SQL 车道的同一判据是同目录随包的 `drain-check.mysql.sql` / `drain-check.pg.sql`,
|
|
5
|
+
* 每条判据一段、段首标记 `-- drain-check:<判据名>:<后端>`,判据名与本表的 `name` 同一个词):
|
|
6
|
+
* · `skill-grants-rollback`(**回滚**到 7.115.x 前,即引擎从 7.41 退回 7.40):四样任一 ——
|
|
7
|
+
* ① pending 行带 `state.skillGrants`(本次 run 冻结在行上的技能授予行:模型激活 / 子代捕获写下的放行与禁止);
|
|
8
|
+
* ② 行带 `pendingAction.boundGrantDigest`(停驻的 `Skill` 卡的同意绑定);
|
|
9
|
+
* ③ 行所在会话的续跑上下文(`checkpoint-ctx/*.json` 的 `ctx.body.userInvokedSkills`)是非空数组 —— 人用名字点过的技能写下的行**不冻结**进 ①
|
|
10
|
+
* (引擎靠续跑重递请求键重写它们),而 7.115.x 的续跑翻译点不认这个键、不重递。
|
|
11
|
+
* ④ 行所在会话的续跑上下文体里,某只技能(顶层 `skills[]` 或某只子代的 `agents[].skills[]`)声明了**非空** `disallowedTools` —— 那只技能停驻时还没被激活(没有授予行可冻在行上),
|
|
12
|
+
* 回滚后模型再激活它,上一版不读这个键、什么也不拒;前三样都数不到这一形。
|
|
13
|
+
* 7.40 的引擎续跑这种行时都不读:这一 run 的技能禁止行丢失、卡的绑定不核。①② 只看键**在不在**(在却读不出的节 7.40 同样不读,一并数);
|
|
14
|
+
* 这是行形 / 上下文形上的事实,本服务运行期没有同一判据的函数(它不拒这种行),所以判据就写在这一张表里(与 `retired-mode-seed` 同),
|
|
15
|
+
* SQL 车道的孪生是随包 `drain-check.*.sql` 里同名的两段只读 SQL(`-- drain-check:skill-grants-rollback:<后端>`);读不懂的 ctx 档计入 `corrupt`(rc 2:一份读不懂的上下文可能正是带这个键的那一份);
|
|
16
|
+
* · `retired-skill-manifest`(升级到 7.116.0 前,core 7.41.0 退役技能上的 `manifest` 键):pending 行所在会话的续跑上下文(`checkpoint-ctx/*.json`)里,提交体顶层 `skills[]` 或某只子代定义的
|
|
17
|
+
* `agents[].skills[]` 仍带 `manifest`(任何值)—— 升级后续跑顶层那种会被引擎按名拒(行仍 pending,只能取消重提),子代那种不被拒、而是那只子代定义被整只丢掉并留一条警告日志(任务照跑、少一只子代)。命中行点名是哪一层(`member=skills` / `member=agents.skills`)。
|
|
18
|
+
* 「带 `manifest`」的判据是服务里**一只**函数(`src/http/retired-skill-manifest.ts`,提交口的顶层技能门读同一只),经包里脚本隔壁的编译产物 `dist/http/retired-skill-manifest.js` 取 —— 本脚本不写第二份;
|
|
5
19
|
* · `retired-mode-seed`(换钉 7.113.0 前,core 7.39.0 删掉退役 mode-seed 的一次性迁移):pending 行的 `state.inheritedGate` 仍带退役成员;
|
|
6
20
|
* · `permissions-allow-list`(换钉 7.115.0 前,本服务把 `settings.permissions.allow` 从名单改成 allow 规则):pending 行所在会话的续跑上下文
|
|
7
21
|
* (`checkpoint-ctx/*.json` 的信封 `{ sessionId, updatedAt, ctx }`)里,`ctx.body.settings.permissions.allow` 是非空数组,且 `ctx.permissionsAllowReading` 不是 `"statement"`
|
|
8
22
|
* —— 这样的行在 7.115.0 上续跑会被拒(行字节不动;出路按停驻种类分,见随包 `MIGRATION.md` 7.115.0 节)。判据**就是**服务续跑腿拒臂的那一只函数(`src/http/checkpoint-ctx.ts` 的 `allowListReadingUnstated`),
|
|
9
23
|
* 经包里脚本隔壁的编译产物 `dist/http/checkpoint-ctx.js` 取(与缺省数据根经 `dist/local-data-root.js` 取同一姿势;该模块只有类型 import,编出来零运行期依赖)—— 本脚本不写第二份。
|
|
10
|
-
* 扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)
|
|
24
|
+
* 扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)各条判据共用;判据只回答「这一条 pending 行算不算命中」。
|
|
11
25
|
*
|
|
12
26
|
* ## 为什么是一只脚本,不是一行 grep
|
|
13
27
|
* ① 判据有三条成员(core `refuseRetiredModeSeed`):`autoModeRequested` / `approverPosture` **任何值**(含 JSON null)、
|
|
@@ -41,13 +55,16 @@
|
|
|
41
55
|
* 根 `LOCK` 是 pidfile、干净退出即删,不能当标记)⇒ rc 2,不给结论 —— 路径打错、env 没对上服务,都会读到一个什么都没有的目录。
|
|
42
56
|
*
|
|
43
57
|
* ## 读数(机读末行 + 退出码)
|
|
44
|
-
* · 每条命中一行:`
|
|
45
|
-
*
|
|
58
|
+
* · 每条命中一行:`SKILL-GRANTS-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skillGrants|boundGrantDigest|userInvokedSkills|disallowedTools>` / `SKILL-MANIFEST-ROW token=<token> sessionId=<id> createdAt=<ms> member=<skills|agents.skills>` /
|
|
59
|
+
* `ALLOW-LIST-ROW token=<token> sessionId=<id> createdAt=<ms>` / `RETIRED-ROW token=<token> sessionId=<id> createdAt=<ms> member=<成员>`;
|
|
60
|
+
* · 每条判据一行末行(表序;`RETIRED-MODE-SEED` 那一行恒在最后,7.113.0 起读「最后一行」的下游不受影响;`PERMISSIONS-ALLOW-LIST` 仍是倒数第二行):
|
|
61
|
+
* `SKILL-GRANTS rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
|
|
62
|
+
* `RETIRED-SKILL-MANIFEST rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
|
|
46
63
|
* `PERMISSIONS-ALLOW-LIST rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
|
|
47
64
|
* `RETIRED-MODE-SEED rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`;
|
|
48
65
|
* `corrupt` = 共用扫描的坏记录 + 这条判据自己读不懂的旁路输入(`permissions-allow-list`:读不懂的 ctx 档,文件名进 stderr —— 一份读不懂的 ctx 可能正是某条 pending 行带 `allow` 的那一份);
|
|
49
66
|
* 没读成 / 不能下结论(rc 2 的几种缺席与不稳形)时前五个计数是 `-`、多一个 `reason=<词>`,**不会出现 `rows=0`**(下游读 `rows=0` 即「干净」);
|
|
50
|
-
* · rc 0 =
|
|
67
|
+
* · rc 0 = 每条判据都零命中(可换钉 / 可回滚)· rc 1 = 有命中(逐行列出;答完或取消再跑)· rc 2 = 读不懂 / 没读成 / 不能下结论(账本里有本脚本不认的事件词 / 不合形的记录 / 读不懂的 ctx 档 /
|
|
51
68
|
* 数据根缺席 / 缺省根解析不可用 / 判据模块不可用(脚本隔壁没有 `dist/`:源码树没 build)/ 读期间文件一直在变 / 读文件出错 / 用法错)—— 不给结论(缺席 ≠ 干净:一条读不懂的记录可能正是让某行回到 pending 的那一条)。
|
|
52
69
|
* rc 2 与 rc 1 同时成立时(有命中、也有读不懂的记录)报 rc 2,命中行照列。
|
|
53
70
|
*/
|
|
@@ -56,6 +73,9 @@ import { homedir } from "node:os";
|
|
|
56
73
|
import { join } from "node:path";
|
|
57
74
|
import { fileURLToPath } from "node:url";
|
|
58
75
|
|
|
76
|
+
/** 从脚本隔壁的编译产物取来的服务判据(脚本不持第二份实现)。
|
|
77
|
+
* @typedef {{ allowListReadingUnstated: (ctx: Record<string, unknown>) => boolean, retiredSkillManifestLayerOf: (body: unknown) => "skills" | "agents.skills" | undefined }} ServiceCriteria */
|
|
78
|
+
|
|
59
79
|
/** 读期间文件在变时的有界重读次数(含第一次)。 */
|
|
60
80
|
const STABLE_READ_ATTEMPTS = 3;
|
|
61
81
|
/** core 账本事件注册表(`stores/file/checkpoint-store.js:7-14`)。 */
|
|
@@ -215,6 +235,18 @@ function retiredMember(gate) {
|
|
|
215
235
|
/** @param {unknown} v */
|
|
216
236
|
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
217
237
|
|
|
238
|
+
/**
|
|
239
|
+
* 提交体里有没有技能声明了**非空** `disallowedTools`(数组、至少一员):顶层 `skills[]`,或某只子代定义的 `agents[].skills[]`。体不是对象 / 层不是数组 / 项不是对象 ⇒ 该处不算。
|
|
240
|
+
* @param {unknown} body */
|
|
241
|
+
const declaresDisallowedTools = (body) => {
|
|
242
|
+
if (!isObject(body)) return false;
|
|
243
|
+
const { skills, agents } = /** @type {{ skills?: unknown, agents?: unknown }} */ (body);
|
|
244
|
+
/** @param {unknown} list */
|
|
245
|
+
const anyDeclares = (list) => Array.isArray(list) && list.some((sk) => isObject(sk) && Array.isArray(/** @type {Record<string, unknown>} */ (sk).disallowedTools) && /** @type {unknown[]} */ (/** @type {Record<string, unknown>} */ (sk).disallowedTools).length > 0);
|
|
246
|
+
if (anyDeclares(skills)) return true;
|
|
247
|
+
return Array.isArray(agents) && agents.some((a) => isObject(a) && anyDeclares(/** @type {Record<string, unknown>} */ (a).skills));
|
|
248
|
+
};
|
|
249
|
+
|
|
218
250
|
/**
|
|
219
251
|
* `<数据根>/checkpoint-ctx/*.json` —— 本服务 local 店的续跑上下文,一档一会话,信封 `{ sessionId, updatedAt, ctx }`(`ctx` 是对象;形判据同本服务 `LocalCheckpointStore` 的
|
|
220
252
|
* `CtxRecordSchema` + 对象读者)。目录不存在 = 没有 ctx(没写过);读不懂的档计入 `corrupt`(不猜它属于哪个会话)、文件名回给调用方;读期间被收割删掉的档跳过。
|
|
@@ -254,14 +286,52 @@ function readCtxIndex(root) {
|
|
|
254
286
|
}
|
|
255
287
|
|
|
256
288
|
/**
|
|
257
|
-
* 判据表 —— 「哪些 pending 行算命中」。扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)
|
|
289
|
+
* 判据表 —— 「哪些 pending 行算命中」。扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)各条共用,判据只管这一问:
|
|
258
290
|
* · `tail` / `hit`:末行前缀 / 命中行前缀;
|
|
259
291
|
* · `load(root, service)`:这条判据要的旁路输入(读不懂的记录计入 `corrupt`,`unreadable` 回显进 stderr);`service` = 从编译产物取来的服务判据({@link drainReport} 取一次);
|
|
260
292
|
* · `judge(row, input)`:命中 ⇒ 命中行的尾巴(可为空串),不命中 ⇒ `undefined`。
|
|
261
293
|
* 表序 = 输出序;`retired-mode-seed` 排最后 ⇒ 最后一行仍是 `RETIRED-MODE-SEED …`(7.113.0 起读最后一行的下游)。
|
|
262
|
-
* @type {ReadonlyArray<{ name: string, tail: string, hit: string, load: (root: string, service:
|
|
294
|
+
* @type {ReadonlyArray<{ name: string, tail: string, hit: string, load: (root: string, service: ServiceCriteria) => { input: unknown, corrupt: number, unreadable: string[] }, judge: (row: Record<string, unknown>, input: unknown) => string | undefined }>}
|
|
263
295
|
*/
|
|
264
296
|
export const DRAIN_CRITERIA = [
|
|
297
|
+
{
|
|
298
|
+
// 回滚到 7.115.x(引擎 7.40)之前:7.41 停驻的行上 7.40 不读的四样(头注第一条)。先看授予节,再看卡的绑定,再看会话上下文里的人点名技能,最后看上下文里还没被激活的技能声明的禁止清单;
|
|
299
|
+
// 命中行点名是哪一样。
|
|
300
|
+
name: "skill-grants-rollback",
|
|
301
|
+
tail: "SKILL-GRANTS",
|
|
302
|
+
hit: "SKILL-GRANTS-ROW",
|
|
303
|
+
load: (root) => {
|
|
304
|
+
const idx = readCtxIndex(root);
|
|
305
|
+
return { input: idx.bySession, corrupt: idx.corrupt, unreadable: idx.unreadable };
|
|
306
|
+
},
|
|
307
|
+
judge: (row, input) => {
|
|
308
|
+
if (isObject(row.state) && Object.hasOwn(/** @type {Record<string, unknown>} */ (row.state), "skillGrants")) return " member=skillGrants";
|
|
309
|
+
if (isObject(row.pendingAction) && Object.hasOwn(/** @type {Record<string, unknown>} */ (row.pendingAction), "boundGrantDigest")) return " member=boundGrantDigest";
|
|
310
|
+
const bySession = /** @type {Map<string, Record<string, unknown>>} */ (input);
|
|
311
|
+
const ctx = typeof row.sessionId === "string" ? bySession.get(row.sessionId) : undefined;
|
|
312
|
+
const body = ctx !== undefined && isObject(ctx.body) ? /** @type {Record<string, unknown>} */ (ctx.body) : undefined;
|
|
313
|
+
const invoked = body?.userInvokedSkills;
|
|
314
|
+
if (Array.isArray(invoked) && invoked.length > 0) return " member=userInvokedSkills";
|
|
315
|
+
return declaresDisallowedTools(body) ? " member=disallowedTools" : undefined;
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
// 升级到 7.116.0 之前:停驻、持久化体里技能还带退役 `manifest` 的行(头注第二条判据)。判据函数是服务里那一只(`retiredSkillManifestLayerOf`,经编译产物取);
|
|
320
|
+
// 命中行点名是顶层 `skills[]` 还是子代 `agents[].skills[]`。表序紧跟回滚前那一条 —— `PERMISSIONS-ALLOW-LIST` 仍倒数第二、`RETIRED-MODE-SEED` 仍最后。
|
|
321
|
+
name: "retired-skill-manifest",
|
|
322
|
+
tail: "RETIRED-SKILL-MANIFEST",
|
|
323
|
+
hit: "SKILL-MANIFEST-ROW",
|
|
324
|
+
load: (root, service) => {
|
|
325
|
+
const idx = readCtxIndex(root);
|
|
326
|
+
return { input: { bySession: idx.bySession, layerOf: service.retiredSkillManifestLayerOf }, corrupt: idx.corrupt, unreadable: idx.unreadable };
|
|
327
|
+
},
|
|
328
|
+
judge: (row, input) => {
|
|
329
|
+
const { bySession, layerOf } = /** @type {{ bySession: Map<string, Record<string, unknown>>, layerOf: ServiceCriteria["retiredSkillManifestLayerOf"] }} */ (input);
|
|
330
|
+
const ctx = typeof row.sessionId === "string" ? bySession.get(row.sessionId) : undefined;
|
|
331
|
+
const layer = ctx === undefined ? undefined : layerOf(ctx.body);
|
|
332
|
+
return layer === undefined ? undefined : ` member=${layer}`;
|
|
333
|
+
},
|
|
334
|
+
},
|
|
265
335
|
{
|
|
266
336
|
name: "permissions-allow-list",
|
|
267
337
|
tail: "PERMISSIONS-ALLOW-LIST",
|
|
@@ -297,6 +367,42 @@ function unreadableReport(reason, sentence, rootEcho) {
|
|
|
297
367
|
return { rc: /** @type {const} */ (2), stdout: DRAIN_CRITERIA.map((c) => `${c.tail} rows=- pending=- total=- unknownEvents=- corrupt=- reason=${reason} root=${rootEcho}`), stderr: [sentence] };
|
|
298
368
|
}
|
|
299
369
|
|
|
370
|
+
/**
|
|
371
|
+
* 取服务判据(每个判据模块一次 import + 导出自检)。任一模块加载失败 / 导出缺席 ⇒ 同一条 `criterion-unavailable` 不给结论(报告),不退回手写判据。
|
|
372
|
+
* @param {string} rootEcho
|
|
373
|
+
* @returns {Promise<ServiceCriteria | ReturnType<typeof unreadableReport>>} */
|
|
374
|
+
async function loadServiceCriteria(rootEcho) {
|
|
375
|
+
/** @type {Array<[string, () => Promise<Record<string, unknown>>, string]>} 〔产物相对路径(报告用), 取模块, 导出名〕 */
|
|
376
|
+
const wanted = [
|
|
377
|
+
["dist/http/checkpoint-ctx.js", () => import("../dist/http/checkpoint-ctx.js"), "allowListReadingUnstated"],
|
|
378
|
+
["dist/http/retired-skill-manifest.js", () => import("../dist/http/retired-skill-manifest.js"), "retiredSkillManifestLayerOf"],
|
|
379
|
+
];
|
|
380
|
+
/** @type {Record<string, unknown>} */
|
|
381
|
+
const fns = {};
|
|
382
|
+
for (const [rel, load, name] of wanted) {
|
|
383
|
+
/** @type {Record<string, unknown>} */
|
|
384
|
+
let mod;
|
|
385
|
+
try {
|
|
386
|
+
mod = await load();
|
|
387
|
+
} catch (e) {
|
|
388
|
+
return unreadableReport(
|
|
389
|
+
"criterion-unavailable",
|
|
390
|
+
`cannot load the service's own criterion (${rel} next to scripts/ is not loadable: ${e instanceof Error ? e.message : String(e)}) — no verdict: run the copy inside an installed @sema-agent/server package (or after \`npm run build\`)`,
|
|
391
|
+
rootEcho,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (typeof mod?.[name] !== "function") {
|
|
395
|
+
return unreadableReport(
|
|
396
|
+
"criterion-unavailable",
|
|
397
|
+
`cannot use the service's own criterion (${rel} next to scripts/ loaded but does not provide it — the compiled output is incomplete or damaged) — no verdict: reinstall @sema-agent/server and run the copy inside the installed package`,
|
|
398
|
+
rootEcho,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
fns[name] = mod[name];
|
|
402
|
+
}
|
|
403
|
+
return /** @type {ServiceCriteria} */ (fns);
|
|
404
|
+
}
|
|
405
|
+
|
|
300
406
|
/**
|
|
301
407
|
* 一次排空检查的完整报告(导出:CLI 与测试共用)。
|
|
302
408
|
* @param {string} root 数据根
|
|
@@ -321,26 +427,10 @@ export async function drainReport(root, opts = {}) {
|
|
|
321
427
|
);
|
|
322
428
|
}
|
|
323
429
|
// 判据的唯一实现在服务里;随包形里编译产物在脚本隔壁。取不到(源码树没 build)⇒ 不给结论 —— 绝不退回一份手写的判据。
|
|
324
|
-
/** @type {{ allowListReadingUnstated: (ctx: Record<string, unknown>) => boolean }} */
|
|
325
|
-
let service;
|
|
326
|
-
try {
|
|
327
|
-
service = await import("../dist/http/checkpoint-ctx.js");
|
|
328
|
-
} catch (e) {
|
|
329
|
-
return unreadableReport(
|
|
330
|
-
"criterion-unavailable",
|
|
331
|
-
`cannot load the service's own criterion (dist/http/checkpoint-ctx.js next to scripts/ is not loadable: ${e instanceof Error ? e.message : String(e)}) — no verdict: run the copy inside an installed @sema-agent/server package (or after \`npm run build\`)`,
|
|
332
|
-
root,
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
430
|
// 模块能加载不等于判据在:产物被截断 / 损坏时 import 照样成功、导出缺席。判据给不出来 ⇒ 同一条「不给结论」,
|
|
336
431
|
// 不许在没有行可判时读成 rows=0,也不许等到调用处抛出(未捕获异常的退出码是 1 = 「有命中」)。
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
"criterion-unavailable",
|
|
340
|
-
"cannot use the service's own criterion (dist/http/checkpoint-ctx.js next to scripts/ loaded but does not provide it — the compiled output is incomplete or damaged) — no verdict: reinstall @sema-agent/server and run the copy inside the installed package",
|
|
341
|
-
root,
|
|
342
|
-
);
|
|
343
|
-
}
|
|
432
|
+
const service = await loadServiceCriteria(root);
|
|
433
|
+
if ("rc" in service) return service;
|
|
344
434
|
let scan;
|
|
345
435
|
/** @type {Array<{ input: unknown, corrupt: number, unreadable: string[] }>} */
|
|
346
436
|
let inputs;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
-- 换钉 / 回滚前的排空检查 —— MySQL 协议后端(TiDB / InnoDB),只读:本文件里只有 SELECT。随 npm 包出。
|
|
2
|
+
--
|
|
3
|
+
-- 怎么跑(整只文件一次跑完,每条判据出一行读数):
|
|
4
|
+
-- mysql -h <主机> -P <端口> -u <用户> -p <库名> < node_modules/@sema-agent/server/scripts/drain-check.mysql.sql
|
|
5
|
+
-- 读数:每条判据一行 `criterion | hit_rows | sessions` ——
|
|
6
|
+
-- criterion 判据名(与随包脚本 drain-check-local.mjs 判据表里的名字同一个词,与下面各段的标记 `-- drain-check:<判据名>:mysql` 同一个词);
|
|
7
|
+
-- hit_rows 命中的停驻行数(pending 行里算命中的那些);**全部为 0 才算排空**,非 0 ⇒ 逐条答完或取消(各判据对应的升级 / 回滚说明见随包 MIGRATION.md);
|
|
8
|
+
-- sessions 命中行所在的会话 id,逗号连接,新的在前。以 hit_rows 为准:这一列被会话变量 group_concat_max_len(缺省 1024 字节)截断时,列出的会话数会少于 hit_rows ——
|
|
9
|
+
-- 先处理列出的再跑一次,或先在同一个会话里 SET SESSION group_concat_max_len = 1048576; 再跑本文件(本文件自己只含 SELECT)。
|
|
10
|
+
-- 每段只数 pending 行(已决 / 已过期的行不数);判据只看**键在不在 / 值形**,不读行的其余内容。
|
|
11
|
+
-- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs,四条判据同名。
|
|
12
|
+
--
|
|
13
|
+
-- 四条判据(顺序同 drain-check-local.mjs 的判据表):
|
|
14
|
+
-- skill-grants-rollback 回滚到 7.115.x(引擎 7.40)之前:7.116.0 停驻的行带 7.40 不读的技能授予节 / 卡绑定 / 会话上下文里的人点名技能 / 还没被激活的技能声明的禁止清单。
|
|
15
|
+
-- retired-skill-manifest 升级到 7.116.0 之前:停驻、持久化提交体里技能(顶层 skills[] 或子代 agents[].skills[])还带已退役的 manifest 键。
|
|
16
|
+
-- permissions-allow-list 换钉 7.115.0 之前:停驻、提交体 settings.permissions.allow 是非空数组且没有「按表态读」的记号。
|
|
17
|
+
-- retired-mode-seed 换钉 7.113.0 之前:检查点的 inheritedGate 里仍带退役 mode-seed 成员。
|
|
18
|
+
|
|
19
|
+
-- drain-check:skill-grants-rollback:mysql —— 期望 hit_rows = 0;每一行 = 一条回滚到 7.115.x 后续跑会丢技能禁止行 / 不核卡绑定的停驻行。
|
|
20
|
+
-- 判据四样任一:前两个键(skillGrants 节 / pendingAction.boundGrantDigest)在不在(在却读不出的授予节 7.40 同样不读、一并数);所在会话续跑上下文里提交体的 userInvokedSkills 是非空数组;
|
|
21
|
+
-- 提交体顶层 skills[] 或某只子代 agents[].skills[] 里某一项声明了非空 disallowedTools(那只技能停驻时还没被激活,没有授予行可冻在行上;回滚后上一版不读这个键)。
|
|
22
|
+
SELECT 'skill-grants-rollback' AS criterion, COUNT(*) AS hit_rows,
|
|
23
|
+
COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
|
|
24
|
+
FROM checkpoint k
|
|
25
|
+
LEFT JOIN checkpoint_ctx c ON c.session_id = k.session_id
|
|
26
|
+
WHERE k.status = 'pending'
|
|
27
|
+
AND (JSON_CONTAINS_PATH(k.checkpoint, 'one', '$.state.skillGrants', '$.pendingAction.boundGrantDigest')
|
|
28
|
+
OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.userInvokedSkills')) = 'ARRAY'
|
|
29
|
+
AND JSON_LENGTH(JSON_EXTRACT(c.ctx, '$.body.userInvokedSkills')) > 0)
|
|
30
|
+
OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.skills')) = 'ARRAY'
|
|
31
|
+
AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.skills[*].disallowedTools[0]'))
|
|
32
|
+
OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.agents')) = 'ARRAY'
|
|
33
|
+
AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.agents[*].skills[*].disallowedTools[0]')));
|
|
34
|
+
|
|
35
|
+
-- drain-check:retired-skill-manifest:mysql —— 期望 hit_rows = 0;每一行 = 一条升级到 7.116.0 后续跑会被引擎拒(顶层 skills[]:行仍 pending、只能取消重提)
|
|
36
|
+
-- 或丢掉子代定义(agents[].skills[]:那只子代定义被整只丢掉并留一条警告日志、任务照跑、少一只子代)的停驻行。
|
|
37
|
+
-- 判据 = 所在会话续跑上下文里提交体的顶层 skills[] 或 agents[].skills[] 某一项带 manifest 键(任何值,含 JSON null)。
|
|
38
|
+
SELECT 'retired-skill-manifest' AS criterion, COUNT(*) AS hit_rows,
|
|
39
|
+
COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
|
|
40
|
+
FROM checkpoint k
|
|
41
|
+
JOIN checkpoint_ctx c ON c.session_id = k.session_id
|
|
42
|
+
WHERE k.status = 'pending'
|
|
43
|
+
AND ((JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.skills')) = 'ARRAY'
|
|
44
|
+
AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.skills[*].manifest'))
|
|
45
|
+
OR (JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.agents')) = 'ARRAY'
|
|
46
|
+
AND JSON_CONTAINS_PATH(c.ctx, 'one', '$.body.agents[*].skills[*].manifest')));
|
|
47
|
+
|
|
48
|
+
-- drain-check:permissions-allow-list:mysql —— 期望 hit_rows = 0;每一行 = 一条升级后续跑会被拒的停驻行。
|
|
49
|
+
-- 判据 = 服务续跑腿拒臂的同一句话:提交体 settings.permissions.allow 是非空数组,且上下文没有读法记号 permissionsAllowReading = "statement"。
|
|
50
|
+
SELECT 'permissions-allow-list' AS criterion, COUNT(*) AS hit_rows,
|
|
51
|
+
COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
|
|
52
|
+
FROM checkpoint k
|
|
53
|
+
JOIN checkpoint_ctx c ON c.session_id = k.session_id
|
|
54
|
+
WHERE k.status = 'pending'
|
|
55
|
+
AND JSON_TYPE(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) = 'ARRAY'
|
|
56
|
+
AND JSON_LENGTH(JSON_EXTRACT(c.ctx, '$.body.settings.permissions.allow')) > 0
|
|
57
|
+
AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(c.ctx, '$.permissionsAllowReading')), '') <> 'statement';
|
|
58
|
+
|
|
59
|
+
-- drain-check:retired-mode-seed:mysql —— 期望 hit_rows = 0;每一行 = 一条升级后会被拒的停放行。
|
|
60
|
+
-- 判据逐字照引擎 refuseRetiredModeSeed:inheritedGate 带 autoModeRequested / approverPosture(任何值,含 JSON null),或 shellGate 不是 "always"(含 JSON null)。
|
|
61
|
+
SELECT 'retired-mode-seed' AS criterion, COUNT(*) AS hit_rows,
|
|
62
|
+
COALESCE(GROUP_CONCAT(k.session_id ORDER BY k.created_at_ms DESC SEPARATOR ','), '') AS sessions
|
|
63
|
+
FROM checkpoint k
|
|
64
|
+
WHERE k.status = 'pending'
|
|
65
|
+
AND (JSON_CONTAINS_PATH(k.checkpoint, 'one', '$.state.inheritedGate.autoModeRequested', '$.state.inheritedGate.approverPosture')
|
|
66
|
+
OR (JSON_CONTAINS_PATH(k.checkpoint, 'one', '$.state.inheritedGate.shellGate')
|
|
67
|
+
AND JSON_UNQUOTE(JSON_EXTRACT(k.checkpoint, '$.state.inheritedGate.shellGate')) <> 'always'));
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
-- 换钉 / 回滚前的排空检查 —— PostgreSQL 后端,只读:本文件里只有 SELECT。随 npm 包出。
|
|
2
|
+
--
|
|
3
|
+
-- 怎么跑(整只文件一次跑完,每条判据出一行读数):
|
|
4
|
+
-- psql "<连接串>" -f node_modules/@sema-agent/server/scripts/drain-check.pg.sql
|
|
5
|
+
-- 读数:每条判据一行 `criterion | hit_rows | sessions` ——
|
|
6
|
+
-- criterion 判据名(与随包脚本 drain-check-local.mjs 判据表里的名字同一个词,与下面各段的标记 `-- drain-check:<判据名>:pg` 同一个词);
|
|
7
|
+
-- hit_rows 命中的停驻行数(pending 行里算命中的那些);**全部为 0 才算排空**,非 0 ⇒ 逐条答完或取消(各判据对应的升级 / 回滚说明见随包 MIGRATION.md);
|
|
8
|
+
-- sessions 命中行所在的会话 id,逗号连接,新的在前。
|
|
9
|
+
-- 每段只数 pending 行(已决 / 已过期的行不数);判据只看**键在不在 / 值形**,不读行的其余内容。
|
|
10
|
+
-- DB_BACKEND=local(File 店)没有 SQL:用同目录的 drain-check-local.mjs,四条判据同名。
|
|
11
|
+
--
|
|
12
|
+
-- 四条判据(顺序同 drain-check-local.mjs 的判据表):
|
|
13
|
+
-- skill-grants-rollback 回滚到 7.115.x(引擎 7.40)之前:7.116.0 停驻的行带 7.40 不读的技能授予节 / 卡绑定 / 会话上下文里的人点名技能 / 还没被激活的技能声明的禁止清单。
|
|
14
|
+
-- retired-skill-manifest 升级到 7.116.0 之前:停驻、持久化提交体里技能(顶层 skills[] 或子代 agents[].skills[])还带已退役的 manifest 键。
|
|
15
|
+
-- permissions-allow-list 换钉 7.115.0 之前:停驻、提交体 settings.permissions.allow 是非空数组且没有「按表态读」的记号。
|
|
16
|
+
-- retired-mode-seed 换钉 7.113.0 之前:检查点的 inheritedGate 里仍带退役 mode-seed 成员。
|
|
17
|
+
|
|
18
|
+
-- drain-check:skill-grants-rollback:pg —— 期望 hit_rows = 0;每一行 = 一条回滚到 7.115.x 后续跑会丢技能禁止行 / 不核卡绑定的停驻行。
|
|
19
|
+
-- 判据四样任一:前两个键(skillGrants 节 / pendingAction.boundGrantDigest)在不在(在却读不出的授予节 7.40 同样不读、一并数);所在会话续跑上下文里提交体的 userInvokedSkills 是非空数组;
|
|
20
|
+
-- 提交体顶层 skills[] 或某只子代 agents[].skills[] 里某一项声明了非空 disallowedTools(那只技能停驻时还没被激活,没有授予行可冻在行上;回滚后上一版不读这个键)。
|
|
21
|
+
SELECT 'skill-grants-rollback' AS criterion, count(*) AS hit_rows,
|
|
22
|
+
coalesce(string_agg(k.session_id, ',' ORDER BY k.created_at_ms DESC), '') AS sessions
|
|
23
|
+
FROM checkpoint k
|
|
24
|
+
LEFT JOIN checkpoint_ctx c ON c.session_id = k.session_id
|
|
25
|
+
WHERE k.status = 'pending'
|
|
26
|
+
AND ((k.checkpoint -> 'state') ? 'skillGrants' OR (k.checkpoint -> 'pendingAction') ? 'boundGrantDigest'
|
|
27
|
+
OR (jsonb_typeof(c.ctx #> '{body,userInvokedSkills}') = 'array' AND (c.ctx #> '{body,userInvokedSkills}') <> '[]'::jsonb)
|
|
28
|
+
OR EXISTS (SELECT 1
|
|
29
|
+
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,skills}') = 'array' THEN c.ctx #> '{body,skills}' ELSE '[]'::jsonb END) AS s
|
|
30
|
+
WHERE jsonb_typeof(s) = 'object' AND jsonb_typeof(s -> 'disallowedTools') = 'array' AND jsonb_array_length(s -> 'disallowedTools') > 0)
|
|
31
|
+
OR EXISTS (SELECT 1
|
|
32
|
+
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,agents}') = 'array' THEN c.ctx #> '{body,agents}' ELSE '[]'::jsonb END) AS a,
|
|
33
|
+
jsonb_array_elements(CASE WHEN jsonb_typeof(a -> 'skills') = 'array' THEN a -> 'skills' ELSE '[]'::jsonb END) AS s
|
|
34
|
+
WHERE jsonb_typeof(s) = 'object' AND jsonb_typeof(s -> 'disallowedTools') = 'array' AND jsonb_array_length(s -> 'disallowedTools') > 0));
|
|
35
|
+
|
|
36
|
+
-- drain-check:retired-skill-manifest:pg —— 期望 hit_rows = 0;每一行 = 一条升级到 7.116.0 后续跑会被引擎拒(顶层 skills[]:行仍 pending、只能取消重提)
|
|
37
|
+
-- 或丢掉子代定义(agents[].skills[]:那只子代定义被整只丢掉并留一条警告日志、任务照跑、少一只子代)的停驻行。
|
|
38
|
+
-- 判据 = 所在会话续跑上下文里提交体的顶层 skills[] 或 agents[].skills[] 某一项(对象)带 manifest 键(任何值,含 JSON null)。
|
|
39
|
+
SELECT 'retired-skill-manifest' AS criterion, count(*) AS hit_rows,
|
|
40
|
+
coalesce(string_agg(k.session_id, ',' ORDER BY k.created_at_ms DESC), '') AS sessions
|
|
41
|
+
FROM checkpoint k
|
|
42
|
+
JOIN checkpoint_ctx c ON c.session_id = k.session_id
|
|
43
|
+
WHERE k.status = 'pending'
|
|
44
|
+
AND (EXISTS (SELECT 1
|
|
45
|
+
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,skills}') = 'array' THEN c.ctx #> '{body,skills}' ELSE '[]'::jsonb END) AS s
|
|
46
|
+
WHERE jsonb_typeof(s) = 'object' AND s ? 'manifest')
|
|
47
|
+
OR EXISTS (SELECT 1
|
|
48
|
+
FROM jsonb_array_elements(CASE WHEN jsonb_typeof(c.ctx #> '{body,agents}') = 'array' THEN c.ctx #> '{body,agents}' ELSE '[]'::jsonb END) AS a,
|
|
49
|
+
jsonb_array_elements(CASE WHEN jsonb_typeof(a -> 'skills') = 'array' THEN a -> 'skills' ELSE '[]'::jsonb END) AS s
|
|
50
|
+
WHERE jsonb_typeof(s) = 'object' AND s ? 'manifest'));
|
|
51
|
+
|
|
52
|
+
-- drain-check:permissions-allow-list:pg —— 期望 hit_rows = 0;每一行 = 一条升级后续跑会被拒的停驻行。
|
|
53
|
+
-- 判据 = 服务续跑腿拒臂的同一句话:提交体 settings.permissions.allow 是非空数组,且上下文没有读法记号 permissionsAllowReading = "statement"。
|
|
54
|
+
SELECT 'permissions-allow-list' AS criterion, count(*) AS hit_rows,
|
|
55
|
+
coalesce(string_agg(k.session_id, ',' ORDER BY k.created_at_ms DESC), '') AS sessions
|
|
56
|
+
FROM checkpoint k
|
|
57
|
+
JOIN checkpoint_ctx c ON c.session_id = k.session_id
|
|
58
|
+
WHERE k.status = 'pending'
|
|
59
|
+
AND jsonb_typeof(c.ctx #> '{body,settings,permissions,allow}') = 'array'
|
|
60
|
+
AND (c.ctx #> '{body,settings,permissions,allow}') <> '[]'::jsonb
|
|
61
|
+
AND (c.ctx -> 'permissionsAllowReading') IS DISTINCT FROM '"statement"'::jsonb;
|
|
62
|
+
|
|
63
|
+
-- drain-check:retired-mode-seed:pg —— 期望 hit_rows = 0;每一行 = 一条升级后会被拒的停放行。
|
|
64
|
+
-- 判据逐字照引擎 refuseRetiredModeSeed:inheritedGate 带 autoModeRequested / approverPosture(任何值,含 JSON null),或 shellGate 不是 "always"(含 JSON null)。
|
|
65
|
+
SELECT 'retired-mode-seed' AS criterion, count(*) AS hit_rows,
|
|
66
|
+
coalesce(string_agg(k.session_id, ',' ORDER BY k.created_at_ms DESC), '') AS sessions
|
|
67
|
+
FROM checkpoint k
|
|
68
|
+
WHERE k.status = 'pending'
|
|
69
|
+
AND ((k.checkpoint #> '{state,inheritedGate}') ?| array['autoModeRequested', 'approverPosture']
|
|
70
|
+
OR (jsonb_typeof(k.checkpoint #> '{state,inheritedGate,shellGate}') IS NOT NULL
|
|
71
|
+
AND (k.checkpoint #> '{state,inheritedGate,shellGate}') <> '"always"'::jsonb));
|