@sema-agent/server 7.113.0 → 7.115.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.
Files changed (87) hide show
  1. package/MIGRATION.md +109 -2
  2. package/USAGE.md +36 -3
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-card.d.ts +21 -24
  10. package/dist/approval-card.js +12 -0
  11. package/dist/approval.js +5 -2
  12. package/dist/boot/exit-boundary.d.ts +3 -2
  13. package/dist/boot/exit-boundary.js +3 -0
  14. package/dist/boot/memory-boundary.d.ts +2 -1
  15. package/dist/boot/memory-consolidation.d.ts +1 -1
  16. package/dist/boot/resolve-spec.js +6 -3
  17. package/dist/boot/shutdown.d.ts +19 -0
  18. package/dist/boot/shutdown.js +12 -1
  19. package/dist/capabilities/composite-members.d.ts +1 -0
  20. package/dist/capabilities/composite-members.js +1 -0
  21. package/dist/config-catalog.js +8 -6
  22. package/dist/config-center/apply-effective.js +11 -11
  23. package/dist/config-invariants.d.ts +27 -0
  24. package/dist/config-invariants.js +54 -39
  25. package/dist/config-provider.d.ts +16 -0
  26. package/dist/config-provider.js +11 -2
  27. package/dist/config-types.d.ts +24 -11
  28. package/dist/config-types.js +0 -1
  29. package/dist/config.d.ts +41 -34
  30. package/dist/config.js +78 -81
  31. package/dist/deployment-governance.d.ts +15 -4
  32. package/dist/deployment-governance.js +20 -2
  33. package/dist/env-facts.d.ts +3 -1
  34. package/dist/env-facts.js +5 -1
  35. package/dist/execution-lane-caps.d.ts +54 -30
  36. package/dist/execution-lane-caps.js +48 -20
  37. package/dist/fleet/fleet-bus.d.ts +1 -22
  38. package/dist/hooks/hook-runner.d.ts +112 -6
  39. package/dist/hooks/hook-runner.js +138 -32
  40. package/dist/hosted-posture.d.ts +2 -2
  41. package/dist/http/admission.js +9 -4
  42. package/dist/http/approval-sweeps.js +20 -0
  43. package/dist/http/checkpoint-ctx.d.ts +68 -0
  44. package/dist/http/checkpoint-ctx.js +36 -0
  45. package/dist/http/durable-run-leg.js +2 -1
  46. package/dist/http/resume-legs.js +9 -0
  47. package/dist/http/route-ctx.d.ts +7 -0
  48. package/dist/http/routes/a2a-serve.d.ts +1 -3
  49. package/dist/http/routes/a2a-serve.js +0 -2
  50. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  51. package/dist/http/routes/admin-memory-optout.js +0 -1
  52. package/dist/http/routes/capabilities.js +3 -2
  53. package/dist/http/routes/diagnostics.d.ts +5 -0
  54. package/dist/http/routes/diagnostics.js +2 -0
  55. package/dist/http/routes/sessions.js +1 -1
  56. package/dist/http/routes/tasks.js +3 -2
  57. package/dist/index.d.ts +1 -1
  58. package/dist/leader/wire.js +2 -1
  59. package/dist/local-data-root.d.ts +1 -1
  60. package/dist/mcp-content-origin.d.ts +0 -8
  61. package/dist/model-entry-refusal.d.ts +10 -2
  62. package/dist/model-entry-refusal.js +19 -3
  63. package/dist/observability/corrupt-read-seat.d.ts +3 -3
  64. package/dist/observability/corrupt-read-seat.js +5 -4
  65. package/dist/observability/fail-open.d.ts +10 -9
  66. package/dist/observability/fail-open.js +5 -1
  67. package/dist/permission-rule-list.d.ts +81 -0
  68. package/dist/permission-rule-list.js +17 -0
  69. package/dist/plugins/remote-env-host.d.ts +0 -35
  70. package/dist/plugins/remote-env-host.js +0 -8
  71. package/dist/plugins/remote-env-local-docker.d.ts +5 -13
  72. package/dist/plugins/remote-env-local-docker.js +2 -4
  73. package/dist/run-local.js +4 -1
  74. package/dist/runtime-governance.d.ts +21 -1
  75. package/dist/runtime-governance.js +6 -1
  76. package/dist/security.js +2 -2
  77. package/dist/session-policy-wire.d.ts +5 -4
  78. package/dist/store-posture.d.ts +7 -7
  79. package/dist/store-posture.js +0 -1
  80. package/dist/task-settings.d.ts +19 -5
  81. package/dist/task-settings.js +19 -30
  82. package/dist/tool-approval.d.ts +16 -4
  83. package/dist/tool-approval.js +2 -1
  84. package/dist/trace/core-keyset-guard.d.ts +9 -4
  85. package/dist/trace/project.js +9 -0
  86. package/package.json +4 -4
  87. package/scripts/{retired-mode-seed-drain-local.mjs → drain-check-local.mjs} +147 -26
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * 换钉 7.113.0(core 7.39.0 删掉退役 mode-seed 的一次性迁移)前的**排空检查 —— `DB_BACKEND=local`(File 店)车道**,只读。
4
- * 随 npm 包出(`scripts/retired-mode-seed-drain-local.mjs`;SQL 车道的同一判据是 `docs/DEPLOY-PREREQS.md` 同名节的两条只读 SQL)。
3
+ * 换钉前的**排空检查 —— `DB_BACKEND=local`(File 店)车道**,只读。随 npm 包出(`scripts/drain-check-local.mjs`)。
4
+ * 一次跑**两条判据**(判据表 {@link DRAIN_CRITERIA},每条一行末行、各自的命中行;SQL 车道的同一判据是 `docs/DEPLOY-PREREQS.md` 各节的只读 SQL):
5
+ * · `retired-mode-seed`(换钉 7.113.0 前,core 7.39.0 删掉退役 mode-seed 的一次性迁移):pending 行的 `state.inheritedGate` 仍带退役成员;
6
+ * · `permissions-allow-list`(换钉 7.115.0 前,本服务把 `settings.permissions.allow` 从名单改成 allow 规则):pending 行所在会话的续跑上下文
7
+ * (`checkpoint-ctx/*.json` 的信封 `{ sessionId, updatedAt, ctx }`)里,`ctx.body.settings.permissions.allow` 是非空数组,且 `ctx.permissionsAllowReading` 不是 `"statement"`
8
+ * —— 这样的行在 7.115.0 上续跑会被拒(行字节不动;出路按停驻种类分,见随包 `MIGRATION.md` 7.115.0 节)。判据**就是**服务续跑腿拒臂的那一只函数(`src/http/checkpoint-ctx.ts` 的 `allowListReadingUnstated`),
9
+ * 经包里脚本隔壁的编译产物 `dist/http/checkpoint-ctx.js` 取(与缺省数据根经 `dist/local-data-root.js` 取同一姿势;该模块只有类型 import,编出来零运行期依赖)—— 本脚本不写第二份。
10
+ * 扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)两条判据共用;判据只回答「这一条 pending 行算不算命中」。
5
11
  *
6
12
  * ## 为什么是一只脚本,不是一行 grep
7
13
  * ① 判据有三条成员(core `refuseRetiredModeSeed`):`autoModeRequested` / `approverPosture` **任何值**(含 JSON null)、
@@ -27,7 +33,7 @@
27
33
  * 仍不稳 ⇒ rc 2 + 一句话(服务在写:停写或重跑),不给结论。
28
34
  *
29
35
  * ## 数据根
30
- * · 带参:`node scripts/retired-mode-seed-drain-local.mjs <数据根>` —— 读那个目录;
36
+ * · 带参:`node scripts/drain-check-local.mjs <数据根>` —— 读那个目录;
31
37
  * · 不带参:数据根 = **服务自己的那只解析**(`dist/local-data-root.js` 的 `resolveLocalDataRoot`,`loadConfig` 调的同一只函数,脚本不自抄那条链),
32
38
  * 所以要在**与服务同一份 env**(同样的数据根相关 env、同一个 home)下跑,读到的才是服务用的那个目录;末行 `root=` 回显实际读了哪个目录。
33
39
  * 随包形里 `dist/` 在脚本隔壁;源码树没 build 过又没给根 ⇒ rc 2 说明(见下)。
@@ -35,19 +41,21 @@
35
41
  * 根 `LOCK` 是 pidfile、干净退出即删,不能当标记)⇒ rc 2,不给结论 —— 路径打错、env 没对上服务,都会读到一个什么都没有的目录。
36
42
  *
37
43
  * ## 读数(机读末行 + 退出码)
38
- * · 每条命中一行:`RETIRED-ROW token=<token> sessionId=<id> createdAt=<ms> member=<成员>`;
39
- * · 末行恒有:`RETIRED-MODE-SEED rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`;
44
+ * · 每条命中一行:`ALLOW-LIST-ROW token=<token> sessionId=<id> createdAt=<ms>` / `RETIRED-ROW token=<token> sessionId=<id> createdAt=<ms> member=<成员>`;
45
+ * · 每条判据一行末行(表序;`RETIRED-MODE-SEED` 那一行恒在最后,7.113.0 起读「最后一行」的下游不受影响):
46
+ * `PERMISSIONS-ALLOW-LIST rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`,
47
+ * `RETIRED-MODE-SEED rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`;
48
+ * `corrupt` = 共用扫描的坏记录 + 这条判据自己读不懂的旁路输入(`permissions-allow-list`:读不懂的 ctx 档,文件名进 stderr —— 一份读不懂的 ctx 可能正是某条 pending 行带 `allow` 的那一份);
40
49
  * 没读成 / 不能下结论(rc 2 的几种缺席与不稳形)时前五个计数是 `-`、多一个 `reason=<词>`,**不会出现 `rows=0`**(下游读 `rows=0` 即「干净」);
41
- * · rc 0 = 零命中(可换钉)· rc 1 = 有命中(逐行列出;答完或取消再跑)· rc 2 = 读不懂 / 没读成 / 不能下结论(账本里有本脚本不认的事件词 / 不合形的记录 /
42
- * 数据根缺席 / 缺省根解析不可用 / 读期间文件一直在变 / 读文件出错 / 用法错)—— 不给结论(缺席 ≠ 干净:一条读不懂的记录可能正是让某行回到 pending 的那一条)。
50
+ * · rc 0 = 两条判据都零命中(可换钉)· rc 1 = 有命中(逐行列出;答完或取消再跑)· rc 2 = 读不懂 / 没读成 / 不能下结论(账本里有本脚本不认的事件词 / 不合形的记录 / 读不懂的 ctx 档 /
51
+ * 数据根缺席 / 缺省根解析不可用 / 判据模块不可用(脚本隔壁没有 `dist/`:源码树没 build)/ 读期间文件一直在变 / 读文件出错 / 用法错)—— 不给结论(缺席 ≠ 干净:一条读不懂的记录可能正是让某行回到 pending 的那一条)。
43
52
  * rc 2 与 rc 1 同时成立时(有命中、也有读不懂的记录)报 rc 2,命中行照列。
44
53
  */
45
- import { readFileSync, realpathSync, statSync } from "node:fs";
54
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
46
55
  import { homedir } from "node:os";
47
56
  import { join } from "node:path";
48
57
  import { fileURLToPath } from "node:url";
49
58
 
50
- const TAIL_PREFIX = "RETIRED-MODE-SEED";
51
59
  /** 读期间文件在变时的有界重读次数(含第一次)。 */
52
60
  const STABLE_READ_ATTEMPTS = 3;
53
61
  /** core 账本事件注册表(`stores/file/checkpoint-store.js:7-14`)。 */
@@ -204,12 +212,89 @@ function retiredMember(gate) {
204
212
  return undefined;
205
213
  }
206
214
 
207
- /** 没读成 / 不能下结论的报告:一句人话进 stderr、机读末行进 stdout(计数全 `-`,不出现 `rows=0`),rc 2。
215
+ /** @param {unknown} v */
216
+ const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
217
+
218
+ /**
219
+ * `<数据根>/checkpoint-ctx/*.json` —— 本服务 local 店的续跑上下文,一档一会话,信封 `{ sessionId, updatedAt, ctx }`(`ctx` 是对象;形判据同本服务 `LocalCheckpointStore` 的
220
+ * `CtxRecordSchema` + 对象读者)。目录不存在 = 没有 ctx(没写过);读不懂的档计入 `corrupt`(不猜它属于哪个会话)、文件名回给调用方;读期间被收割删掉的档跳过。
221
+ * @param {string} root
222
+ * @returns {{ bySession: Map<string, Record<string, unknown>>, corrupt: number, unreadable: string[] }} */
223
+ function readCtxIndex(root) {
224
+ const dir = join(root, "checkpoint-ctx");
225
+ /** @type {Map<string, Record<string, unknown>>} */
226
+ const bySession = new Map();
227
+ /** @type {string[]} */
228
+ const unreadable = [];
229
+ /** @type {string[]} */
230
+ let names;
231
+ try {
232
+ names = readdirSync(dir);
233
+ } catch (e) {
234
+ if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") return { bySession, corrupt: 0, unreadable };
235
+ throw e;
236
+ }
237
+ for (const name of names.sort()) {
238
+ if (!name.endsWith(".json")) continue;
239
+ let text;
240
+ try {
241
+ text = readFileSync(join(dir, name), "utf8");
242
+ } catch (e) {
243
+ if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") continue; // 读期间被收割删掉
244
+ throw e;
245
+ }
246
+ const rec = parse(text);
247
+ if (rec === undefined || typeof rec.sessionId !== "string" || typeof rec.updatedAt !== "number" || !isObject(rec.ctx)) {
248
+ unreadable.push(name);
249
+ continue;
250
+ }
251
+ bySession.set(rec.sessionId, /** @type {Record<string, unknown>} */ (rec.ctx));
252
+ }
253
+ return { bySession, corrupt: unreadable.length, unreadable };
254
+ }
255
+
256
+ /**
257
+ * 判据表 —— 「哪些 pending 行算命中」。扫描骨架(快照 + 账本折成当前行、一致性重读、数据根判定)两条共用,判据只管这一问:
258
+ * · `tail` / `hit`:末行前缀 / 命中行前缀;
259
+ * · `load(root, service)`:这条判据要的旁路输入(读不懂的记录计入 `corrupt`,`unreadable` 回显进 stderr);`service` = 从编译产物取来的服务判据({@link drainReport} 取一次);
260
+ * · `judge(row, input)`:命中 ⇒ 命中行的尾巴(可为空串),不命中 ⇒ `undefined`。
261
+ * 表序 = 输出序;`retired-mode-seed` 排最后 ⇒ 最后一行仍是 `RETIRED-MODE-SEED …`(7.113.0 起读最后一行的下游)。
262
+ * @type {ReadonlyArray<{ name: string, tail: string, hit: string, load: (root: string, service: { allowListReadingUnstated: (ctx: Record<string, unknown>) => boolean }) => { input: unknown, corrupt: number, unreadable: string[] }, judge: (row: Record<string, unknown>, input: unknown) => string | undefined }>}
263
+ */
264
+ export const DRAIN_CRITERIA = [
265
+ {
266
+ name: "permissions-allow-list",
267
+ tail: "PERMISSIONS-ALLOW-LIST",
268
+ hit: "ALLOW-LIST-ROW",
269
+ load: (root, service) => {
270
+ const idx = readCtxIndex(root);
271
+ return { input: { bySession: idx.bySession, unstated: service.allowListReadingUnstated }, corrupt: idx.corrupt, unreadable: idx.unreadable };
272
+ },
273
+ judge: (row, input) => {
274
+ const { bySession, unstated } = /** @type {{ bySession: Map<string, Record<string, unknown>>, unstated: (ctx: Record<string, unknown>) => boolean }} */ (input);
275
+ const ctx = typeof row.sessionId === "string" ? bySession.get(row.sessionId) : undefined;
276
+ return ctx !== undefined && unstated(ctx) ? "" : undefined;
277
+ },
278
+ },
279
+ {
280
+ name: "retired-mode-seed",
281
+ tail: "RETIRED-MODE-SEED",
282
+ hit: "RETIRED-ROW",
283
+ load: () => ({ input: undefined, corrupt: 0, unreadable: [] }),
284
+ judge: (row) => {
285
+ const state = isObject(row.state) ? /** @type {Record<string, unknown>} */ (row.state) : {};
286
+ const member = retiredMember(state.inheritedGate);
287
+ return member === undefined ? undefined : ` member=${member}`;
288
+ },
289
+ },
290
+ ];
291
+
292
+ /** 没读成 / 不能下结论的报告:一句人话进 stderr、每条判据一行机读末行进 stdout(计数全 `-`,不出现 `rows=0`),rc 2。
208
293
  * @param {string} reason 机读词
209
294
  * @param {string} sentence
210
295
  * @param {string} rootEcho */
211
296
  function unreadableReport(reason, sentence, rootEcho) {
212
- return { rc: /** @type {const} */ (2), stdout: [`${TAIL_PREFIX} rows=- pending=- total=- unknownEvents=- corrupt=- reason=${reason} root=${rootEcho}`], stderr: [sentence] };
297
+ return { rc: /** @type {const} */ (2), stdout: DRAIN_CRITERIA.map((c) => `${c.tail} rows=- pending=- total=- unknownEvents=- corrupt=- reason=${reason} root=${rootEcho}`), stderr: [sentence] };
213
298
  }
214
299
 
215
300
  /**
@@ -235,12 +320,36 @@ export async function drainReport(root, opts = {}) {
235
320
  root,
236
321
  );
237
322
  }
323
+ // 判据的唯一实现在服务里;随包形里编译产物在脚本隔壁。取不到(源码树没 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
+ // 模块能加载不等于判据在:产物被截断 / 损坏时 import 照样成功、导出缺席。判据给不出来 ⇒ 同一条「不给结论」,
336
+ // 不许在没有行可判时读成 rows=0,也不许等到调用处抛出(未捕获异常的退出码是 1 = 「有命中」)。
337
+ if (typeof service?.allowListReadingUnstated !== "function") {
338
+ return unreadableReport(
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
+ }
238
344
  let scan;
345
+ /** @type {Array<{ input: unknown, corrupt: number, unreadable: string[] }>} */
346
+ let inputs;
239
347
  try {
240
348
  scan = await scanCheckpointDir(dir, opts);
349
+ inputs = DRAIN_CRITERIA.map((c) => c.load(root, service));
241
350
  } catch (e) {
242
351
  // 读文件出错(权限 / 路径是目录 / …)⇒ 不给结论。未捕获异常的退出码是 1 = 「有命中」,会被下游读成有停放行。
243
- return unreadableReport("read-error", `cannot read the checkpoint files under ${dir}: ${e instanceof Error ? e.message : String(e)} — no verdict`, root);
352
+ return unreadableReport("read-error", `cannot read the checkpoint files under ${root}: ${e instanceof Error ? e.message : String(e)} — no verdict`, root);
244
353
  }
245
354
  if (!scan.stable) {
246
355
  return unreadableReport(
@@ -250,25 +359,37 @@ export async function drainReport(root, opts = {}) {
250
359
  );
251
360
  }
252
361
  const { rows, corrupt, unknownEvents } = scan;
362
+ /** @type {string[]} */
253
363
  const stdout = [];
254
- let hits = 0;
255
- let pending = 0;
256
- for (const [token, row] of rows) {
257
- if (row.status !== "pending") continue;
258
- pending++;
259
- const state = row.state !== null && typeof row.state === "object" ? /** @type {Record<string, unknown>} */ (row.state) : {};
260
- const member = retiredMember(state.inheritedGate);
261
- if (member === undefined) continue;
262
- hits++;
263
- stdout.push(`RETIRED-ROW token=${token} sessionId=${String(row.sessionId)} createdAt=${String(row.createdAt)} member=${member}`);
264
- }
265
- stdout.push(`${TAIL_PREFIX} rows=${hits} pending=${pending} total=${rows.size} unknownEvents=${unknownEvents} corrupt=${corrupt} root=${root}`);
266
- return { rc: unknownEvents > 0 || corrupt > 0 ? 2 : hits > 0 ? 1 : 0, stdout, stderr: [] };
364
+ /** @type {string[]} */
365
+ const stderr = [];
366
+ /** @type {string[]} */
367
+ const tails = [];
368
+ let anyHit = false;
369
+ let anyUnreadable = unknownEvents > 0 || corrupt > 0;
370
+ DRAIN_CRITERIA.forEach((criterion, i) => {
371
+ const { input, corrupt: ownCorrupt, unreadable } = inputs[i];
372
+ let hits = 0;
373
+ let pending = 0;
374
+ for (const [token, row] of rows) {
375
+ if (row.status !== "pending") continue;
376
+ pending++;
377
+ const tailOfHit = criterion.judge(row, input);
378
+ if (tailOfHit === undefined) continue;
379
+ hits++;
380
+ stdout.push(`${criterion.hit} token=${token} sessionId=${String(row.sessionId)} createdAt=${String(row.createdAt)}${tailOfHit}`);
381
+ }
382
+ if (unreadable.length > 0) stderr.push(`${criterion.name}: unreadable input files (no verdict — one of them may be what makes a pending row count): ${unreadable.join(", ")}`);
383
+ if (ownCorrupt > 0) anyUnreadable = true;
384
+ if (hits > 0) anyHit = true;
385
+ tails.push(`${criterion.tail} rows=${hits} pending=${pending} total=${rows.size} unknownEvents=${unknownEvents} corrupt=${corrupt + ownCorrupt} root=${root}`);
386
+ });
387
+ return { rc: anyUnreadable ? 2 : anyHit ? 1 : 0, stdout: [...stdout, ...tails], stderr };
267
388
  }
268
389
 
269
390
  /** CLI 入口:解析实参与缺省根,打印报告,按报告的 rc 退出。 */
270
391
  async function main() {
271
- const usage = "usage: retired-mode-seed-drain-local.mjs [<data root>]";
392
+ const usage = "usage: drain-check-local.mjs [<data root>]";
272
393
  const args = process.argv.slice(2);
273
394
  /** @param {{ rc: number, stdout: string[], stderr: string[] }} r */
274
395
  const finish = (r) => {