@sema-agent/server 7.112.0 → 7.114.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 (88) hide show
  1. package/MIGRATION.md +95 -0
  2. package/USAGE.md +4 -4
  3. package/dist/approval-ask-audit-store.d.ts +3 -4
  4. package/dist/approval-ask-audit-store.js +3 -3
  5. package/dist/approval-card.d.ts +4 -4
  6. package/dist/boot/coordinators.js +1 -1
  7. package/dist/boot/memory-boundary.d.ts +2 -1
  8. package/dist/boot/memory-consolidation.d.ts +1 -1
  9. package/dist/boot/resolve-spec.js +2 -2
  10. package/dist/boot/stores.js +4 -6
  11. package/dist/boot/workflow-orchestration.js +4 -4
  12. package/dist/config-catalog.js +6 -5
  13. package/dist/config-invariants.d.ts +27 -0
  14. package/dist/config-invariants.js +54 -39
  15. package/dist/config-types.d.ts +5 -11
  16. package/dist/config-types.js +0 -1
  17. package/dist/config.d.ts +0 -16
  18. package/dist/config.js +44 -69
  19. package/dist/env-facts.d.ts +3 -1
  20. package/dist/env-facts.js +5 -1
  21. package/dist/execution-lane-caps.d.ts +54 -30
  22. package/dist/execution-lane-caps.js +48 -20
  23. package/dist/hooks/hook-runner.d.ts +87 -7
  24. package/dist/hooks/hook-runner.js +113 -20
  25. package/dist/host-decision.d.ts +5 -3
  26. package/dist/hosted-posture.d.ts +2 -2
  27. package/dist/http/admission.js +8 -4
  28. package/dist/http/request-guards.js +2 -2
  29. package/dist/http/resume-legs.d.ts +9 -10
  30. package/dist/http/resume-legs.js +64 -18
  31. package/dist/http/route-ctx.d.ts +38 -8
  32. package/dist/http/route-ctx.js +4 -0
  33. package/dist/http/routes/background.js +2 -2
  34. package/dist/http/routes/capabilities.js +2 -2
  35. package/dist/http/routes/mcp-reconnect.js +2 -2
  36. package/dist/http/routes/notify-wake.js +3 -3
  37. package/dist/http/routes/runs.js +19 -13
  38. package/dist/http/routes/session-sync.js +6 -6
  39. package/dist/http/routes/sessions.js +20 -20
  40. package/dist/http/routes/tasks.js +1 -0
  41. package/dist/http/send.d.ts +1 -1
  42. package/dist/http/send.js +1 -1
  43. package/dist/http/wire-types.d.ts +1 -1
  44. package/dist/local-data-root.d.ts +14 -0
  45. package/dist/local-data-root.js +5 -0
  46. package/dist/observability/corrupt-read-seat.d.ts +46 -66
  47. package/dist/observability/corrupt-read-seat.js +79 -63
  48. package/dist/observability/fail-open.d.ts +28 -0
  49. package/dist/observability/fail-open.js +28 -0
  50. package/dist/observability/run-terminal.js +39 -0
  51. package/dist/orchestration/workflow-agent-session-index.d.ts +1 -5
  52. package/dist/orchestration/workflow-agent-session-index.js +2 -2
  53. package/dist/orchestration/workflow-notify-journal.d.ts +1 -3
  54. package/dist/orchestration/workflow-notify-journal.js +2 -2
  55. package/dist/plugins/approval-ask-store-file.d.ts +1 -4
  56. package/dist/plugins/approval-ask-store-file.js +2 -2
  57. package/dist/plugins/file-resume-anchor-store.d.ts +1 -6
  58. package/dist/plugins/file-resume-anchor-store.js +2 -4
  59. package/dist/plugins/file-run-store.d.ts +0 -4
  60. package/dist/plugins/file-run-store.js +1 -3
  61. package/dist/plugins/remote-env-host.d.ts +0 -35
  62. package/dist/plugins/remote-env-host.js +0 -8
  63. package/dist/plugins/remote-env-local-docker.d.ts +5 -13
  64. package/dist/plugins/remote-env-local-docker.js +2 -4
  65. package/dist/plugins/send-file-ledger.d.ts +1 -5
  66. package/dist/plugins/send-file-ledger.js +2 -2
  67. package/dist/plugins/store-backend.js +8 -9
  68. package/dist/plugins/user-input-ledger-file.d.ts +1 -5
  69. package/dist/plugins/user-input-ledger-file.js +2 -2
  70. package/dist/run-local.d.ts +1 -1
  71. package/dist/run-local.js +2 -2
  72. package/dist/runs.js +1 -0
  73. package/dist/spec-fields.js +0 -1
  74. package/dist/store-posture.d.ts +7 -7
  75. package/dist/store-posture.js +0 -1
  76. package/dist/task-settings.d.ts +13 -0
  77. package/dist/task-settings.js +3 -0
  78. package/dist/tool-approval.d.ts +1 -1
  79. package/dist/trace/core-keyset-guard.d.ts +1 -1
  80. package/dist/trace/project.js +2 -3
  81. package/dist/trace/redact.d.ts +18 -0
  82. package/dist/trace/redact.js +4 -0
  83. package/dist/unattended-permission-mode.d.ts +9 -2
  84. package/dist/unattended-permission-mode.js +5 -1
  85. package/dist/user-input-ledger.d.ts +24 -5
  86. package/dist/user-input-ledger.js +46 -11
  87. package/package.json +4 -3
  88. package/scripts/retired-mode-seed-drain-local.mjs +317 -0
@@ -1,4 +1,5 @@
1
1
  import { homedir } from "node:os";
2
+ import { EXECUTION_LANE_WORDS as DECLARABLE_LANE_WORDS, EXECUTION_LANES } from "@sema-agent/settings-schema/execution-lanes";
2
3
  const NO = "unsupported";
3
4
  const YES = "supported";
4
5
  const SU = "single-user-only";
@@ -38,7 +39,7 @@ export function executionLaneCaps(lane) {
38
39
  hostFilePlane: YES,
39
40
  coreRemoteExecutionEnv: YES,
40
41
  canonicalPathAuthority: YES,
41
- tenantSeparation: SU,
42
+ tenantSeparation: executionLaneRow(lane).tenantSeparation,
42
43
  };
43
44
  case "e2b":
44
45
  return {
@@ -56,7 +57,7 @@ export function executionLaneCaps(lane) {
56
57
  hostFilePlane: NO,
57
58
  coreRemoteExecutionEnv: YES,
58
59
  canonicalPathAuthority: NO,
59
- tenantSeparation: YES,
60
+ tenantSeparation: executionLaneRow(lane).tenantSeparation,
60
61
  };
61
62
  case "k8s":
62
63
  return {
@@ -74,7 +75,7 @@ export function executionLaneCaps(lane) {
74
75
  hostFilePlane: NO,
75
76
  coreRemoteExecutionEnv: YES,
76
77
  canonicalPathAuthority: YES,
77
- tenantSeparation: YES,
78
+ tenantSeparation: executionLaneRow(lane).tenantSeparation,
78
79
  };
79
80
  case "ssh":
80
81
  return {
@@ -92,7 +93,7 @@ export function executionLaneCaps(lane) {
92
93
  hostFilePlane: NO,
93
94
  coreRemoteExecutionEnv: YES,
94
95
  canonicalPathAuthority: YES,
95
- tenantSeparation: SU,
96
+ tenantSeparation: executionLaneRow(lane).tenantSeparation,
96
97
  };
97
98
  case "adb":
98
99
  return {
@@ -110,7 +111,7 @@ export function executionLaneCaps(lane) {
110
111
  hostFilePlane: NO,
111
112
  coreRemoteExecutionEnv: YES,
112
113
  canonicalPathAuthority: YES,
113
- tenantSeparation: SU,
114
+ tenantSeparation: executionLaneRow(lane).tenantSeparation,
114
115
  };
115
116
  case "local-docker":
116
117
  return {
@@ -128,7 +129,7 @@ export function executionLaneCaps(lane) {
128
129
  hostFilePlane: NO,
129
130
  coreRemoteExecutionEnv: YES,
130
131
  canonicalPathAuthority: NO,
131
- tenantSeparation: YES,
132
+ tenantSeparation: executionLaneRow(lane).tenantSeparation,
132
133
  };
133
134
  case "device":
134
135
  return {
@@ -146,25 +147,52 @@ export function executionLaneCaps(lane) {
146
147
  hostFilePlane: NO,
147
148
  coreRemoteExecutionEnv: YES,
148
149
  canonicalPathAuthority: YES,
149
- tenantSeparation: YES,
150
+ tenantSeparation: executionLaneRow(lane).tenantSeparation,
150
151
  };
151
152
  }
152
153
  }
153
- const EXECUTION_LANE_WORDS = {
154
- "in-process": true,
155
- host: true,
156
- e2b: true,
157
- k8s: true,
158
- ssh: true,
159
- adb: true,
160
- "local-docker": true,
161
- device: true,
162
- };
154
+ export const ALL_EXECUTION_LANES = Object.freeze(["in-process", ...DECLARABLE_LANE_WORDS]);
155
+ const EXECUTION_LANE_SET = new Set(ALL_EXECUTION_LANES);
163
156
  export function isExecutionLane(word) {
164
- return Object.hasOwn(EXECUTION_LANE_WORDS, word);
157
+ return EXECUTION_LANE_SET.has(word);
158
+ }
159
+ export const REMOTE_EXEC_PROVIDER_WORDS = Object.freeze([...DECLARABLE_LANE_WORDS]);
160
+ const LANE_ROWS = (() => {
161
+ const rows = new Map();
162
+ for (const row of EXECUTION_LANES) {
163
+ if (rows.has(row.lane))
164
+ throw new Error(`@sema-agent/settings-schema EXECUTION_LANES lists lane "${row.lane}" twice — the lane table is the single source; refusing to guess which row is meant`);
165
+ rows.set(row.lane, row);
166
+ }
167
+ const missing = DECLARABLE_LANE_WORDS.filter((lane) => !rows.has(lane));
168
+ if (missing.length > 0)
169
+ throw new Error(`@sema-agent/settings-schema EXECUTION_LANES has no row for lane(s) ${missing.join(", ")} — the lane table is the single source; refusing to start with a lane whose required env / tenant separation is unknown`);
170
+ return rows;
171
+ })();
172
+ export function executionLaneRow(lane) {
173
+ const row = LANE_ROWS.get(lane);
174
+ if (row === undefined)
175
+ throw new Error(`execution lane "${lane}" has no row in the settings-schema lane table (load-time completeness check was bypassed)`);
176
+ return row;
177
+ }
178
+ export function laneRequiredEnv(lane) {
179
+ const row = executionLaneRow(lane);
180
+ return [...row.requiredEnv, ...row.requiredSecretEnv];
181
+ }
182
+ export function executionLaneRequiredRefusal(deployment) {
183
+ if (!deployment.requireExecutionLane || executionLaneOf(deployment.remoteExec?.provider) !== "in-process")
184
+ return undefined;
185
+ return (`REQUIRE_EXECUTION_LANE=true but NO execution lane is named on this deployment — refusing to start. ` +
186
+ `With no lane the engine falls back to its in-process stub environment: the hands band (the roster entries whose ` +
187
+ `wiring_manifest mountedBy is "hands", Bash among them) is not mounted, while the rest of the roster still executes, ` +
188
+ `and every task still returns status=completed — for any request that needs a hand, indistinguishable from "the model ` +
189
+ `chose not to use its tools". (How much of the roster is mounted is a per-run fact: read the wiring_manifest event and ` +
190
+ `count entries whose mountedBy is "hands" — never judge by tool names.) ` +
191
+ `Cause: REMOTE_EXEC is unset/empty AND CONFIG_PROVIDER is not "local". ` +
192
+ `Fix: set REMOTE_EXEC to one of ${REMOTE_EXEC_PROVIDER_WORDS.join(" | ")} (and that lane's required env), ` +
193
+ `or set CONFIG_PROVIDER=local to take the single-machine \`host\` default, ` +
194
+ `or unset REQUIRE_EXECUTION_LANE to keep running without hands on purpose.`);
165
195
  }
166
- export const ALL_EXECUTION_LANES = Object.freeze(Object.keys(EXECUTION_LANE_WORDS).filter(isExecutionLane));
167
- export const REMOTE_EXEC_PROVIDER_WORDS = Object.freeze(ALL_EXECUTION_LANES.filter((lane) => lane !== "in-process"));
168
196
  export function executionLaneOf(provider) {
169
197
  if (provider === undefined)
170
198
  return "in-process";
@@ -36,17 +36,97 @@ export interface ParseHooksConfigResult {
36
36
  error?: string;
37
37
  }
38
38
  /**
39
- * fresh 提交面专用判据(S-924):配置里第一条「永远打不中」的工具事件 matcher 的拒句;没有 ⇒ undefined。
40
- * 入参是 {@link parseHooksConfig} 已校验过的配置。**只有 `admission.ts` 的 fresh 提交门调它** —— resume 重放不调(理由见上面的头注)。
39
+ * 🔴 **S-924 → S-995(clay [ref] ③;八病审计 SG H6-d;test 7.112.0 验收 OBS ①)—— 工具事件的 matcher 选中了工具**别名**、却选不中它的正名 ⇒ fresh 提交面响亮拒。**
40
+ *
41
+ * 病:三个工具事件(PreToolUse / PostToolUse / PostToolUseFailure)的 matcher 都按调用的 **wire 名**匹配(本文件顶注
42
+ * 「tool_name 是哪一个名」;core `NAME_READINGS` 三行均为 `wire`)。引擎自带工具 `ListAgents` 的别名 `ListPeers` 写进 matcher
43
+ * ⇒ **永不命中、一句话都没有**(post 两事件自 7.107.0 / core 7.36.0 起如此,PreToolUse 一直如此)。静默不命中 = 写的人以为在生效的 hook
44
+ * 其实没跑,硬破坏三句要求的「响亮」没有着落。7.112.0(S-924)只拒**字面**别名;同一个错误写成 `^ListPeers$` / `ListPeers.*` / `(ListPeers)` 照样 200(S-995)。
45
+ *
46
+ * 规则(一条;字面形是它的特例):{@link deadToolMatcherIssue} 对 core 静态目录里**每一条退役别名**问「这只 matcher 选不选它 / 选不选它的正名」:
47
+ * matcher **命中别名 ∧ 不命中它的正名** ⇒ `400 request.field_invalid`(`admission.ts` fresh 门)。「选不选」与运行期 {@link hookMatcherMatches} 同一套读法,按**一条顺序**判(两档共用,7.114.0 修复轮):
48
+ * ① **长度上限**:超 {@link MAX_MATCHER_EVAL_CHARS} 的 matcher 两档都不读 ⇒ 未判(reason = oversize);
49
+ * ② 空 / `*` 命中一切 ⇒ 永不「死」;
50
+ * ③ **字面 / 名字表读法**({@link matcherLiteralNames},与运行期共用:`|` 与 `,` 分隔、每段 trim〔容忍两端与分隔符两侧空白〕、不折大小写、不剥词内空白)
51
+ * 两档都先走、每只 matcher 读一次、**不占求值预算**,按名字表成员答 —— 所以两档在字面子集上答案逐位相同是构造出来的(R5f 语料再钉);
52
+ * ④ 只剩**正则形**:**`"regex"` 档**(hooks 会被采纳的车道,`taskHooksHonored`)问运行期正则臂的那一只函数 {@link matcherPatternMatches}(同一个函数对象,锚定正则),
53
+ * 经 `node:vm` 的 `timeout` 带一次受理共享的预算调用({@link matcherPatternMatchesWithin});**`"literal"` 档**(多租户车道:hooks 整只被 resolveSpec 忽略)
54
+ * = **不适用**:该车道没有被守的对象(hooks 不装配),不判、不算未判、不登记 —— **零正则求值、零调用方正则执行**(7.112.0 的字面判保留:裸 `ListPeers` / `Read|ListPeers` 照旧 400)。
55
+ * · **为什么两档**(codex r1 [high] + 主会话裁):受理面是**所有**车道,而运行期只有单用户车道跑 hooks。在受理线程上跑调用方的正则 = 给多租户加一个事件循环 DoS 面
56
+ * (实测:`(.*)*` ×10 + `X` 对 9 字符名字首次执行 1.7 s,18 字符嵌套形 15 s,24 字符 157 s;8 MiB 交替串编译 + 求值 9 s)—— 预算只界住一次请求、界不住「每个请求再拿一份」。
57
+ * 但也不能因此在多租户车道「不判决」(那会放宽已发行为:裸 `ListPeers` 的 400)。所以两车道同一条规则、两档深度。谓词 `taskHooksHonored` 是唯一属主(与 resolveSpec / capabilities 同调)。
58
+ * · **为什么不进 {@link parseHooksConfig}**(codex r1 [high],S-924):`parseHooksConfig` 同时是 resume 重放(`parseTaskSettings` 宽容层)的校验器,
59
+ * 它判不过 ⇒ **整只 hooks 不采纳**。存量 body 里同一份 hooks 常常还有会真拦的 PreToolUse 钩子 —— 一个永不命中的别名 matcher 把它们一并带走,
60
+ * 升级前被拦的 Bash 升级后放行(安全轴上的回退)。所以这条规则住在 fresh 专用的判据里,与 `taskSettingsKeyIssue` 同姿势
61
+ * (「fresh 400 / resume 宽容」双层:会砖存量的规则只咬 fresh)。resume 照旧整只采纳,别名那一组继续永不命中(与 7.111.0 逐字同),其余照跑。
62
+ * · **哪些事件** = 契约 `HOOK_EVENT_MATCHER_FIELD[event] === "tool_name"` ∧ 引擎属主 `HOOK_EVENT_OWNER[event] === "engine"`
63
+ * (今天恰是上面三个;`PermissionRequest` / `PermissionDenied` 也匹配 `tool_name` 但属壳,壳自己跑,不是本 runner 的读法)。**不手抄事件表**([ref])。
64
+ * · **哪些名字算别名** = core 静态目录(`describeToolCatalog()`)里某条 `face.aliases` 的成员,且它自己不是另一条目的 wire 名({@link engineToolAliases})。
65
+ * **只读这一只真源**([ref])。本仓自身不声明带别名的工具;嵌入方经 `resolveSpec` 席挂的别名与 MCP 工具名在受理面看不见(名册是每任务装配的),**不在射程**。
66
+ * · **正名同在则不拒**:`正名|别名` / `List.*` 会命中正名,不是「静默不命中」,且正是 7.107.0 CHANGELOG 给的「滚动期两名都写」补偿形。
67
+ * 只拒「别名那一路永远打不中、而它的正名不在选中集里」—— 含 `Read|ListPeers`(7.112.0 已拒;主会话裁:规则形采「按条目逐条问」,不放宽;卡面整 matcher「匹配不到任何现役名」形会把它放回 200)。
68
+ * 多拒(相对卡面形)三例与出路:`Read|ListPeers` / `ListPeers|mcp__.*`(别名支永不命中,出路:改成正名)/ 排除式 `^(?!ListAgents).*`(出路:`^(?!ListAgents|ListPeers).*` 两名都不命中 ⇒ 放)。
69
+ * · **空白与大小写(S-995 / sema-test [ref] 起,运行期与 CC 2.1.280 同形)**:字面 / 名字表读法({@link matcherLiteralNames};`|` 与 `,` 分隔)容忍两端与分隔符两侧空白 ⇒ `" ListPeers"` / `"ListPeers "` / `"Read | ListPeers"` 会命中别名 ⇒ 落进本规则 400;
70
+ * **区分大小写、不剥词内空白** ⇒ `"listpeers"` / `"List Peers"` 对别名与正名都选不中,照旧受理(CC 同形)。
71
+ * · **不在本规则内**:两边都选不中的 matcher(大小写错 / 词内空白 / 纯错字;目录之外的 MCP / 嵌入方工具名同样「选不中目录里任何名」,受理面无从区分错字与它们)=
72
+ * **「选不到目录里任何名」另一类,候设计(台账另立号,主会话裁)**。
73
+ *
74
+ * 🔴 **「未判」臂过 `recordFailOpen`**(F 类 tag `MATCHER_UNJUDGED_TAG`;主会话裁,CLAUDE.md「合法 F 类回退走登记 tag」):一只 matcher 没能被判 ⇒ 照旧受理 + 登记一笔,
75
+ * detail = `event=… reason=… matcherChars=…`(**不带 matcher 原字节**,S-716 姿势)。三种 reason 同一个 tag:
76
+ * `oversize` —— 两档:matcher 超 {@link MAX_MATCHER_EVAL_CHARS} 字符不读(多租户车道上 hooks 不装配,这一笔只为让超长输入显形);
77
+ * `over_budget` ——「regex」档正则形:一次受理共享一份 {@link ADMISSION_MATCHER_EVAL_BUDGET_MS} 墙钟预算,用尽 ⇒ 这一只与其后各只正则形未判(`vm` 的 `timeout` 打断回溯,含 tier-up 之后的原生码);
78
+ * `invalid_regex` ——「regex」档正则形编不过(运行期另有每次调用的 `hook_matcher_invalid`;这是确定性的坏配置,不是负载 —— 拒它是新检测臂,不在本规则)。
79
+ * 放行最坏后果 = 那一只 matcher 没被这条判据守住、照旧永不命中,**不是安全闸**(受理面有效性判);反方向「越界即拒」会把负载抖动 / 病态但合法的 matcher 变成对合法请求的 400。
80
+ * 拒放集合相对 7.113.0 的一处放宽(披露在 CHANGELOG / MIGRATION):超过上限的字面别名 matcher 7.113.0 拒(当时字面判不设上限),自 7.114.0 两档都判未判、受理。
81
+ *
82
+ * 错误句只回显**目录里的**别名与正名(闭集字符串),不回显调用方的原字节(S-716 姿势)。
83
+ */
84
+ /** 超此长度的 matcher 不在受理面读(两档同一个上限;未判,reason = oversize)。与 {@link MAX_HOOK_COMMAND_CHARS} 同量纲的服务侧防线:真实 matcher(精确名 / 交替 / `mcp__.*`)远在其内;目录全部工具名交替 ~0.5 KiB。 */
85
+ export declare const MAX_MATCHER_EVAL_CHARS = 8192;
86
+ /** 一次 fresh 受理里 regex 档**正则形**求值共享的墙钟预算(毫秒)。字面形不占它。良性求值 ~0.1 ms;灾难回溯的 matcher 把预算吃完就停,后面的正则形组直接「未判」。 */
87
+ export declare const ADMISSION_MATCHER_EVAL_BUDGET_MS = 100;
88
+ /** 受理面判据的求值深度(见上头注):`"regex"` = 字面形按名字表答、正则形问运行期同一只正则求值(带预算);`"literal"` = 7.112.0 的字面判(零正则,正则形不适用)。 */
89
+ export type MatcherEvalDepth = "regex" | "literal";
90
+ /**
91
+ * **仅供测试注入**的求值预算(生产调用点不传 ⇒ 预算 = {@link ADMISSION_MATCHER_EVAL_BUDGET_MS}、时钟 = `performance.now`)。
92
+ * 让「预算是一次受理共享的」「耗尽之后的组」这类格子按求值次序断言,不按机器速度断言(`test/hooks-alias-matcher-s995.test.ts` R4)。
41
93
  */
42
- export declare function deadToolMatcherIssue(config: HooksConfig): string | undefined;
94
+ export interface MatcherEvalBudget {
95
+ readonly budgetMs?: number;
96
+ readonly now?: () => number;
97
+ }
98
+ /**
99
+ * fresh 提交面专用判据(S-924 / S-995):配置里第一条「永远打不中」的工具事件 matcher 的拒句;没有 ⇒ undefined。
100
+ * 入参是 {@link parseHooksConfig} 已校验过的配置;`depth` 由调用点按车道定(见头注:`taskHooksHonored(config) ? "regex" : "literal"`)。
101
+ * `budget` **仅供测试注入**({@link MatcherEvalBudget});生产调用点只传前两个实参。
102
+ * **只有 `admission.ts` 的 fresh 提交门调它** —— resume 重放不调(理由见上面的头注)。未判的 matcher 过 `recordFailOpen`(tag `MATCHER_UNJUDGED_TAG`)。
103
+ */
104
+ export declare function deadToolMatcherIssue(config: HooksConfig, depth: MatcherEvalDepth, budget?: MatcherEvalBudget): string | undefined;
43
105
  /**
44
106
  * 把 wire 上的 untrusted `settings.hooks` 校验成契约 {@link HooksConfig} + 服务侧上限。
45
107
  * 错误返回 error 字符串而不 throw——submit 路径 400 fail-loud、resume 防御路径 warn-drop,由调用方选。
46
108
  */
47
109
  export declare function parseHooksConfig(raw: unknown): ParseHooksConfigResult;
48
- /** CC matcher 语义:精确名、`a|b` 交替、或 JS 正则(锚定全匹配,防 "Bash" 误配 "BashOutput");
49
- * 空/`*`/缺省匹配一切;非法正则 → 该条不匹配(fail-closed 到"不跑",由调用方记账一次)。 */
110
+ /**
111
+ * matcher 的**字面 / 名字表读法**(S-995 / sema-test [ref];与 CC 2.1.280 同形):matcher 只含名字字符(`[A-Za-z0-9_-]`)、竖线、**逗号**与**空格**时,它不是正则而是「名字列表」——
112
+ * 按 `[|,]` 拆、**每段 trim**、丢空段;返回这张名字表。含任何别的字符(`.` / `*` / 括号 / 制表 / 换行 …)⇒ `undefined` = 不是字面读法,整体当正则(由调用方处置,**一字不动**)。
113
+ * 大小写敏感、词内空白不剥(`"Ba sh"` 是一个词 `Ba sh`)。**对所有带 matcher 的事件同一只读法**(工具事件 / StopFailure 的 `error` 词 / PreCompact·PostCompact 的 `trigger` 词 /
114
+ * `if` 条件的名部),不按事件分档。
115
+ *
116
+ * 等价性的依据(收车档 ⑥ / ⑦):
117
+ * · 黑盒 —— sema-test `repro/cc-r219-mcpstar-hookmatcher-20261003/`([ref],CC 2.1.280 真 TUI + 真 command 钩子):`" Bash"` / `"Bash "` / `" Bash "` / `"Read | Bash"` 命中 3/3,
118
+ * `"Ba sh"`(词内空白)/ `bash` / `BASH` 不命中(零告警),`^Bash$` / `Bash.*` / `(Bash)` / `Read|Bash` 照命中;PreToolUse 与 PostToolUse 同向。**逗号没有黑盒读数**,只有语料。
119
+ * · 语料 —— `sema-internal/blueprints/cc-decoded/cli280.js:216638` `uAe(matchValue, matcher, flag, …)` → `$mr(matcher, flag, …)`:`flag` 真时字符集门 `/^[a-zA-Z0-9_|, -]+$/` 过了才
120
+ * `matcher.split(/[|,]/).map(trim).filter(Boolean)` 做成员判;过不了才 `new RegExp(matcher)`(CC 的正则路径**不锚定**,本仓锚定 `^(?:…)$` —— 既有差异,不动)。
121
+ * 本函数的字符集与拆符与 CC 逐字同(空白只认空格 0x20)。
122
+ * · 已知差异(如实登记,本车不做):CC 的空白 / 逗号放行**按事件分档**(`dAe` 集合:工具事件 / PreCompact / PostCompact 等放,StopFailure 不放),本函数不分事件(第二批裁:同一只读法);
123
+ * CC 正则路径不锚定、本仓锚定。两条登记在 CHANGELOG 7.114.0 线 · 车 UW 已知边界。
124
+ * 零正则:单趟逐字符扫描(7.114.0 修复轮起;只为非空段分配名字,不按分隔符个数分配)。受理面两档({@link deadToolMatcherIssue})与运行期求值**共用这一只**读法,且两档都先走它、不占求值预算 ——
125
+ * 两档在字面子集上答案逐位相同是**构造出来的**,不是对拍出来的(超过长度上限的 matcher 两档都不读、同判未判)。
126
+ */
127
+ export declare function matcherLiteralNames(matcher: string): readonly string[] | undefined;
128
+ /** CC matcher 语义:空/`*`/缺省匹配一切;**字面 / 名字表读法**({@link matcherLiteralNames}:`|` 与 `,` 分隔、每段 trim、不剥词内空白、区分大小写)按名字表成员判;
129
+ * 其余整体当 JS 正则({@link matcherPatternMatches};锚定全匹配;S-995 起正则路径一字不动);非法正则 → 该条不匹配(fail-closed 到"不跑",由调用方记账一次)。 */
50
130
  export declare function hookMatcherMatches(matcher: string | undefined, value: string): boolean | "invalid";
51
131
  /** `if` 条目条件(CC-exact,对照 CC 2.1.187/198 实测行为核验的真语义,两版一致):
52
132
  * 值 = permission-rule 模式(`ToolName` 或 `ToolName(arg-模式)`),【不是】表达式语言——按当前工具调用
@@ -179,8 +259,8 @@ export declare function createTaskHooks(config: HooksConfig, ctx: HookRunnerCont
179
259
  * 把部署级 hooks 基线(deps.hooks,如 TOOL_TRACE 观测)折进 task 级 hooks——core 是整槽覆盖
180
260
  * (`spec.hooks ?? deps.hooks`),不折叠就静默 shadow 部署观测(runtask gateBaseline 注释点名的坑)。
181
261
  * 语义:部署槽先跑(观测看到原始输入/输出),task 槽后跑;结果合并:
182
- * - preToolUse:deny > ask > allow 折叠;updatedInput 串行线程(部署重写喂给 task 槽);context 拼接。读不出词的结果
183
- * (非对象、集外 `action`)原样交回、由引擎的接手屏判(S-959,短路同 deny)。
262
+ * - preToolUse:每一槽的结果**先过 core 的 hook 席收据** `receiveHookResult`(S-971,core 7.39.0 #1274)—— 引擎拒的载体 ⇒ 交回引擎的拒绝体
263
+ * (短路,引擎按自己的 fail-closed 拒处置);收下的 ⇒ 折它的冻结副本:deny > ask > allow 折叠;updatedInput 串行线程(部署重写喂给 task 槽);context 拼接。
184
264
  * - postToolUse/postToolUseFailure/postToolBatch:双跑,additionalContext 拼接,updatedOutput 取
185
265
  * 后者(task 槽)优先。
186
266
  * - userPromptSubmit/stop:部署 block 优先短路(不再跑 task 槽),否则双跑、context 拼接、task block 生效。
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
+ import vm from "node:vm";
2
3
  import { hostShell, resolveHostShell, adoptHostForegroundChild } from "../plugins/host-platform.js";
3
4
  import { HooksConfig, DEFAULT_HOOK_TIMEOUT_SECONDS, HOOK_EVENT_OWNER, HOOK_EVENT_MATCHER_FIELD, } from "@sema-agent/settings-schema/hooks";
4
- import { describeToolCatalog } from "@sema-agent/core";
5
+ import { describeToolCatalog, isEngineRefusal, receiveHookResult } from "@sema-agent/core";
5
6
  import { boundedNoticeText } from "../fleet/fleet-bus.js";
6
7
  import { recordFailOpen } from "../observability/fail-open.js";
7
8
  import { redactSecrets, buildDisplaySafeUrl } from "../trace/redact.js";
@@ -57,7 +58,9 @@ function toAssistantTurnError(errorKind) {
57
58
  return "unknown";
58
59
  return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
59
60
  }
60
- const NAME_ALTERNATION = /^[A-Za-z0-9_-]+(?:\|[A-Za-z0-9_-]+)*$/;
61
+ export const MAX_MATCHER_EVAL_CHARS = MAX_HOOK_COMMAND_CHARS;
62
+ export const ADMISSION_MATCHER_EVAL_BUDGET_MS = 100;
63
+ const MATCHER_UNJUDGED_TAG = "server.hooks.matcher-unjudged";
61
64
  function matcherReadsWireToolName(event) {
62
65
  return HOOK_EVENT_OWNER[event] === "engine" && HOOK_EVENT_MATCHER_FIELD[event] === "tool_name";
63
66
  }
@@ -77,25 +80,85 @@ function engineToolAliases() {
77
80
  engineToolAliasTable = table;
78
81
  return table;
79
82
  }
80
- function deadAliasInMatcher(event, matcher) {
81
- if (matcher === undefined || !matcherReadsWireToolName(event) || !NAME_ALTERNATION.test(matcher))
83
+ let matcherEvalBox;
84
+ function matcherPatternMatchesWithin(matcher, name, budget) {
85
+ const remaining = budget.deadlineMs - budget.now();
86
+ if (remaining <= 0)
87
+ return "over_budget";
88
+ matcherEvalBox ??= {
89
+ script: new vm.Script("matcherPatternMatches(matcher, name)"),
90
+ context: vm.createContext({ matcherPatternMatches, matcher: "", name: "" }),
91
+ };
92
+ matcherEvalBox.context.matcher = matcher;
93
+ matcherEvalBox.context.name = name;
94
+ try {
95
+ const verdict = matcherEvalBox.script.runInContext(matcherEvalBox.context, { timeout: Math.max(1, Math.ceil(remaining)) });
96
+ return verdict === true ? true : verdict === false ? false : "invalid";
97
+ }
98
+ catch (err) {
99
+ if (typeof err === "object" && err !== null && "code" in err && err.code === "ERR_SCRIPT_EXECUTION_TIMEOUT")
100
+ return "over_budget";
101
+ throw err;
102
+ }
103
+ }
104
+ function deadAliasInMatcher(matcher, depth, budget) {
105
+ if (matcher.length > MAX_MATCHER_EVAL_CHARS)
106
+ return { kind: "unjudged", reason: "oversize" };
107
+ if (matcher === "" || matcher === "*")
108
+ return undefined;
109
+ const names = matcherLiteralNames(matcher);
110
+ if (names !== undefined) {
111
+ for (const [alias, canonical] of engineToolAliases()) {
112
+ if (names.includes(alias) && !names.includes(canonical))
113
+ return { kind: "dead", alias, canonical };
114
+ }
82
115
  return undefined;
83
- const branches = matcher.split("|");
84
- const aliases = engineToolAliases();
85
- for (const branch of branches) {
86
- const canonical = aliases.get(branch);
87
- if (canonical !== undefined && !branches.includes(canonical))
88
- return { alias: branch, canonical };
116
+ }
117
+ switch (depth) {
118
+ case "literal":
119
+ return undefined;
120
+ case "regex":
121
+ break;
122
+ default: {
123
+ const unreachable = depth;
124
+ return unreachable;
125
+ }
126
+ }
127
+ const selects = (name) => {
128
+ const verdict = matcherPatternMatchesWithin(matcher, name, budget);
129
+ return verdict === "invalid" ? "invalid_regex" : verdict;
130
+ };
131
+ for (const [alias, canonical] of engineToolAliases()) {
132
+ const selectsAlias = selects(alias);
133
+ if (typeof selectsAlias === "string")
134
+ return { kind: "unjudged", reason: selectsAlias };
135
+ if (!selectsAlias)
136
+ continue;
137
+ const selectsCanonical = selects(canonical);
138
+ if (typeof selectsCanonical === "string")
139
+ return { kind: "unjudged", reason: selectsCanonical };
140
+ if (!selectsCanonical)
141
+ return { kind: "dead", alias, canonical };
89
142
  }
90
143
  return undefined;
91
144
  }
92
- export function deadToolMatcherIssue(config) {
145
+ export function deadToolMatcherIssue(config, depth, budget = {}) {
146
+ const now = budget.now ?? (() => performance.now());
147
+ const deadline = { deadlineMs: now() + (budget.budgetMs ?? ADMISSION_MATCHER_EVAL_BUDGET_MS), now };
93
148
  for (const [event, groups] of Object.entries(config)) {
149
+ if (!matcherReadsWireToolName(event))
150
+ continue;
94
151
  for (const group of groups ?? []) {
95
- const dead = deadAliasInMatcher(event, group.matcher);
96
- if (dead) {
97
- return `settings.hooks.${event}: matcher "${dead.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${dead.canonical}"`;
152
+ if (group.matcher === undefined)
153
+ continue;
154
+ const verdict = deadAliasInMatcher(group.matcher, depth, deadline);
155
+ if (verdict === undefined)
156
+ continue;
157
+ if (verdict.kind === "unjudged") {
158
+ recordFailOpen(MATCHER_UNJUDGED_TAG, `event=${event} reason=${verdict.reason} matcherChars=${group.matcher.length}`);
159
+ continue;
98
160
  }
161
+ return `settings.hooks.${event}: matcher "${verdict.alias}" is an alias; tool-event matchers match the tool's wire name (PostToolUse / PostToolUseFailure since 7.107.0) — use "${verdict.canonical}"`;
99
162
  }
100
163
  }
101
164
  return undefined;
@@ -144,9 +207,31 @@ export function parseHooksConfig(raw) {
144
207
  }
145
208
  return { config };
146
209
  }
147
- export function hookMatcherMatches(matcher, value) {
148
- if (matcher === undefined || matcher === "" || matcher === "*")
149
- return true;
210
+ export function matcherLiteralNames(matcher) {
211
+ const names = [];
212
+ let first = -1;
213
+ let last = -1;
214
+ for (let i = 0; i <= matcher.length; i++) {
215
+ const c = i < matcher.length ? matcher.charCodeAt(i) : 0x7c;
216
+ if (c === 0x7c || c === 0x2c) {
217
+ if (first !== -1)
218
+ names.push(matcher.slice(first, last + 1));
219
+ first = -1;
220
+ last = -1;
221
+ continue;
222
+ }
223
+ if (c === 0x20)
224
+ continue;
225
+ const nameChar = (c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) || c === 0x5f || c === 0x2d;
226
+ if (!nameChar)
227
+ return undefined;
228
+ if (first === -1)
229
+ first = i;
230
+ last = i;
231
+ }
232
+ return names;
233
+ }
234
+ function matcherPatternMatches(matcher, value) {
150
235
  if (matcher === value)
151
236
  return true;
152
237
  try {
@@ -156,6 +241,14 @@ export function hookMatcherMatches(matcher, value) {
156
241
  return "invalid";
157
242
  }
158
243
  }
244
+ export function hookMatcherMatches(matcher, value) {
245
+ if (matcher === undefined || matcher === "" || matcher === "*")
246
+ return true;
247
+ const names = matcherLiteralNames(matcher);
248
+ if (names !== undefined)
249
+ return names.includes(value);
250
+ return matcherPatternMatches(matcher, value);
251
+ }
159
252
  export function hookIfMatches(cond, payload) {
160
253
  if (cond.length > 500 || (cond.match(/\*/g)?.length ?? 0) > 16)
161
254
  return "unevaluable";
@@ -1234,10 +1327,10 @@ export function composeHooks(deployment, task) {
1234
1327
  for (const slot of [d.preToolUse?.bind(d), t.preToolUse?.bind(t)]) {
1235
1328
  if (!slot)
1236
1329
  continue;
1237
- const r = await slot(toolName, current, ctx);
1238
- if (r === undefined || Object.is(r, null) || Object.is(r, false))
1330
+ const r = receiveHookResult(await slot(toolName, current, ctx));
1331
+ if (r === undefined || r === null || r === false)
1239
1332
  continue;
1240
- if (typeof r !== "object")
1333
+ if (isEngineRefusal(r))
1241
1334
  return r;
1242
1335
  if (r.additionalContext)
1243
1336
  contexts.push(r.additionalContext);
@@ -22,7 +22,9 @@ import { type AskOutcome, type ResumeOutcome } from "@sema-agent/core";
22
22
  * 而 sweep 结构上不能 `allow`)。
23
23
  * · `decidedBy: "policy"`(core 7.26.2 / #1016)—— **这台部署自己的策略**拒的:答话的是机器不是人。调用方
24
24
  * 经决裁体的 `settledBy` 自报;core 据它给模型铸一句**策略句**(而不是「用户不同意,停下来等他」),
25
- * 并把结算记成 `policy_refused`(`who.party:"none"`)。`allow` 侧结构上不可(core pre-CAS 拒:只有人批准)。
25
+ * 并把结算记成 `policy_refused`(`who.party:"policy"`,core 7.39.0 #1265 起;此前 `"none"`)。`allow` 侧:core 7.39.0 起引擎收策略的
26
+ * 放行(`policy_allowed`),但本仓的两条 decide 口**不开**这一形(`tool-approval.ts` 的 `SETTLED_BY_ONLY_ON_DENY` 与店列的同一条拒仍在 ——
27
+ * 耐久车道上的机器放行是一件没裁过的新能力,不在 7.113.0 提货批内,候登)。
26
28
  * · `approver` —— 通道自报的结算方标识串。**缺席永不读成**「没人批」或「有人批」,只意味着通道没报名字。
27
29
  */
28
30
  /**
@@ -74,8 +76,8 @@ export type HostDecidedBy = HostDecision["decidedBy"];
74
76
  * 被改造过的串):归属是转录,不是可以「修一修」的东西 —— 修出来的名字既不是通道报的那个,也不是
75
77
  * 「通道没报」,两头都不真。
76
78
  *
77
- * 🔴 归属**不按词分岔**:core 的 `policy_refused` 结算记录上确实没有归属席(`who.party:"none"`),但那是
78
- * **core 的记录**怎么写,不是**本服务观察到的事实**是什么 —— 送出这次请求的那把已验证身份是真的,本仓的
79
+ * 🔴 归属**不按词分岔**:core 7.38.x 的 `policy_refused` 结算记录上没有归属席(`who.party:"none"`),7.39.0 #1265 起有了
80
+ * (`who.party:"policy"`,`approver?`)—— 无论 core 的记录怎么写,**本服务观察到的事实**不变:送出这次请求的那把已验证身份是真的,本仓的
79
81
  * 迟到结算账本(`approval-ask-audit-store.ts`)记的就是它。按词删一格 = 给账本挖一个只在某个词上出现的洞。
80
82
  *
81
83
  * 🔴 **入参是 `string`,不是 `HostDecidedBy` —— 这一格刻意让类型说真话**(wire 边界强类型宪法):两条
@@ -13,8 +13,8 @@
13
13
  * 比手抄的值更难发现烂掉,[ref] 的第一排查点就是它;
14
14
  * · 能力授予面的赦免挂在 **`requirePrincipal !== true`** 上 —— 但**三条面各有各的谓词,不是一只**
15
15
  * (异源复核实测,别照「它们都一样」写):
16
- * · `settings.hooks` —— `boot/resolve-spec.ts` 的 hooks 臂,判据**只有** `config.requirePrincipal === true`
17
- * ⇒ 忽略(**不看车道**,任何 `REMOTE_EXEC` 都一样);
16
+ * · `settings.hooks` —— `task-settings.ts` 的 `taskHooksHonored`(唯一属主;resolve-spec 装配 / 受理面 / 能力位三处同调),判据**只有**
17
+ * `requirePrincipal !== true` ⇒ 采纳,否则忽略(**不看车道**,任何 `REMOTE_EXEC` 都一样);
18
18
  * · `additionalDirectories` / `additionalReadDirectories` —— **只有** `cwdHonored`
19
19
  * (= `provider === "host" ∧ requirePrincipal !== true`);
20
20
  * · `settings.env` / `cwd` —— `cwdHonored ‖ deviceCwdHonored`,而
@@ -2,7 +2,7 @@ import { isThinkingLevel, expandTiers } from "@sema-agent/core";
2
2
  import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError } from "../security.js";
5
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
5
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
6
6
  import { taskBodyKeyIssue } from "../request-key-closure.js";
7
7
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
8
8
  import { selfOrchestrationDenial } from "../task-workflow.js";
@@ -200,7 +200,7 @@ export function createAdmission(ctx) {
200
200
  sendError(res, 400, "request.field_invalid", hooksParsed.error ?? "settings.hooks: invalid shape");
201
201
  return null;
202
202
  }
203
- const deadMatcher = deadToolMatcherIssue(hooksParsed.config);
203
+ const deadMatcher = deadToolMatcherIssue(hooksParsed.config, taskHooksHonored(deps.config) ? "regex" : "literal");
204
204
  if (deadMatcher !== undefined) {
205
205
  sendError(res, 400, "request.field_invalid", deadMatcher);
206
206
  return null;
@@ -550,11 +550,15 @@ export function createAdmission(ctx) {
550
550
  if (body.attachments !== undefined) {
551
551
  const a = body.attachments;
552
552
  if (typeof a !== "object" || a === null || Array.isArray(a)) {
553
- sendError(res, 400, "request.field_invalid", "attachments must be an object { todoReminder?, todoReminderMode?, changedFiles?, planModeReminder?, budgetUsd?, backgroundTasks?, toolsDelta?, agentListing?, skillsListing?, mcpInstructions? }");
553
+ sendError(res, 400, "request.field_invalid", "attachments must be an object { todoReminder?, todoReminderMode?, changedFiles?, budgetUsd?, backgroundTasks?, toolsDelta?, agentListing?, skillsListing?, mcpInstructions? }");
554
554
  return null;
555
555
  }
556
556
  const rec = a;
557
- for (const k of ["todoReminder", "planModeReminder", "budgetUsd", "backgroundTasks", "toolsDelta", "agentListing", "skillsListing", "mcpInstructions"]) {
557
+ if (rec.planModeReminder !== undefined) {
558
+ sendError(res, 400, "request.field_invalid", 'attachments.planModeReminder was retired in core 7.39.0 — the engine now states plan posture itself whenever the permission mode is "plan" (first request, periodic reminders, exit frame); remove the key (send permissionMode "plan" for plan posture)');
559
+ return null;
560
+ }
561
+ for (const k of ["todoReminder", "budgetUsd", "backgroundTasks", "toolsDelta", "agentListing", "skillsListing", "mcpInstructions"]) {
558
562
  if (rec[k] !== undefined && typeof rec[k] !== "boolean") {
559
563
  sendError(res, 400, "request.field_invalid", `attachments.${k} must be a boolean`);
560
564
  return null;
@@ -1,6 +1,6 @@
1
1
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "@sema-agent/core";
2
2
  import { HttpError, principalFrom } from "../security.js";
3
- import { RUN_NOT_FOUND_MESSAGE } from "./route-ctx.js";
3
+ import { RUN_NOT_FOUND_MESSAGE, SESSION_NOT_FOUND_MESSAGE } from "./route-ctx.js";
4
4
  import { sendError } from "./send.js";
5
5
  import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "./principal-gate.js";
6
6
  const MAX_BODY = 8 * 1024 * 1024;
@@ -165,7 +165,7 @@ export function createRequestGuards(ctx) {
165
165
  }
166
166
  if (!runOwnerReadable(principal, owner, deps.config)) {
167
167
  if (notFoundForm === "session")
168
- sendError(res, 404, "not_found.session", "session not found");
168
+ sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
169
169
  else
170
170
  sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
171
171
  return false;
@@ -12,16 +12,21 @@
12
12
  * (`RunRegistry` / `RouteHelpers` / `RouteLegs` / `ReturnType<typeof createRequestGuards>`),不手抄签名 ⇒ 不会漂。
13
13
  */
14
14
  import type { IncomingMessage } from "node:http";
15
- import { type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
15
+ import { CheckpointError, type QuestionAnswer, type CheckpointToken } from "@sema-agent/core";
16
16
  import type { DecideBinding } from "./wire-types.js";
17
17
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
18
18
  import { type HostDecision } from "../host-decision.js";
19
19
  import { type PlanApprovePermissionModeAfter } from "../task-settings.js";
20
- import type { RouteCtxBase, DriveResumeArgs, PreparedPlanReview, ResumeLegOutcome } from "./route-ctx.js";
20
+ import type { RouteCtxBase, DriveResumeArgs, HostLegOutcome, PreparedPlanReview, ResumeLegOutcome } from "./route-ctx.js";
21
21
  import { type TrustedRunInternals } from "./durable-run-leg.js";
22
22
  import type { RunRegistry, RouteHelpers, RouteLegs } from "./route-ctx.js";
23
23
  import type { createRequestGuards } from "./request-guards.js";
24
24
  import type { FlatServiceDeps } from "./server.js";
25
+ /**
26
+ * {@link CHECKPOINT_ERROR_DISPOSITION} 的读口(纯函数,resume 腿的 catch 臂与钉共用)。运行期遇到表外的码(装树 core 比本二进制多一个码的版本偏斜 ——
27
+ * 型面上拼不出来)⇒ `"terminal"`:与 `by_reason` 认不得的词同一条 fail-closed 方向(不宽容默认成可重试),也与修前手列串的缺省逐字相同。
28
+ */
29
+ export declare function checkpointErrorDisposition(e: CheckpointError): "cancelled" | "retriable" | "terminal";
25
30
  export declare function createResumeLegs(ctx: {
26
31
  deps: FlatServiceDeps;
27
32
  runnerFor: FlatServiceDeps["runnerFor"];
@@ -50,14 +55,8 @@ export declare function createResumeLegs(ctx: {
50
55
  status: number;
51
56
  body: object;
52
57
  }>;
53
- startFreshHostTurn: (sessionId: string, objective: string, principal: string | undefined, req: IncomingMessage, trustedInternals: TrustedRunInternals, runId: string) => Promise<{
54
- status: number;
55
- body: object;
56
- }>;
57
- resumeWake: (sessionId: string, message: string | undefined, caller: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, workflowParkedResume?: DriveResumeArgs["workflowParkedResume"], ownerLeaseAdmittedFor?: string) => Promise<{
58
- status: number;
59
- body: object;
60
- }>;
58
+ startFreshHostTurn: (sessionId: string, objective: string, principal: string | undefined, req: IncomingMessage, trustedInternals: TrustedRunInternals, runId: string) => Promise<HostLegOutcome>;
59
+ resumeWake: (sessionId: string, message: string | undefined, caller: string | undefined, req: IncomingMessage | undefined, acceptEarly?: boolean, workflowParkedResume?: DriveResumeArgs["workflowParkedResume"], ownerLeaseAdmittedFor?: string) => Promise<HostLegOutcome>;
61
60
  preparePlanReview: (sessionId: string, log?: {
62
61
  taskId?: string;
63
62
  principalPresent?: boolean;