@sema-agent/server 7.114.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.
Files changed (159) hide show
  1. package/MIGRATION.md +237 -2
  2. package/USAGE.md +39 -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-ask-audit-store.d.ts +25 -6
  10. package/dist/approval-ask-audit-store.js +6 -1
  11. package/dist/approval-card.d.ts +25 -24
  12. package/dist/approval-card.js +14 -0
  13. package/dist/approval-crash-convergence.d.ts +60 -0
  14. package/dist/approval-crash-convergence.js +75 -0
  15. package/dist/approval-deny-reasons.d.ts +13 -0
  16. package/dist/approval-deny-reasons.js +2 -0
  17. package/dist/approval.js +7 -3
  18. package/dist/boot/coordinators.js +0 -12
  19. package/dist/boot/execution-env.js +2 -1
  20. package/dist/boot/exit-boundary.d.ts +3 -2
  21. package/dist/boot/exit-boundary.js +3 -0
  22. package/dist/boot/resolve-spec.js +5 -1
  23. package/dist/boot/shutdown.d.ts +19 -0
  24. package/dist/boot/shutdown.js +12 -1
  25. package/dist/boot/stage-07-capability-layer.js +21 -15
  26. package/dist/boot/stores.js +4 -4
  27. package/dist/capabilities/composite-members.d.ts +1 -0
  28. package/dist/capabilities/composite-members.js +1 -0
  29. package/dist/capabilities/hands-lane.d.ts +0 -23
  30. package/dist/capabilities/hands-lane.js +3 -2
  31. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  32. package/dist/capabilities/send-user-file-tool.js +1 -8
  33. package/dist/capability-facts.d.ts +296 -0
  34. package/dist/capability-facts.js +354 -0
  35. package/dist/config-catalog.js +2 -1
  36. package/dist/config-center/apply-effective.js +11 -11
  37. package/dist/config-provider.d.ts +16 -0
  38. package/dist/config-provider.js +11 -2
  39. package/dist/config-types.d.ts +20 -1
  40. package/dist/config.d.ts +55 -29
  41. package/dist/config.js +48 -25
  42. package/dist/deployment-governance.d.ts +15 -4
  43. package/dist/deployment-governance.js +20 -2
  44. package/dist/fleet/fleet-bus.d.ts +1 -22
  45. package/dist/hooks/hook-runner.d.ts +29 -3
  46. package/dist/hooks/hook-runner.js +30 -17
  47. package/dist/http/admission.d.ts +6 -0
  48. package/dist/http/admission.js +29 -7
  49. package/dist/http/approval-sweeps.js +20 -0
  50. package/dist/http/checkpoint-ctx.d.ts +68 -0
  51. package/dist/http/checkpoint-ctx.js +36 -0
  52. package/dist/http/durable-run-leg.js +2 -1
  53. package/dist/http/principal-gate.d.ts +3 -0
  54. package/dist/http/principal-gate.js +3 -0
  55. package/dist/http/resume-legs.js +19 -1
  56. package/dist/http/retired-skill-manifest.d.ts +23 -0
  57. package/dist/http/retired-skill-manifest.js +22 -0
  58. package/dist/http/route-ctx.d.ts +7 -0
  59. package/dist/http/route-table.d.ts +3 -2
  60. package/dist/http/route-table.js +5 -5
  61. package/dist/http/routes/a2a-serve.d.ts +1 -3
  62. package/dist/http/routes/a2a-serve.js +14 -23
  63. package/dist/http/routes/admin-config-refresh.js +7 -2
  64. package/dist/http/routes/admin-drain.js +7 -2
  65. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  66. package/dist/http/routes/admin-memory-optout.js +20 -7
  67. package/dist/http/routes/adoption.js +19 -5
  68. package/dist/http/routes/agents-roster.js +4 -2
  69. package/dist/http/routes/approvals-assistant.js +41 -18
  70. package/dist/http/routes/background.d.ts +0 -2
  71. package/dist/http/routes/background.js +4 -5
  72. package/dist/http/routes/capabilities.d.ts +2 -1
  73. package/dist/http/routes/capabilities.js +63 -65
  74. package/dist/http/routes/config-catalog.d.ts +4 -5
  75. package/dist/http/routes/config-catalog.js +16 -2
  76. package/dist/http/routes/devices.js +17 -11
  77. package/dist/http/routes/diagnostics.d.ts +5 -0
  78. package/dist/http/routes/diagnostics.js +8 -1
  79. package/dist/http/routes/fleet.js +4 -2
  80. package/dist/http/routes/images.js +30 -3
  81. package/dist/http/routes/leader.js +7 -1
  82. package/dist/http/routes/mcp-reconnect.js +17 -10
  83. package/dist/http/routes/memory-bundle.js +20 -6
  84. package/dist/http/routes/memory-compliance.js +19 -5
  85. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  86. package/dist/http/routes/memory-consolidation.js +17 -3
  87. package/dist/http/routes/memory-origin.js +19 -5
  88. package/dist/http/routes/notify-wake.js +5 -3
  89. package/dist/http/routes/observability.js +19 -5
  90. package/dist/http/routes/retention-ops.js +6 -1
  91. package/dist/http/routes/rules.js +49 -21
  92. package/dist/http/routes/run-memory-capture.js +5 -2
  93. package/dist/http/routes/runs.js +90 -60
  94. package/dist/http/routes/session-sync.js +64 -53
  95. package/dist/http/routes/sessions-list.js +8 -5
  96. package/dist/http/routes/sessions.js +40 -25
  97. package/dist/http/routes/shared-memory.js +5 -3
  98. package/dist/http/routes/tasks.js +5 -3
  99. package/dist/http/routes/trace-usage.js +18 -11
  100. package/dist/http/routes/workflows.js +36 -13
  101. package/dist/http/server.js +4 -1
  102. package/dist/http/wire-types.d.ts +6 -0
  103. package/dist/index.d.ts +1 -1
  104. package/dist/leader/wire.js +2 -1
  105. package/dist/local-data-root.d.ts +1 -1
  106. package/dist/mcp-content-origin.d.ts +0 -8
  107. package/dist/model-entry-refusal.d.ts +10 -2
  108. package/dist/model-entry-refusal.js +19 -3
  109. package/dist/namespaced-rule-name.d.ts +54 -0
  110. package/dist/namespaced-rule-name.js +33 -0
  111. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  112. package/dist/observability/corrupt-read-seat.js +4 -3
  113. package/dist/observability/fail-open.d.ts +10 -9
  114. package/dist/observability/fail-open.js +5 -1
  115. package/dist/observability/permission-rule-events.d.ts +3 -0
  116. package/dist/observability/permission-rule-events.js +4 -0
  117. package/dist/permission-rule-list.d.ts +81 -0
  118. package/dist/permission-rule-list.js +17 -0
  119. package/dist/plugins/file-run-store.d.ts +7 -1
  120. package/dist/plugins/file-run-store.js +20 -7
  121. package/dist/plugins/memory-run-store.js +2 -1
  122. package/dist/plugins/one-time-migrations.d.ts +4 -2
  123. package/dist/plugins/one-time-migrations.js +26 -3
  124. package/dist/plugins/pg-session-storage.js +1 -1
  125. package/dist/plugins/run-store-sql.js +4 -4
  126. package/dist/plugins/session-policy-store-sql.js +4 -2
  127. package/dist/plugins/store-backend.d.ts +6 -1
  128. package/dist/plugins/store-backend.js +2 -1
  129. package/dist/plugins/store-contracts.d.ts +27 -8
  130. package/dist/plugins/store-contracts.js +12 -3
  131. package/dist/plugins/tidb-session-store.js +1 -1
  132. package/dist/request-key-closure.js +1 -1
  133. package/dist/run-local.d.ts +37 -3
  134. package/dist/run-local.js +52 -6
  135. package/dist/runs.d.ts +0 -58
  136. package/dist/runs.js +15 -20
  137. package/dist/runtime-governance.d.ts +21 -1
  138. package/dist/runtime-governance.js +6 -1
  139. package/dist/security.js +2 -2
  140. package/dist/session-policy-wire.d.ts +40 -59
  141. package/dist/session-policy-wire.js +60 -32
  142. package/dist/spec-fields.d.ts +32 -0
  143. package/dist/spec-fields.js +31 -16
  144. package/dist/standing-answer-grant.d.ts +8 -0
  145. package/dist/standing-answer-grant.js +5 -0
  146. package/dist/task-settings.d.ts +17 -6
  147. package/dist/task-settings.js +16 -30
  148. package/dist/tool-approval.d.ts +32 -4
  149. package/dist/tool-approval.js +31 -8
  150. package/dist/trace/core-keyset-guard.d.ts +13 -8
  151. package/dist/trace/project.js +9 -0
  152. package/dist/trace/self-minted-wire.d.ts +42 -0
  153. package/dist/trace/self-minted-wire.js +13 -0
  154. package/dist/user-input-ledger.d.ts +8 -5
  155. package/package.json +6 -4
  156. package/scripts/drain-check-local.mjs +528 -0
  157. package/scripts/drain-check.mysql.sql +67 -0
  158. package/scripts/drain-check.pg.sql +71 -0
  159. package/scripts/retired-mode-seed-drain-local.mjs +0 -317
@@ -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));
@@ -1,317 +0,0 @@
1
- #!/usr/bin/env node
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)。
5
- *
6
- * ## 为什么是一只脚本,不是一行 grep
7
- * ① 判据有三条成员(core `refuseRetiredModeSeed`):`autoModeRequested` / `approverPosture` **任何值**(含 JSON null)、
8
- * `shellGate` **不是 `"always"`**(含 JSON null)。`"always"` 是 7.34.0 起的合法新形,grep 表达不了「值不是 always」;
9
- * ② core 的 File 店是**快照 + 账本**两只文件(`checkpoints/checkpoints.snapshot.jsonl` 每行一条行,`checkpoints/checkpoints.jsonl` 是快照之后的事件):
10
- * 压实之后旧行**只在快照里**,只扫账本会把最该排的那批(≤7.105.0 停放的老行)整批漏掉;
11
- * ③ 账本是追加写:同一条行先 `put` 后 `resolve` / `expire` / `reopen`,只有折叠到**当前状态**才知道它还 pending 没有。
12
- * ⇒ 本脚本照 core 的同一组事件把两只文件折成当前行,再按上面三条成员判 pending 行。
13
- *
14
- * ## 与 core 同判 —— 判据坐标(core 7.39.1 安装树 `node_modules/@sema-agent/core/dist/`;core 改了这些,本脚本要跟着改)
15
- * · 行形:`stores/file/checkpoint-store.js:61` `isCheckpointRow`(`token` / `scope` / `status` 三个串)。快照行(`:70` `judgeRow`)与账本 `put` 的 `cp`(`:71-78` `judgeEvent`)
16
- * **同一只判**。不合形的记录 core **跳过并披露、保留旧行**(`stores/file/fs-atomic.js:122` `readJsonlRecords`)—— 升级后那张旧卡照样在、照样被拒;所以本脚本对不合形的 `put`
17
- * **不覆盖**旧行、计入 `corrupt`(⇒ rc 2),旧卡仍被数到;不合形的快照行(含缺 `scope`)不当行数、计入 `corrupt`。
18
- * · 事件词:`stores/file/checkpoint-store.js:7-14` 注册表六词(put / resolve / reopen / execution / expire / steer_append);不认的词 core **拒绝重放**
19
- * (`:16-19` `applyCheckpointEvent` 抛 `ledger_event_unknown`)⇒ 本脚本计入 `unknownEvents`(⇒ rc 2),不替它猜。
20
- * · 重放序:`stores/file/shared-ledger.js:20-31` 先快照、后账本。压实 `stores/file/shared-ledger.js:63-72` `compact()` 先**原子换新快照**、再**清空账本**。
21
- * · 退役成员:`core/checkpoint-store.js:739-756` `RETIRED_MODE_SEED_MEMBERS` / `refuseRetiredModeSeed`。
22
- *
23
- * ## 只读、不取锁;服务运行中也可以跑,但脚本只对**一致的**快照 + 账本组合下结论
24
- * 不打开 core 的店(那会取 `checkpoints/LOCK`、与在跑的服务争锁),只读两只 JSONL。服务在写时,两次读之间可能发生压实 ——
25
- * 先读快照(旧)、压实(新快照含账本里的行,账本清空)、再读账本(空)⇒ 只在账本里的 pending 退役卡两边都读不到,读出一个假的「干净」。
26
- * 所以读前读后各取两文件的身份(dev / ino / size / mtimeNs),任一变了(压实 = 原子替换 ⇒ ino 变;追加 ⇒ size 变)就**重读**,至多 3 次;
27
- * 仍不稳 ⇒ rc 2 + 一句话(服务在写:停写或重跑),不给结论。
28
- *
29
- * ## 数据根
30
- * · 带参:`node scripts/retired-mode-seed-drain-local.mjs <数据根>` —— 读那个目录;
31
- * · 不带参:数据根 = **服务自己的那只解析**(`dist/local-data-root.js` 的 `resolveLocalDataRoot`,`loadConfig` 调的同一只函数,脚本不自抄那条链),
32
- * 所以要在**与服务同一份 env**(同样的数据根相关 env、同一个 home)下跑,读到的才是服务用的那个目录;末行 `root=` 回显实际读了哪个目录。
33
- * 随包形里 `dist/` 在脚本隔壁;源码树没 build 过又没给根 ⇒ rc 2 说明(见下)。
34
- * · **数据根缺席 ≠ 干净**:根目录不存在 / 不是目录 / 根下没有 `checkpoints/`(core File 店在后端构造时就建这个目录,所以它是「这里跑过 local 后端」的标记;
35
- * 根 `LOCK` 是 pidfile、干净退出即删,不能当标记)⇒ rc 2,不给结论 —— 路径打错、env 没对上服务,都会读到一个什么都没有的目录。
36
- *
37
- * ## 读数(机读末行 + 退出码)
38
- * · 每条命中一行:`RETIRED-ROW token=<token> sessionId=<id> createdAt=<ms> member=<成员>`;
39
- * · 末行恒有:`RETIRED-MODE-SEED rows=<命中数> pending=<pending 行数> total=<行数> unknownEvents=<n> corrupt=<n> root=<实际读的数据根>`;
40
- * 没读成 / 不能下结论(rc 2 的几种缺席与不稳形)时前五个计数是 `-`、多一个 `reason=<词>`,**不会出现 `rows=0`**(下游读 `rows=0` 即「干净」);
41
- * · rc 0 = 零命中(可换钉)· rc 1 = 有命中(逐行列出;答完或取消再跑)· rc 2 = 读不懂 / 没读成 / 不能下结论(账本里有本脚本不认的事件词 / 不合形的记录 /
42
- * 数据根缺席 / 缺省根解析不可用 / 读期间文件一直在变 / 读文件出错 / 用法错)—— 不给结论(缺席 ≠ 干净:一条读不懂的记录可能正是让某行回到 pending 的那一条)。
43
- * rc 2 与 rc 1 同时成立时(有命中、也有读不懂的记录)报 rc 2,命中行照列。
44
- */
45
- import { readFileSync, realpathSync, statSync } from "node:fs";
46
- import { homedir } from "node:os";
47
- import { join } from "node:path";
48
- import { fileURLToPath } from "node:url";
49
-
50
- const TAIL_PREFIX = "RETIRED-MODE-SEED";
51
- /** 读期间文件在变时的有界重读次数(含第一次)。 */
52
- const STABLE_READ_ATTEMPTS = 3;
53
- /** core 账本事件注册表(`stores/file/checkpoint-store.js:7-14`)。 */
54
- const KNOWN_LEDGER_EVENTS = new Set(["put", "resolve", "reopen", "execution", "expire", "steer_append"]);
55
-
56
- /** @param {string} path */
57
- const dirKind = (path) => {
58
- try {
59
- return statSync(path).isDirectory() ? "dir" : "file";
60
- } catch {
61
- return "absent";
62
- }
63
- };
64
-
65
- /** 文件全文;不存在 = 空(与 core `readJsonlRecords` 的 ENOENT 臂同);其余错误抛出(由 {@link drainReport} 转 rc 2)。
66
- * @param {string} path */
67
- const textOf = (path) => {
68
- try {
69
- return readFileSync(path, "utf8");
70
- } catch (e) {
71
- if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") return "";
72
- throw e;
73
- }
74
- };
75
-
76
- /** @param {string} path */
77
- const linesOf = (path) => textOf(path).split("\n").filter((l) => l.trim().length > 0);
78
-
79
- /** @param {string} line */
80
- const parse = (line) => {
81
- try {
82
- const v = JSON.parse(line);
83
- return v !== null && typeof v === "object" && !Array.isArray(v) ? v : undefined;
84
- } catch {
85
- return undefined;
86
- }
87
- };
88
-
89
- /** core `isCheckpointRow`(`stores/file/checkpoint-store.js:61`)逐字:`token` / `scope` / `status` 三个串。快照行与账本 `put` 的 `cp` 同一只判。
90
- * @param {unknown} x
91
- * @returns {x is Record<string, unknown> & { token: string, scope: string, status: string }} */
92
- const isCheckpointRow = (x) =>
93
- x !== null && typeof x === "object" && typeof (/** @type {Record<string, unknown>} */ (x).token) === "string" && typeof (/** @type {Record<string, unknown>} */ (x).scope) === "string" && typeof (/** @type {Record<string, unknown>} */ (x).status) === "string";
94
-
95
- /** 一只文件的身份;不存在 = `absent`(账本可以合法不存在);其余 stat 错误抛出。
96
- * @param {string} path */
97
- const identityOf = (path) => {
98
- try {
99
- const st = statSync(path, { bigint: true });
100
- return `${st.dev}:${st.ino}:${st.size}:${st.mtimeNs}`;
101
- } catch (e) {
102
- if (/** @type {NodeJS.ErrnoException} */ (e).code === "ENOENT") return "absent";
103
- throw e;
104
- }
105
- };
106
-
107
- /** 两只文件的身份(快照 | 账本)。
108
- * @param {string} dir */
109
- const identitiesOf = (dir) => `${identityOf(join(dir, "checkpoints.snapshot.jsonl"))}|${identityOf(join(dir, "checkpoints.jsonl"))}`;
110
-
111
- /**
112
- * 读一遍(不判一致性):快照 → 钩子 → 账本,折成当前行。
113
- * @param {string} dir
114
- * @param {() => void | Promise<void>} betweenReads
115
- * @returns {Promise<{ rows: Map<string, Record<string, unknown>>, corrupt: number, unknownEvents: number }>}
116
- */
117
- async function foldOnce(dir, betweenReads) {
118
- /** @type {Map<string, Record<string, unknown>>} token → 行(当前状态) */
119
- const rows = new Map();
120
- let corrupt = 0;
121
- let unknownEvents = 0;
122
-
123
- // ① 快照:每行一条行;不合形(core `isCheckpointRow`)⇒ core 跳过 ⇒ 本脚本也不当行数,计 corrupt
124
- for (const line of linesOf(join(dir, "checkpoints.snapshot.jsonl"))) {
125
- const r = parse(line);
126
- if (r === undefined || !isCheckpointRow(r)) {
127
- corrupt++;
128
- continue;
129
- }
130
- rows.set(r.token, r);
131
- }
132
-
133
- await betweenReads();
134
-
135
- // ② 账本:快照之后的事件,照 core `applyCheckpointEvent` 的同一组词折叠(状态相关的三词改 status;其余两词与 pending 判定无关)
136
- for (const line of linesOf(join(dir, "checkpoints.jsonl"))) {
137
- const ev = parse(line);
138
- if (ev === undefined || typeof ev.t !== "string") {
139
- corrupt++;
140
- continue;
141
- }
142
- if (!KNOWN_LEDGER_EVENTS.has(ev.t)) {
143
- unknownEvents++; // 新版本写的事件词:本脚本不替它猜(core 对未知词拒绝重放)
144
- continue;
145
- }
146
- if (typeof ev.token !== "string") {
147
- corrupt++;
148
- continue;
149
- }
150
- if (ev.t === "put") {
151
- // core:`put` 的 cp 不合形 ⇒ 跳过该事件、保留旧行。本脚本同:不覆盖,计 corrupt(⇒ rc 2),旧行照数。
152
- if (!isCheckpointRow(ev.cp)) {
153
- corrupt++;
154
- continue;
155
- }
156
- rows.set(ev.token, ev.cp);
157
- continue;
158
- }
159
- const row = rows.get(ev.token);
160
- switch (ev.t) {
161
- case "resolve":
162
- if (row) row.status = "resolved";
163
- break;
164
- case "reopen":
165
- if (row) row.status = "pending";
166
- break;
167
- case "expire":
168
- if (row) row.status = "expired";
169
- break;
170
- default:
171
- break; // execution / steer_append:不改 status
172
- }
173
- }
174
- return { rows, corrupt, unknownEvents };
175
- }
176
-
177
- /**
178
- * 把 `<dir>` 下的快照 + 账本折成当前行,**只对一致的组合交结果**(导出:测试与 CLI 共用同一只;CLI 不传钩子)。
179
- * 读前读后各取两文件身份,变了就重读,至多 {@link STABLE_READ_ATTEMPTS} 次;仍变 ⇒ `stable:false`(不交行)。
180
- * @param {string} dir `<数据根>/checkpoints`
181
- * @param {{ afterSnapshotRead?: (attempt: number) => void | Promise<void> }} [opts] 测试钩子:快照读完、账本还没读的那一刻(`attempt` 从 1 起)
182
- * @returns {Promise<{ stable: true, attempts: number, rows: Map<string, Record<string, unknown>>, corrupt: number, unknownEvents: number } | { stable: false, attempts: number }>}
183
- */
184
- export async function scanCheckpointDir(dir, opts = {}) {
185
- for (let attempt = 1; attempt <= STABLE_READ_ATTEMPTS; attempt++) {
186
- const before = identitiesOf(dir);
187
- const folded = await foldOnce(dir, async () => {
188
- await opts.afterSnapshotRead?.(attempt);
189
- });
190
- if (identitiesOf(dir) === before) return { stable: true, attempts: attempt, ...folded };
191
- }
192
- return { stable: false, attempts: STABLE_READ_ATTEMPTS };
193
- }
194
-
195
- /** core `refuseRetiredModeSeed` 的三条成员(`core/checkpoint-store.js:739-756` 逐字):前两条任何值都算(含 null),`shellGate` 不是 "always" 才算。
196
- * @param {unknown} gate
197
- * @returns {string | undefined} */
198
- function retiredMember(gate) {
199
- if (gate === null || typeof gate !== "object") return undefined;
200
- const g = /** @type {Record<string, unknown>} */ (gate);
201
- if (g.autoModeRequested !== undefined) return "autoModeRequested";
202
- if (g.approverPosture !== undefined) return "approverPosture";
203
- if (g.shellGate !== undefined && g.shellGate !== "always") return "shellGate";
204
- return undefined;
205
- }
206
-
207
- /** 没读成 / 不能下结论的报告:一句人话进 stderr、机读末行进 stdout(计数全 `-`,不出现 `rows=0`),rc 2。
208
- * @param {string} reason 机读词
209
- * @param {string} sentence
210
- * @param {string} rootEcho */
211
- 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] };
213
- }
214
-
215
- /**
216
- * 一次排空检查的完整报告(导出:CLI 与测试共用)。
217
- * @param {string} root 数据根
218
- * @param {{ afterSnapshotRead?: (attempt: number) => void | Promise<void> }} [opts]
219
- * @returns {Promise<{ rc: 0 | 1 | 2, stdout: string[], stderr: string[] }>}
220
- */
221
- export async function drainReport(root, opts = {}) {
222
- const dir = join(root, "checkpoints");
223
- switch (dirKind(root)) {
224
- case "absent":
225
- return unreadableReport("root-absent", `data root ${root} does not exist — no verdict (absent is not clean): check that this process has the same data-root env as the service, or pass the data root as the argument`, root);
226
- case "file":
227
- return unreadableReport("root-not-a-directory", `data root ${root} is not a directory — no verdict: pass the service's data root`, root);
228
- case "dir":
229
- break;
230
- }
231
- if (dirKind(dir) !== "dir") {
232
- return unreadableReport(
233
- "no-checkpoints-dir",
234
- `no checkpoints/ directory under ${root} — no verdict (absent is not clean): this is not the data root of a local-backend service that has booted (the service creates checkpoints/ when it starts); check that this process has the same data-root env as the service, or pass the data root as the argument`,
235
- root,
236
- );
237
- }
238
- let scan;
239
- try {
240
- scan = await scanCheckpointDir(dir, opts);
241
- } catch (e) {
242
- // 读文件出错(权限 / 路径是目录 / …)⇒ 不给结论。未捕获异常的退出码是 1 = 「有命中」,会被下游读成有停放行。
243
- return unreadableReport("read-error", `cannot read the checkpoint files under ${dir}: ${e instanceof Error ? e.message : String(e)} — no verdict`, root);
244
- }
245
- if (!scan.stable) {
246
- return unreadableReport(
247
- "unstable-read",
248
- `the checkpoint files under ${dir} changed while being read (${scan.attempts} reads in a row) — the service is writing to its checkpoint store, so no consistent snapshot + ledger could be read: no verdict; stop the service (or its checkpoint traffic) and rerun`,
249
- root,
250
- );
251
- }
252
- const { rows, corrupt, unknownEvents } = scan;
253
- 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: [] };
267
- }
268
-
269
- /** CLI 入口:解析实参与缺省根,打印报告,按报告的 rc 退出。 */
270
- async function main() {
271
- const usage = "usage: retired-mode-seed-drain-local.mjs [<data root>]";
272
- const args = process.argv.slice(2);
273
- /** @param {{ rc: number, stdout: string[], stderr: string[] }} r */
274
- const finish = (r) => {
275
- for (const l of r.stderr) process.stderr.write(`${l}\n`);
276
- for (const l of r.stdout) process.stdout.write(`${l}\n`);
277
- process.exit(r.rc);
278
- };
279
- if (args.length > 1 || (args[0] !== undefined && args[0].startsWith("-"))) finish(unreadableReport("usage", usage, "-"));
280
- /** @type {string} */
281
- let root;
282
- if (args[0] !== undefined) {
283
- root = args[0];
284
- } else {
285
- try {
286
- const { resolveLocalDataRoot } = await import("../dist/local-data-root.js");
287
- root = resolveLocalDataRoot(process.env, homedir());
288
- } catch (e) {
289
- finish(
290
- unreadableReport(
291
- "default-root-unavailable",
292
- `cannot resolve the default data root (dist/local-data-root.js next to scripts/ is not loadable: ${e instanceof Error ? e.message : String(e)}) — run the copy inside an installed @sema-agent/server package (or after \`npm run build\`), or pass the data root as the argument`,
293
- "-",
294
- ),
295
- );
296
- }
297
- }
298
- finish(await drainReport(root));
299
- }
300
-
301
- // 作脚本跑才进 CLI;被 import(测试)时只导出函数。
302
- // 7.113.0 合并复审的修复验证两腿同根发现(主会话按「门轴新代码 fail-closed」收口):入口判定**自己不许失败成「什么都不跑、rc 0」**
303
- // (rc 0 = 「干净,可换钉」)。两边都取 realpath 再比(`--preserve-symlinks-main` 下 `import.meta.url` 留软链路径、argv[1] 解析成真路径 ⇒ 修前
304
- // 两边不等 ⇒ main 不跑 ⇒ 静默 rc 0);argv[1] 缺席 / 为 `-`(stdin 导入方)/ 解析不到 ⇒ 当 import,不抛(修前 realpathSync 对 `-` 抛 ENOENT);
305
- // Node ≥ 22.18 有 `import.meta.main`,在场就以它为准(平台自己的判定,比路径比对可靠)。
306
- function invokedAsMain() {
307
- const metaMain = /** @type {{ main?: unknown }} */ (import.meta).main;
308
- if (typeof metaMain === "boolean") return metaMain;
309
- const arg = process.argv[1];
310
- if (arg === undefined || arg === "-" || arg === "") return false;
311
- try {
312
- return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(arg);
313
- } catch {
314
- return false;
315
- }
316
- }
317
- if (invokedAsMain()) await main();