@sema-agent/server 7.130.0 → 7.132.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 (294) hide show
  1. package/MIGRATION.md +396 -0
  2. package/README.md +13 -6
  3. package/README.zh-CN.md +9 -5
  4. package/USAGE.md +187 -50
  5. package/dist/approval-card.d.ts +3 -1
  6. package/dist/approval-card.js +2 -3
  7. package/dist/audit.d.ts +7 -10
  8. package/dist/audit.js +4 -1
  9. package/dist/auth-bridge.d.ts +4 -0
  10. package/dist/auth-bridge.js +10 -11
  11. package/dist/bench/s1/live-deps.js +2 -0
  12. package/dist/boot/deployment-mods.d.ts +31 -0
  13. package/dist/boot/deployment-mods.js +26 -0
  14. package/dist/boot/execution-env.d.ts +1 -1
  15. package/dist/boot/execution-env.js +19 -6
  16. package/dist/boot/memory-boundary.d.ts +2 -2
  17. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  18. package/dist/boot/read-deny-anchor-audit.js +36 -0
  19. package/dist/boot/resolve-spec.d.ts +1 -5
  20. package/dist/boot/resolve-spec.js +56 -61
  21. package/dist/boot/runner-deps.d.ts +19 -8
  22. package/dist/boot/runner-deps.js +14 -2
  23. package/dist/boot/session-faces.d.ts +1 -7
  24. package/dist/boot/stage-02-web-admin-pull.js +9 -1
  25. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  26. package/dist/boot/stage-03-store-backend.js +2 -2
  27. package/dist/boot/stage-04-stores.d.ts +1 -1
  28. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  29. package/dist/boot/stage-05-execution-env.js +7 -10
  30. package/dist/boot/stage-06-runners.d.ts +2 -2
  31. package/dist/boot/stage-06-runners.js +5 -4
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  33. package/dist/boot/stage-07-capability-layer.js +3 -3
  34. package/dist/boot/stage-08-reapers.d.ts +2 -2
  35. package/dist/boot/stage-09-leader.d.ts +3 -9
  36. package/dist/boot/stage-10-http-server.d.ts +3 -9
  37. package/dist/boot/stage-10-http-server.js +12 -10
  38. package/dist/boot/stores.d.ts +1 -1
  39. package/dist/boot/stores.js +5 -6
  40. package/dist/boot/web-admin.d.ts +0 -3
  41. package/dist/boot/web-admin.js +35 -35
  42. package/dist/brain.js +17 -22
  43. package/dist/budget.js +5 -4
  44. package/dist/caller-text-echo.d.ts +20 -0
  45. package/dist/caller-text-echo.js +24 -0
  46. package/dist/capabilities/brand-identity.d.ts +26 -0
  47. package/dist/capabilities/brand-identity.js +27 -0
  48. package/dist/capabilities/builtin-tools.js +2 -1
  49. package/dist/capabilities/center-plugins.js +1 -1
  50. package/dist/capabilities/center-prompts.d.ts +32 -59
  51. package/dist/capabilities/center-prompts.js +18 -32
  52. package/dist/capabilities/composite-members.d.ts +1 -0
  53. package/dist/capabilities/composite-members.js +1 -0
  54. package/dist/capabilities/engine-capabilities.js +2 -1
  55. package/dist/capabilities/hands-lane.d.ts +9 -6
  56. package/dist/capabilities/hands-lane.js +3 -3
  57. package/dist/capabilities/host-tool-names.d.ts +17 -0
  58. package/dist/capabilities/host-tool-names.js +15 -0
  59. package/dist/capabilities/mcp-probe.d.ts +14 -1
  60. package/dist/capabilities/mcp-probe.js +22 -133
  61. package/dist/capabilities/repo-tools.js +4 -3
  62. package/dist/capabilities/scenarios.d.ts +42 -47
  63. package/dist/capabilities/scenarios.js +19 -35
  64. package/dist/capabilities/select-environment-tool.js +2 -1
  65. package/dist/capabilities/send-user-file-tool.d.ts +0 -1
  66. package/dist/capabilities/send-user-file-tool.js +2 -2
  67. package/dist/capabilities/skills.d.ts +19 -4
  68. package/dist/capabilities/skills.js +30 -10
  69. package/dist/capabilities/tool-defer.js +2 -1
  70. package/dist/config-catalog.js +22 -11
  71. package/dist/config-invariants.js +2 -2
  72. package/dist/config-lkg.d.ts +9 -9
  73. package/dist/config-lkg.js +20 -4
  74. package/dist/config-provider.d.ts +3 -1
  75. package/dist/config-provider.js +14 -6
  76. package/dist/config-types.d.ts +13 -9
  77. package/dist/config.d.ts +2 -2
  78. package/dist/config.js +34 -20
  79. package/dist/device-ws-protocol.d.ts +1 -1
  80. package/dist/device-ws-protocol.js +1 -0
  81. package/dist/elicitation.d.ts +7 -0
  82. package/dist/elicitation.js +20 -5
  83. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  84. package/dist/env-name-allowlist-knobs.js +5 -4
  85. package/dist/exec-bounds.d.ts +15 -3
  86. package/dist/exec-bounds.js +4 -0
  87. package/dist/execution-lane-caps.d.ts +9 -2
  88. package/dist/execution-lane-caps.js +11 -0
  89. package/dist/hooks/classic-stations.d.ts +86 -0
  90. package/dist/hooks/classic-stations.js +27 -0
  91. package/dist/hooks/hook-llm.d.ts +8 -5
  92. package/dist/hooks/hook-llm.js +80 -37
  93. package/dist/hooks/hook-runner.d.ts +273 -75
  94. package/dist/hooks/hook-runner.js +293 -317
  95. package/dist/http/admission.d.ts +8 -2
  96. package/dist/http/admission.js +103 -59
  97. package/dist/http/approval-sweeps.js +59 -18
  98. package/dist/http/checkpoint-ctx.d.ts +36 -18
  99. package/dist/http/checkpoint-ctx.js +14 -10
  100. package/dist/http/claimed-run.d.ts +13 -0
  101. package/dist/http/claimed-run.js +15 -0
  102. package/dist/http/device-placement.js +5 -10
  103. package/dist/http/dispatch.d.ts +0 -4
  104. package/dist/http/dispatch.js +6 -10
  105. package/dist/http/durable-run-leg.d.ts +2 -1
  106. package/dist/http/durable-run-leg.js +5 -3
  107. package/dist/http/health-self-report.d.ts +7 -5
  108. package/dist/http/health-self-report.js +4 -18
  109. package/dist/http/principal-gate.d.ts +23 -6
  110. package/dist/http/principal-gate.js +24 -5
  111. package/dist/http/refusal-body.d.ts +21 -0
  112. package/dist/http/refusal-body.js +4 -0
  113. package/dist/http/request-guards.d.ts +27 -0
  114. package/dist/http/request-guards.js +24 -3
  115. package/dist/http/resume-legs.d.ts +25 -0
  116. package/dist/http/resume-legs.js +48 -26
  117. package/dist/http/routes/approvals-assistant.js +1 -1
  118. package/dist/http/routes/capabilities.js +7 -17
  119. package/dist/http/routes/diagnostics.js +1 -1
  120. package/dist/http/routes/rules.js +2 -2
  121. package/dist/http/routes/runs.js +13 -17
  122. package/dist/http/routes/session-sync.js +4 -2
  123. package/dist/http/routes/sessions.js +23 -11
  124. package/dist/http/routes/shared-memory.js +2 -13
  125. package/dist/http/routes/side-query.js +2 -3
  126. package/dist/http/routes/tasks.js +34 -19
  127. package/dist/http/scenario-read-face.d.ts +14 -0
  128. package/dist/http/scenario-read-face.js +5 -0
  129. package/dist/http/send.d.ts +13 -1
  130. package/dist/http/send.js +12 -1
  131. package/dist/http/server.d.ts +37 -23
  132. package/dist/http/server.js +30 -4
  133. package/dist/http/stored-body-replay.d.ts +10 -3
  134. package/dist/http/stored-body-replay.js +27 -6
  135. package/dist/http/submit-ctx.d.ts +25 -0
  136. package/dist/http/submit-ctx.js +39 -0
  137. package/dist/http/wire-types.d.ts +18 -5
  138. package/dist/index.d.ts +3 -2
  139. package/dist/index.js +3 -2
  140. package/dist/lane-home-probe.d.ts +43 -0
  141. package/dist/lane-home-probe.js +35 -0
  142. package/dist/limit-refusal.d.ts +47 -0
  143. package/dist/limit-refusal.js +12 -0
  144. package/dist/main.js +1 -1
  145. package/dist/mcp-assertion-header.d.ts +3 -11
  146. package/dist/mcp-assertion-header.js +3 -9
  147. package/dist/mcp-tool-faces.d.ts +5 -0
  148. package/dist/mcp-tool-faces.js +5 -1
  149. package/dist/memory-scope.d.ts +1 -1
  150. package/dist/model-entry-refusal.d.ts +1 -7
  151. package/dist/model-provider.d.ts +39 -12
  152. package/dist/model-provider.js +19 -2
  153. package/dist/model-route-endpoint.d.ts +52 -12
  154. package/dist/model-route-endpoint.js +48 -26
  155. package/dist/model-route-gap.d.ts +5 -4
  156. package/dist/model-route-gap.js +1 -1
  157. package/dist/model-select.d.ts +58 -0
  158. package/dist/model-select.js +31 -0
  159. package/dist/namespaced-rule-name.d.ts +7 -4
  160. package/dist/namespaced-rule-name.js +4 -3
  161. package/dist/observability/cost-taxonomy.d.ts +7 -7
  162. package/dist/observability/fail-open.d.ts +53 -12
  163. package/dist/observability/fail-open.js +47 -7
  164. package/dist/observability/metrics.js +3 -1
  165. package/dist/observability/prompt-manifest.d.ts +47 -82
  166. package/dist/observability/prompt-manifest.js +119 -17
  167. package/dist/observability/run-terminal.js +14 -6
  168. package/dist/observability/secret-env-scrub.d.ts +13 -1
  169. package/dist/observability/secret-env-scrub.js +13 -1
  170. package/dist/observability/tool-trace.d.ts +16 -21
  171. package/dist/observability/tool-trace.js +51 -42
  172. package/dist/per-task-image.d.ts +7 -1
  173. package/dist/per-task-image.js +3 -3
  174. package/dist/plain-string-record.d.ts +25 -0
  175. package/dist/plain-string-record.js +32 -0
  176. package/dist/plugins/blob-backend.d.ts +68 -13
  177. package/dist/plugins/blob-backend.js +53 -10
  178. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  179. package/dist/plugins/checkpoint-store-sql.js +39 -26
  180. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  181. package/dist/plugins/k8s-bg-scripts.js +22 -0
  182. package/dist/plugins/local-checkpoint-store.js +5 -2
  183. package/dist/plugins/local-session-store.js +3 -3
  184. package/dist/plugins/one-time-migrations.d.ts +147 -91
  185. package/dist/plugins/one-time-migrations.js +372 -208
  186. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  187. package/dist/plugins/permission-rule-store-sql.js +4 -18
  188. package/dist/plugins/pg-session-storage.d.ts +4 -5
  189. package/dist/plugins/pg-session-storage.js +7 -17
  190. package/dist/plugins/remote-env-adb.d.ts +6 -0
  191. package/dist/plugins/remote-env-adb.js +9 -4
  192. package/dist/plugins/remote-env-device.js +3 -0
  193. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  194. package/dist/plugins/remote-env-e2b.js +21 -9
  195. package/dist/plugins/remote-env-host.d.ts +9 -9
  196. package/dist/plugins/remote-env-host.js +10 -32
  197. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  198. package/dist/plugins/remote-env-k8s.js +265 -90
  199. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  200. package/dist/plugins/remote-env-local-docker.js +77 -9
  201. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  202. package/dist/plugins/remote-env-ssh.js +109 -3
  203. package/dist/plugins/remote-shell.d.ts +0 -1
  204. package/dist/plugins/remote-shell.js +1 -2
  205. package/dist/plugins/retention-store-sql.js +6 -2
  206. package/dist/plugins/schema-consistency.d.ts +54 -0
  207. package/dist/plugins/schema-consistency.js +52 -0
  208. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  209. package/dist/plugins/session-epoch-anchor.js +23 -0
  210. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  211. package/dist/plugins/shared-memory-store-sql.js +5 -16
  212. package/dist/plugins/sql-driver.d.ts +141 -0
  213. package/dist/plugins/sql-driver.js +115 -1
  214. package/dist/plugins/sql-errors.d.ts +41 -0
  215. package/dist/plugins/sql-errors.js +36 -0
  216. package/dist/plugins/store-backend.js +2 -1
  217. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  218. package/dist/plugins/tidb-session-storage.js +6 -16
  219. package/dist/plugins/tidb-session-store.js +1 -1
  220. package/dist/prompts-domain-validate.d.ts +17 -61
  221. package/dist/prompts-domain-validate.js +9 -119
  222. package/dist/question.d.ts +18 -9
  223. package/dist/question.js +21 -13
  224. package/dist/request-key-closure.d.ts +4 -4
  225. package/dist/request-key-closure.js +18 -13
  226. package/dist/rule-entry-list.d.ts +134 -24
  227. package/dist/rule-entry-list.js +162 -30
  228. package/dist/run-local.d.ts +3 -3
  229. package/dist/run-local.js +26 -25
  230. package/dist/runs.d.ts +48 -2
  231. package/dist/runs.js +61 -2
  232. package/dist/server-secret-env.d.ts +44 -18
  233. package/dist/server-secret-env.js +49 -8
  234. package/dist/session-incarnation.d.ts +15 -5
  235. package/dist/session-incarnation.js +16 -9
  236. package/dist/session-titler.js +12 -2
  237. package/dist/spec-fields.d.ts +34 -18
  238. package/dist/spec-fields.js +46 -53
  239. package/dist/sql-engine-posture.d.ts +29 -4
  240. package/dist/sql-engine-posture.js +36 -1
  241. package/dist/stored-request-hooks-m20.d.ts +23 -0
  242. package/dist/stored-request-hooks-m20.js +124 -0
  243. package/dist/stored-request-tool-axes-m19.d.ts +20 -5
  244. package/dist/stored-request-tool-axes-m19.js +30 -2
  245. package/dist/task-a2a.d.ts +9 -1
  246. package/dist/task-a2a.js +73 -47
  247. package/dist/task-mcp.d.ts +54 -102
  248. package/dist/task-mcp.js +194 -92
  249. package/dist/task-settings.d.ts +47 -25
  250. package/dist/task-settings.js +46 -61
  251. package/dist/text-bidi.d.ts +3 -46
  252. package/dist/text-bidi.js +3 -3
  253. package/dist/tool-approval.d.ts +1 -1
  254. package/dist/trace/core-keyset-guard.d.ts +24 -4
  255. package/dist/trace/engine-notice-wire.d.ts +11 -3
  256. package/dist/trace/engine-notice-wire.js +8 -1
  257. package/dist/trace/ledger-events.d.ts +3 -1
  258. package/dist/trace/ledger-sink.d.ts +29 -0
  259. package/dist/trace/ledger-sink.js +15 -1
  260. package/dist/trace/model-segment.js +1 -1
  261. package/dist/trace/project.d.ts +25 -6
  262. package/dist/trace/project.js +68 -22
  263. package/dist/trace/wire-projection-faces.d.ts +80 -2
  264. package/dist/trace/wire-projection-faces.js +23 -1
  265. package/dist/user-authored-text.d.ts +7 -0
  266. package/dist/user-authored-text.js +3 -0
  267. package/dist/web-admin-client/apply-effective.js +10 -5
  268. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  269. package/dist/web-admin-client/candidate-gate.js +3 -3
  270. package/dist/web-admin-client/http-client.js +0 -6
  271. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  272. package/dist/web-admin-client/read-warnings.js +10 -0
  273. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  274. package/dist/web-admin-client/skills-mcp.js +9 -6
  275. package/dist/web-admin-client/types.d.ts +3 -4
  276. package/dist/workspace-cache.d.ts +35 -0
  277. package/dist/workspace-cache.js +36 -0
  278. package/dist/workspace-space.d.ts +3 -2
  279. package/dist/workspace-space.js +2 -0
  280. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  281. package/package.json +4 -4
  282. package/scripts/drain-check-local.mjs +37 -3
  283. package/scripts/drain-check.mysql.sql +30 -2
  284. package/scripts/drain-check.pg.sql +29 -2
  285. package/dist/capabilities/memory-notice.d.ts +0 -138
  286. package/dist/capabilities/memory-notice.js +0 -64
  287. package/dist/capabilities/prompt.d.ts +0 -17
  288. package/dist/capabilities/prompt.js +0 -6
  289. package/dist/mcp-content-origin.d.ts +0 -34
  290. package/dist/mcp-content-origin.js +0 -10
  291. package/dist/tool-axis-words.d.ts +0 -27
  292. package/dist/tool-axis-words.js +0 -9
  293. package/dist/wire-name-bound.d.ts +0 -14
  294. package/dist/wire-name-bound.js +0 -13
@@ -1,23 +1,29 @@
1
- import { compileAllowList } from "@sema-agent/core";
1
+ import { PROTOCOL_TABLE, compileAllowList, parsePermissionRule } from "@sema-agent/core";
2
2
  import { displayListEntry } from "./list-entry-display.js";
3
- import { NO_DECLARED_TOOL_NAMES } from "./namespaced-rule-name.js";
3
+ import { limitRefusal } from "./limit-refusal.js";
4
+ import { TOOL_NAME_ROSTER } from "./namespaced-rule-name.js";
4
5
  export const ruleEntryListShapeOk = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
5
6
  export const MAX_ALLOW_LAYER_ENTRIES = 1_000;
7
+ const ALLOW_LAYER_OVER_CAP_TAIL = "Refused rather than truncated: a truncated list silently drops the entries past the cap. Merge entries into broader ones (mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**)) or trim the list";
8
+ function allowLayerOverCap(field, count) {
9
+ return limitRefusal(field, "entries", count, MAX_ALLOW_LAYER_ENTRIES, { tail: ALLOW_LAYER_OVER_CAP_TAIL });
10
+ }
6
11
  export function describeAllowLayerOverCap(field, count) {
7
- return (`${field} has ${count} entries — at most ${MAX_ALLOW_LAYER_ENTRIES} are accepted. Refused rather than truncated: a truncated list silently drops the entries past the cap. ` +
8
- `Merge entries into broader ones (mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**)) or trim the list`);
12
+ return allowLayerOverCap(field, count).message;
9
13
  }
10
14
  export function allowLayerEntryCountIssue(field, list) {
11
- if (list.length <= MAX_ALLOW_LAYER_ENTRIES)
12
- return undefined;
13
- return { message: describeAllowLayerOverCap(field, list.length), count: list.length, cap: MAX_ALLOW_LAYER_ENTRIES };
15
+ return list.length <= MAX_ALLOW_LAYER_ENTRIES ? undefined : allowLayerOverCap(field, list.length);
14
16
  }
15
17
  export function inertRuleEntriesOf(list, behavior) {
16
- return compileAllowList(list, behavior, NO_DECLARED_TOOL_NAMES).inert;
18
+ return compileAllowList(list, behavior, TOOL_NAME_ROSTER).inert;
19
+ }
20
+ function peerSegmentIsWild(entry) {
21
+ const at = entry.indexOf("__");
22
+ return at >= 0 && (entry.slice(at + 2).split("__")[0] ?? "").includes("*");
17
23
  }
18
24
  export function inertRuleEntryGuidance(i) {
19
25
  const reason = i.reason;
20
- const write = i.currentName !== undefined ? ` — write ${displayListEntry(i.currentName)}` : "";
26
+ const write = i.currentName !== undefined && !peerSegmentIsWild(i.entry) ? ` — write ${displayListEntry(i.currentName)}` : "";
21
27
  switch (reason) {
22
28
  case "tool_name":
23
29
  return `the tool name can never match a mounted tool (${i.verdict ?? "stale"})${write}`;
@@ -39,30 +45,87 @@ export function inertRuleEntryGuidance(i) {
39
45
  }
40
46
  }
41
47
  }
48
+ export const MAX_NAMED_INERT_ENTRIES = 5;
42
49
  export function describeInertRuleEntries(field, inert) {
43
- return inert.map((i) => `${field} entry ${displayListEntry(i.entry)} reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i)}`).join("; ");
50
+ const named = inert.slice(0, MAX_NAMED_INERT_ENTRIES).map((i) => `${field} entry ${displayListEntry(i.entry)} reads as nothing (${i.reason}): ${inertRuleEntryGuidance(i)}`).join("; ");
51
+ const more = inert.length - MAX_NAMED_INERT_ENTRIES;
52
+ return more > 0 ? `${named}; and ${more} more` : named;
53
+ }
54
+ const TOOL_LIST_FACES = {
55
+ "skills": { side: "deny", kind: "rule_table", code: "request.field_invalid" },
56
+ "agents.skills": { side: "deny", kind: "rule_table", code: "request.field_invalid" },
57
+ "skills.allowedTools": { side: "allow", kind: "rule_table", code: "request.field_invalid" },
58
+ "agents.skills.allowedTools": { side: "allow", kind: "rule_table", code: "request.field_invalid" },
59
+ "agents.denyTools": { side: "deny", kind: "selector", code: "config.agent.unknown_tool" },
60
+ "agents.allowTools": { side: "allow", kind: "selector", code: "config.agent.unknown_tool" },
61
+ "excludeTools": { side: "deny", kind: "selector", code: "config.tool_face_control" },
62
+ "allowTools": { side: "allow", kind: "selector", code: "config.tool_face_control" },
63
+ "settings.permissions.allow": { side: "allow", kind: "rule_table", code: "request.field_invalid" },
64
+ };
65
+ const LIST_OVER_CAP_CODE = "request.field_invalid";
66
+ export function replayInertEntry(layer, side, code, message, extra) {
67
+ return { layer, side, code, message, ...(extra !== undefined ? { extra } : {}) };
44
68
  }
45
69
  const isPlainRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
46
- function skillDenyInertSentence(skill) {
47
- if (!isPlainRecord(skill))
48
- return undefined;
49
- const deny = skill["disallowedTools"];
50
- if (!ruleEntryListShapeOk(deny))
51
- return undefined;
52
- const inert = inertRuleEntriesOf(deny, "deny");
53
- if (inert.length === 0)
54
- return undefined;
55
- return `${describeInertRuleEntries("skill.disallowedTools", inert)} — a deny entry that reads as nothing refuses nothing, so it is refused here rather than taken as in effect`;
70
+ const REFUSAL_TAIL = {
71
+ deny: "a deny entry that reads as nothing refuses nothing, so it is refused here rather than taken as in effect",
72
+ allow: "an allow entry that reads as nothing admits nothing, so it is refused here rather than taken as in effect",
73
+ };
74
+ function selectorEntryName(entry) {
75
+ return entry !== entry.trim() ? entry : parsePermissionRule(entry).toolName;
56
76
  }
57
- export function skillDenyInertEntryOf(body) {
58
- if (!isPlainRecord(body))
59
- return undefined;
77
+ const isProtocolWideName = (name) => PROTOCOL_TABLE.some((e) => name === `${e.prefix}*`);
78
+ function selectorInertEntriesOf(list, side) {
79
+ return list.flatMap((entry) => {
80
+ if (entry === "*" && side === "allow")
81
+ return [];
82
+ const name = selectorEntryName(entry);
83
+ if (side === "deny" && isProtocolWideName(name))
84
+ return [];
85
+ const inert = compileAllowList([name], "deny", TOOL_NAME_ROSTER).inert.map((i) => ({ ...i, entry }));
86
+ return side === "allow" ? inert.filter((i) => allowSideReadsAsNothing(i.reason)) : inert;
87
+ });
88
+ }
89
+ function allowSideReadsAsNothing(reason) {
90
+ switch (reason) {
91
+ case "near_miss":
92
+ return true;
93
+ case "grammar":
94
+ case "tool_name":
95
+ case "content_unsupported":
96
+ case "wildcard_all":
97
+ case "namespace_wildcard":
98
+ return false;
99
+ case "earlier_grammar":
100
+ throw new Error(`allow-entry inert reason ${JSON.stringify(reason)} belongs to the session-rule store's bucket reading, never to an allow list`);
101
+ default: {
102
+ const unhandled = reason;
103
+ throw new Error(`unhandled allow-entry inert reason: ${String(unhandled)}`);
104
+ }
105
+ }
106
+ }
107
+ function requestAllowRuleInertEntriesOf(list) {
108
+ return list.flatMap((entry) => compileAllowList([entry], "allow", TOOL_NAME_ROSTER).inert.filter((i) => allowSideReadsAsNothing(i.reason)));
109
+ }
110
+ function inertEntriesOfList(list, layer) {
111
+ const face = TOOL_LIST_FACES[layer];
112
+ if (face.kind === "selector")
113
+ return selectorInertEntriesOf(list, face.side);
114
+ return face.side === "deny" ? inertRuleEntriesOf(list, "deny") : requestAllowRuleInertEntriesOf(list);
115
+ }
116
+ function* toolListsOf(body) {
60
117
  const skills = body["skills"];
61
118
  if (Array.isArray(skills)) {
62
- for (const skill of skills) {
63
- const sentence = skillDenyInertSentence(skill);
64
- if (sentence !== undefined)
65
- return { layer: "skills", message: sentence };
119
+ for (let k = 0; k < skills.length; k++) {
120
+ const skill = skills[k];
121
+ if (!isPlainRecord(skill))
122
+ continue;
123
+ yield { layer: "skills", field: "skill.disallowedTools", prefix: "", at: `skills[${k}].disallowedTools`, list: skill["disallowedTools"] };
124
+ }
125
+ for (let k = 0; k < skills.length; k++) {
126
+ const skill = skills[k];
127
+ if (isPlainRecord(skill))
128
+ yield { layer: "skills.allowedTools", field: "skill.allowedTools", prefix: "", at: `skills[${k}].allowedTools`, list: skill["allowedTools"] };
66
129
  }
67
130
  }
68
131
  const agents = body["agents"];
@@ -73,12 +136,81 @@ export function skillDenyInertEntryOf(body) {
73
136
  if (!Array.isArray(agentSkills))
74
137
  continue;
75
138
  for (let j = 0; j < agentSkills.length; j++) {
76
- const sentence = skillDenyInertSentence(agentSkills[j]);
77
- if (sentence !== undefined)
78
- return { layer: "agents.skills", message: `agents[${i}]: skills[${j}]: ${sentence}` };
139
+ const skill = agentSkills[j];
140
+ if (isPlainRecord(skill))
141
+ yield { layer: "agents.skills", field: "skill.disallowedTools", prefix: `agents[${i}]: skills[${j}]: `, at: `agents[${i}].skills[${j}].disallowedTools`, list: skill["disallowedTools"] };
79
142
  }
80
143
  }
144
+ for (let i = 0; i < agents.length; i++) {
145
+ const agent = agents[i];
146
+ const agentSkills = isPlainRecord(agent) ? agent["skills"] : undefined;
147
+ if (!Array.isArray(agentSkills))
148
+ continue;
149
+ for (let j = 0; j < agentSkills.length; j++) {
150
+ const skill = agentSkills[j];
151
+ if (isPlainRecord(skill))
152
+ yield { layer: "agents.skills.allowedTools", field: "skill.allowedTools", prefix: `agents[${i}]: skills[${j}]: `, at: `agents[${i}].skills[${j}].allowedTools`, list: skill["allowedTools"] };
153
+ }
154
+ }
155
+ for (let i = 0; i < agents.length; i++) {
156
+ const agent = agents[i];
157
+ if (!isPlainRecord(agent))
158
+ continue;
159
+ yield { layer: "agents.denyTools", field: `agents[${i}].denyTools`, prefix: "", at: `agents[${i}].denyTools`, list: agent["denyTools"] };
160
+ yield { layer: "agents.allowTools", field: `agents[${i}].allowTools`, prefix: "", at: `agents[${i}].allowTools`, list: agent["allowTools"] };
161
+ }
162
+ }
163
+ yield { layer: "excludeTools", field: "excludeTools", prefix: "", at: "excludeTools", list: body["excludeTools"] };
164
+ yield { layer: "allowTools", field: "allowTools", prefix: "", at: "allowTools", list: body["allowTools"] };
165
+ const settings = body["settings"];
166
+ const permissions = isPlainRecord(settings) ? settings["permissions"] : undefined;
167
+ if (isPlainRecord(permissions))
168
+ yield { layer: "settings.permissions.allow", field: "settings.permissions.allow", prefix: "", at: "settings.permissions.allow", list: permissions["allow"] };
169
+ }
170
+ function entryOfList(l) {
171
+ const face = TOOL_LIST_FACES[l.layer];
172
+ const over = overCapOfList(l);
173
+ if (over !== undefined)
174
+ return over;
175
+ if (!ruleEntryListShapeOk(l.list))
176
+ return undefined;
177
+ const inert = inertEntriesOfList(l.list, l.layer);
178
+ if (inert.length === 0)
179
+ return undefined;
180
+ return { layer: l.layer, side: face.side, code: face.code, message: `${l.prefix}${describeInertRuleEntries(l.field, inert)} — ${REFUSAL_TAIL[face.side]}` };
181
+ }
182
+ function overCapOfList(l) {
183
+ if (!Array.isArray(l.list))
184
+ return undefined;
185
+ const over = allowLayerEntryCountIssue(l.at, l.list);
186
+ return over === undefined ? undefined : { layer: l.layer, side: TOOL_LIST_FACES[l.layer].side, code: LIST_OVER_CAP_CODE, message: over.message, extra: over.facts };
187
+ }
188
+ export function skillToolListsOverCap(skill, at, scope) {
189
+ if (!isPlainRecord(skill))
190
+ return undefined;
191
+ const allowLayer = scope === "top" ? "skills.allowedTools" : "agents.skills.allowedTools";
192
+ const denyLayer = scope === "top" ? "skills" : "agents.skills";
193
+ return (overCapOfList({ layer: allowLayer, field: "skill.allowedTools", prefix: "", at: `${at}.allowedTools`, list: skill["allowedTools"] }) ??
194
+ overCapOfList({ layer: denyLayer, field: "skill.disallowedTools", prefix: "", at: `${at}.disallowedTools`, list: skill["disallowedTools"] }));
195
+ }
196
+ function firstInertEntry(body, acceptLayer, acceptSide) {
197
+ if (!isPlainRecord(body))
198
+ return undefined;
199
+ for (const l of toolListsOf(body)) {
200
+ if (!acceptLayer(l.layer))
201
+ continue;
202
+ const entry = entryOfList(l);
203
+ if (entry === undefined)
204
+ continue;
205
+ if (entry.extra !== undefined || acceptSide(entry.side))
206
+ return entry;
81
207
  }
82
208
  return undefined;
83
209
  }
210
+ export function skillDenyInertEntryOf(body) {
211
+ return firstInertEntry(body, (layer) => layer === "skills" || layer === "agents.skills", () => true);
212
+ }
213
+ export function inertToolListEntryOf(body, sides = ["allow", "deny"]) {
214
+ return firstInertEntry(body, () => true, (side) => sides.includes(side));
215
+ }
84
216
  //# sourceMappingURL=rule-entry-list.js.map
@@ -54,15 +54,15 @@ export declare class UsageError extends Error {
54
54
  * set: …" list rather than letting the first brain call crash with a cryptic auth error. Returns a list of
55
55
  * missing items (empty = OK).
56
56
  *
57
- * 🔴 S-623:这里**不再**判网关 base URL。「配置能派发到的每一条 openai 路由都解析到显式端点」是 `loadConfig()`
58
- * 那一层的拒启判据(`model-route-endpoint.ts`,本入口在预检之前就调 `loadConfig()` —— 没设网关的配置根本走不到
57
+ * 🔴 S-623:这里**不再**判网关 base URL。「将要服务的那张表里每一条 openai 路由都解析到显式端点」是配置源应用之后的拒启判据
58
+ * (`model-route-endpoint.ts` 的 `judgeServedModelRoute`,本入口在预检之前就判过 —— 没设网关的配置根本走不到
59
59
  * 这里),原来那条臂(a)是它的一份更弱的手抄:注释要求「本机用户必须指向自己的网关」,判据却只查 `/v1` 后缀,
60
60
  * 旧的出厂缺省值恰好以 `/v1` 结尾 ⇒ 缺省值照样过(本仓自己也被骗)。`/v1` 后缀本身也一并删,理由三条:
61
61
  * ① 同一个问题两个判官 —— `serve` 从不要求 `/v1`,同一份 .env 在 `serve` 起得来、在 `run-local` 被拒;
62
62
  * ② 它会误拒真实存在的 OpenAI 兼容端点(DeepSeek 根路径直接服务 `/chat/completions`,本仓
63
63
  * `src/bench/s1/live-deps.ts` 用的正是 `https://api.deepseek.com`;Gemini 的兼容面 `…/v1beta/openai`、Azure 的
64
64
  * `…/openai/deployments/<name>` 也都不以 `/v1` 结尾);
65
- * ③ 规则集只许变小:端点判据只有 loadConfig 那一只。
65
+ * ③ 规则集只许变小:端点判据只有 `model-route-endpoint.ts` 那一只。
66
66
  *
67
67
  * NOTE: with an injected mock `brain` the gateway is irrelevant (no real call is made), so the caller skips
68
68
  * this check in that case — the check is about a REAL run, not the assembly.
package/dist/run-local.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { DEFAULT_AT_MENTION, DEFAULT_MODEL_REF, resolveTaskModel } from "./model-select.js";
3
3
  import { defaultModelRefusal, modelUnavailableMessage } from "./model-entry-refusal.js";
4
+ import { servedModelTableLeg } from "./model-select.js";
4
5
  import { createDurableAbsenceSeat } from "./observability/corrupt-read-seat.js";
5
6
  import { outwardPlane } from "./terminal.js";
6
7
  import { join } from "node:path";
7
8
  import { realpathSync } from "node:fs";
8
9
  import { createInterface } from "node:readline/promises";
9
10
  import { fileURLToPath } from "node:url";
10
- import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
11
+ import { BIDI_CONTROL_CHARS, Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
11
12
  import { readAskClassPair } from "./approval-card.js";
12
13
  import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
13
14
  import { withUnattendedLeg } from "./unattended-permission-mode.js";
@@ -17,14 +18,13 @@ import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, buildAppr
17
18
  import { applyRuntimeGovernance, autonomyModeFloor, entryPermissionMode } from "./runtime-governance.js";
18
19
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
19
20
  import { taskWallClockSec } from "./task-workflow.js";
20
- import { applyCatalogToSource } from "./capabilities/center-prompts.js";
21
- import { CORE_ENGINE_VERSION } from "./prompts-domain-validate.js";
21
+ import { applyCenterArtifact, centerSourceOnError } from "./capabilities/center-prompts.js";
22
22
  import { buildUnpricedCeilingNotice, loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./config.js";
23
23
  import { buildStoreOpenRefusalTail } from "./plugins/store-open-refusal.js";
24
24
  import { acquireLocalBackendWithWait } from "./plugins/store-lock-wait.js";
25
25
  import { configLocalRoot, configProviderKind, createConfigProvider } from "./config-provider.js";
26
26
  import { CONFIG_DOMAIN_INVALID_NOTE, buildCandidateRejectedFields, judgeConfigCandidate } from "./web-admin-client/candidate-gate.js";
27
- import { buildUnreadConfigFileFields } from "./web-admin-client/read-warnings.js";
27
+ import { buildUnreadConfigFileFields, buildUnreadConfigKeyFields } from "./web-admin-client/read-warnings.js";
28
28
  import { composeExecutionLane, settleHostLaneShell } from "./boot/execution-lane.js";
29
29
  import { judgeConsolidationValve } from "./boot/memory-consolidation.js";
30
30
  import { logConfigDirMigration, migrateLocalConfigDirectory } from "./config-d-migration.js";
@@ -36,20 +36,20 @@ import { createInstructionSourceSeat } from "./instruction-source-notice.js";
36
36
  import { loadDeploymentSkills } from "./capabilities/skills.js";
37
37
  import { createRepoClient } from "./capabilities/repo-tools.js";
38
38
  import { webSearchConfigFromEnv, createWebSearchBackend } from "./plugins/web-search.js";
39
- import { buildScenarios, gateScenarioRequest, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
39
+ import { buildPromptSeats, buildScenarios, createScenarioGate, selectScenario, centerScenarios } from "./capabilities/scenarios.js";
40
40
  import { createTaskListLane } from "./boot/task-list-lane.js";
41
41
  import { pickHandsRunner, withoutHands } from "./capabilities/hands-lane.js";
42
42
  import { HttpError, sessionIdShapeIssue } from "./security.js";
43
43
  import { createMemoryEngineIncidentSink } from "./memory-incident-sink.js";
44
44
  import { fileMemoryEngineRoot, memoryEngineBackendFor, memoryProjectKeyFor, memorySpecForRequest } from "./memory-scope.js";
45
- import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
45
+ import { judgeServedModelRoute } from "./model-route-endpoint.js";
46
46
  import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
47
47
  import { buildPricing, cappedCeiling, createTracer } from "./budget.js";
48
48
  import { createSharedRunnerDeps } from "./boot/runner-deps.js";
49
49
  import { createGovernanceSeams } from "./boot/governance-seams.js";
50
50
  import { assertRetentionLaneWirable } from "./boot/retention-lane.js";
51
51
  import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
52
- import { createPermissionDeniedMeter } from "./observability/tool-trace.js";
52
+ import { assertDeploymentModsAdmitted, createDeploymentMods } from "./boot/deployment-mods.js";
53
53
  import { createKeyResolver } from "./key-resolver.js";
54
54
  import { createLogger } from "./observability/logger.js";
55
55
  import { fatalExit } from "./boot/fatal-exit.js";
@@ -253,8 +253,9 @@ function argsDisplayOf(args) {
253
253
  function buildLocalAskPrompt(req, argsShown) {
254
254
  return `approve "${terminalSafeText(req.toolName)}"? ${terminalSafeText(req.message)}\n ${argsShown}\n[y/N] `;
255
255
  }
256
+ const TERMINAL_UNSAFE_RE = new RegExp(`[\\u0000-\\u001f\\u007f-\\u009f${BIDI_CONTROL_CHARS}\\u200b-\\u200d\\u2028\\u2029\\u2060-\\u2065\\ufeff]`, "g");
256
257
  function terminalSafeText(text) {
257
- return text.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028-\u202e\u2060-\u2069\ufeff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
258
+ return text.replace(TERMINAL_UNSAFE_RE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
258
259
  }
259
260
  function inputAlreadyEnded(input) {
260
261
  const state = input;
@@ -365,6 +366,8 @@ export async function runLocal(argv, deps = {}) {
365
366
  else {
366
367
  for (const f of r.unreadFiles ?? [])
367
368
  logger.warn("config_unread_file", buildUnreadConfigFileFields(f, configProvider.kind === "local" ? root : undefined));
369
+ for (const k of r.unreadKeys ?? [])
370
+ logger.warn("config_unread_key", buildUnreadConfigKeyFields(k, configProvider.kind === "local" ? root : undefined));
368
371
  for (const d of r.droppedEntries ?? [])
369
372
  logger.warn("config_entry_dropped", { domain: d.domain, entry: d.entry, reasonCode: d.reasonCode, error: d.error, version: r.effective.version, phase: "boot", note: "this one entry failed its domain schema and was dropped on its own — the rest of the candidate applies (per-entry tolerance); fix the named entry to bring it back" });
370
373
  applying = true;
@@ -386,12 +389,12 @@ export async function runLocal(argv, deps = {}) {
386
389
  else
387
390
  logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
388
391
  }
389
- const noDefaultModel = defaultModelRefusal(config, "env");
392
+ const noDefaultModel = defaultModelRefusal(config, servedModelTableLeg(config));
390
393
  if (noDefaultModel !== undefined)
391
394
  throw new Error(noDefaultModel);
392
- const endpointRefusal = modelRouteEndpointRefusal(config);
393
- if (endpointRefusal !== undefined)
394
- throw new Error(endpointRefusal);
395
+ const routeVerdict = judgeServedModelRoute(config, { envTableIsPlaceholder: false });
396
+ if (routeVerdict !== undefined)
397
+ throw new Error(routeVerdict.reason);
395
398
  const unpricedCeiling = buildUnpricedCeilingNotice(config.models, config);
396
399
  if (unpricedCeiling)
397
400
  logger.warn(unpricedCeiling.event, unpricedCeiling.fields);
@@ -487,25 +490,25 @@ export async function runLocal(argv, deps = {}) {
487
490
  return new CenterPromptSource({
488
491
  store: new FilePromptArtifactStore(promptEpochDir),
489
492
  stateStore: new FilePromptSourceStateStore(promptEpochDir),
490
- engineVersion: CORE_ENGINE_VERSION,
493
+ onError: centerSourceOnError(logger),
491
494
  });
492
495
  }
493
496
  catch (e) {
494
497
  logger.warn("center_prompts_store_unwritable", { dir: promptEpochDir, err: String(e), note: "file-backed prompt-epoch store unavailable — memory fallback; built-ins unaffected" });
495
- return new CenterPromptSource({ store: new MemoryPromptArtifactStore(), stateStore: new MemoryPromptSourceStateStore(), engineVersion: CORE_ENGINE_VERSION });
498
+ return new CenterPromptSource({ store: new MemoryPromptArtifactStore(), stateStore: new MemoryPromptSourceStateStore(), onError: centerSourceOnError(logger) });
496
499
  }
497
500
  })();
498
501
  try {
499
502
  await promptSource.restore();
500
503
  if (candidatePrompts !== undefined) {
501
- await applyCatalogToSource(promptSource, candidatePrompts, undefined, logger, "run-local");
504
+ await applyCenterArtifact(promptSource, candidatePrompts, undefined, logger, "run-local");
502
505
  }
503
506
  else {
504
- await applyCatalogToSource(promptSource, undefined, undefined, logger, "run-local");
507
+ await applyCenterArtifact(promptSource, undefined, undefined, logger, "run-local");
505
508
  }
506
509
  }
507
510
  catch (e) {
508
- logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "catalog candidate absent — built-ins serve" });
511
+ logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "center candidate absent — built-ins serve" });
509
512
  }
510
513
  let refusedWithoutPerson = 0;
511
514
  const onAsk = createLocalApprover({
@@ -558,6 +561,8 @@ export async function runLocal(argv, deps = {}) {
558
561
  { name: "toolResultStore", store: fileBackend.toolResultStore },
559
562
  ],
560
563
  });
564
+ const deploymentMods = createDeploymentMods({ metrics, brandIdentity: true });
565
+ await assertDeploymentModsAdmitted(deploymentMods);
561
566
  const sharedRunnerDeps = createSharedRunnerDeps({
562
567
  metrics,
563
568
  config,
@@ -574,10 +579,7 @@ export async function runLocal(argv, deps = {}) {
574
579
  backgroundAgentStore: fileBackend.backgroundAgentStore,
575
580
  mailboxStore: fileBackend.mailboxStore,
576
581
  rosterStore: fileBackend.rosterStore,
577
- deploymentHooks: {
578
- ...createPermissionDeniedMeter(metrics),
579
- ...(config.hooksTimeoutMs !== undefined ? { timeoutMs: config.hooksTimeoutMs } : {}),
580
- },
582
+ deploymentMods,
581
583
  toolResultStore: fileBackend.toolResultStore,
582
584
  checkpointStore: undefined,
583
585
  sessionPolicyStore: fileBackend.sessionPolicyStore,
@@ -603,7 +605,7 @@ export async function runLocal(argv, deps = {}) {
603
605
  const subRunnerDeps = { ...sharedRunnerDeps, sessionStore: new ForkRoutingSessionStore(sessionStore, new TtlSessionStore({ defaultTtlDays: 1 / 24 })), onError: engineOnError, onAsk };
604
606
  const subRunner = new Runner(subRunnerDeps);
605
607
  const handslessRunner = new Runner(withoutHands(runnerDeps));
606
- const deploymentSkills = loadDeploymentSkills(config);
608
+ const deploymentSkills = loadDeploymentSkills(config, logger);
607
609
  const skills = effective?.skills ? await applySkillsManifest(deploymentSkills, effective.skills, configProvider, logger) : deploymentSkills;
608
610
  const repoClient = config.gitApiBaseUrl ? createRepoClient(config.gitApiKind, config.gitApiBaseUrl, config.gitApiToken) : undefined;
609
611
  const webSearchCfg = webSearchConfigFromEnv();
@@ -614,7 +616,6 @@ export async function runLocal(argv, deps = {}) {
614
616
  ...(webSearch ? { webSearch } : {}),
615
617
  backgroundAgentStore: fileBackend.backgroundAgentStore,
616
618
  taskListStoreFor: taskListLane.storeFor,
617
- brandIdentity: true,
618
619
  };
619
620
  const scenarios = buildScenarios(scenarioDeps);
620
621
  if (effective?.scenarios) {
@@ -625,7 +626,7 @@ export async function runLocal(argv, deps = {}) {
625
626
  const sessionId = args.session ?? uuidv7();
626
627
  let cap;
627
628
  try {
628
- scenarioName = gateScenarioRequest(undefined, args.scenario, config.defaultScenario, Object.keys(scenarios), logger);
629
+ scenarioName = await createScenarioGate({ config, scenarios, principalCaps: undefined, logger }, "enforce")(undefined, args.scenario);
629
630
  cap = selectScenario(scenarios, scenarioName)({ scenario: scenarioName, objective: args.objective }, undefined, { sessionId });
630
631
  }
631
632
  catch (err) {
@@ -672,7 +673,7 @@ export async function runLocal(argv, deps = {}) {
672
673
  getApiKeyAndHeaders: keyResolver,
673
674
  tools: cap.tools,
674
675
  skills: cap.skills,
675
- ...(cap.promptProvider ? { promptProvider: cap.promptProvider } : {}),
676
+ ...buildPromptSeats(scenarioName, cap.prompt, undefined),
676
677
  ...(cap.finalVerification === true ? { finalVerification: true } : {}),
677
678
  ...(mcp ? { mcp } : {}),
678
679
  ...(a2a ? { a2a } : {}),
package/dist/runs.d.ts CHANGED
@@ -109,6 +109,12 @@ export declare function resumeAtHttpStatus(result: {
109
109
  * `resumeAtHttpStatus` 自己(单元测试直接喂码)。
110
110
  */
111
111
  export declare function syncRefusalHttpStatus(result: Pick<TaskResult, "terminal" | "stats">): number | undefined;
112
+ /**
113
+ * 同步腿零工作拒体的机读事实(交 `refusalBody` 的 `facts`)—— 唯一一处。入参是**出口封口 + 脱敏后**的终局(调用方递 `redactTerminalResult(sealResultForExit(result)).terminal`,
114
+ * 与 run 记录读面 / 流式 `done.result` 上的 `terminal` 同一只来源),本函数只按码取它的 `detail.detail`,不另读、不另脱。码不在登记表里、终局不是 `failed`、
115
+ * 引擎没给 `coded` 因由 ⇒ `{}`(缺席不造键)。
116
+ */
117
+ export declare function zeroWorkRefusalFacts(terminal: unknown): Readonly<Record<string, unknown>>;
112
118
  /**
113
119
  * 一条提交腿把引擎终局落行之后的**唯一**收尾:**零工作的拒不占提交幂等键**。判据就是上面这只 {@link syncRefusalHttpStatus}
114
120
  * (零 turn、零 token 且是 prepare 期的拒码)—— 三条提交腿(同步 / 流式 / durable)同调这一只,不另开判据。
@@ -137,6 +143,38 @@ export declare function releaseKeyIfZeroWorkRefusal(runStore: {
137
143
  * surfaces the degrade (a metric) so a SYSTEMATIC failure — e.g. an un-migrated DB missing `resume_anchor` — is
138
144
  * observable instead of a silent feature outage.
139
145
  */
146
+ /**
147
+ * 「这一次用户消息提交是不是一句**提问**」的唯一读法 —— 三处用户回退锚(后台腿与续跑腿的 {@link TurnAnchorCapture}、`/v1/tasks/stream`
148
+ * 腿的 `userMsgEntryId`)同调这一只。
149
+ *
150
+ * 目标声明了附件时,引擎把每只附件铸成**一条自己的用户消息**,紧跟在目标后面提交(core `human_input.attachments` 顶注:
151
+ * 「each attachment is delivered as its own message after the objective; its `message_committed` is the delivery」)。
152
+ * 所以一次带 N 只附件的提交会出 1 + N 次 `message_committed{role:"user"}`,而「回到这条提问」的锚要的是**人的那条**
153
+ * (core 的 `resumeAt` 认它:「a `resumeAt` that keeps the message keeps its group」;以附件帧作 `resumeAt` 引擎拒 `resume_at.not_a_message`),
154
+ * 不是排在最后的附件帧。目标**之前**还可能有引擎排队的注入消息同批提交(亲读 core `engine/harness/agent-harness.js` 的 `takePrompts`:
155
+ * 排队消息 ⇒ 目标 ⇒ 附件帧;真案 = 工作区是 git 仓且带图时,git 状态帧单独排在目标前面),所以「窗口里第一次用户提交」不是判据。
156
+ *
157
+ * **为什么是「窗口 + 严格吻合」而不是读标记**:`message_committed` 上只有 `entryId / role / toolCallId?`,附件帧与用户真提交在事件上**不可辨认**;
158
+ * 落盘的行上有标记(`attachment` 键;引擎收尾的中断标记带 `provenance: "engine-note"`),但事件到手的那一刻本仓读不到那一行
159
+ * (会话店面没有按条目读的口,只有整日志导出),所以只能数。数的前提是「目标之后紧接恰好 N 只帧、其后才是别的」—— 前提成立才落锚,
160
+ * **不成立就不猜**:猜错的锚会落在附件帧上(引擎拒)或引擎的中断标记上(回到的不是用户那句)。
161
+ *
162
+ * 读法:
163
+ * · `human_input{delivery:"applied"}` 带 N 只附件 ⇒ 开窗;窗内的用户提交只攒、不落锚(此刻还不知道它们是不是『目标 + N 只帧』)。
164
+ * · 窗被**非用户提交**(第一次助手消息 / 工具结果)关上 ⇒ 窗内共 k 次用户提交,k ≥ N+1 时提问 = 倒数第 N+1 次(严格吻合);k < N+1(帧数不符)⇒ 不猜。
165
+ * · 窗被 `turn_end` 先切断 ⇒ 第一次模型调用没出任何提交就收了(halt / interrupt / 空失败):窗里可能夹着引擎收尾的中断标记,数 N 会落在帧或标记上 ⇒ 不猜。
166
+ * · 窗内又应用了一条别的输入(`human_input{applied}`,如第一次模型调用前排进来的 steer;事件先于它的正文提交到手)⇒ 窗里多了一条别人的消息,数 N 会落在帧上 ⇒ 窗作废、不猜,
167
+ * 之后的用户提交(含那条 steer 正文)是窗口之外的提交,最后一次赢。排队未应用(`delivery: "queued"`)的输入不动窗。
168
+ * · 「不猜」= 返回 `undefined`、这次提交不落锚;拿这次 run 的 taskId 去 `resumeAt` 得 404 `resume_at.unknown_event`(文案点名这一类成因)。
169
+ * 窗口之外的用户提交(如 interrupt 带话的 steer 正文,在 `turn_end` 之后提交)照修前读法(是提问,最后一次赢)。
170
+ * 返回这次提交之后的提问 `entryId`(调用方照旧「最后一次赢」地落锚),不是提问相关的提交返回 `undefined`。
171
+ *
172
+ * 退役条件:引擎在落盘的帧上(`message_committed`)带上附件标记之日,本读法整体换成「按标记判一次」(零计数、无窗口、无拒臂),
173
+ * 窗口 / 严格吻合 / 不猜三臂与 `resume_at.unknown_event` 文案里对应的那一句一并删除。
174
+ */
175
+ export declare function createPromptCommitReader(): (ev: {
176
+ readonly type: string;
177
+ }) => string | undefined;
140
178
  export declare class TurnAnchorCapture {
141
179
  private readonly capture;
142
180
  private readonly onCaptureFail?;
@@ -151,6 +189,8 @@ export declare class TurnAnchorCapture {
151
189
  /** The turn's FIRST text_delta eventId = the message's stable resume handle (also stamped on the durable text event). */
152
190
  firstTextEventId: string | undefined;
153
191
  private turnHadTool;
192
+ /** 用户提交是不是提问的唯一读法({@link createPromptCommitReader};附件帧不是提问)。 */
193
+ private readonly promptCommit;
154
194
  constructor(capture: ((eventId: string) => Promise<void>) | undefined, onCaptureFail?: (() => void) | undefined,
155
195
  /** R8 (CC-parity rewind): capture a USER-message anchor straight from the
156
196
  * `message_committed{role:"user"}` event — its `entryId` is the persisted user-message `SessionTreeEntry.id`,
@@ -160,9 +200,15 @@ export declare class TurnAnchorCapture {
160
200
  * Keyed by the run's taskId (the handle the shell already holds from the POST response — no eventId→entryId
161
201
  * side map / no `getLeafId` inference needed). The assistant-turn capture above stays as an additive bonus. */
162
202
  captureUser?: ((entryId: string) => Promise<void>) | undefined);
203
+ /** A `human_input` arrived — the objective's declared attachments open the frame window of {@link createPromptCommitReader}. */
204
+ onHumanInput(ev: {
205
+ readonly type: string;
206
+ }): void;
163
207
  /** A `message_committed` arrived — capture a USER-message rewind anchor (CC parity). Best-effort (a failure only
164
- * makes that one prompt non-rewindable). Only `role==="user"`; assistant/toolResult committed entries are handled
165
- * by the turn-text path (assistant) or deliberately not anchored (toolResult — `resumeAt` rejects them). */
208
+ * makes that one prompt non-rewindable). Only a user commit that is a PROMPT (not an attachment frame trailing the
209
+ * objective — {@link createPromptCommitReader}; an attachment window is decided when a non-user commit closes it, so
210
+ * the anchor of an attachment-carrying prompt lands on THAT call); assistant/toolResult committed entries are handled by
211
+ * the turn-text path (assistant) or deliberately not anchored (toolResult — `resumeAt` rejects them). */
166
212
  onMessageCommitted(role: string | undefined, entryId: string | undefined): Promise<void>;
167
213
  /** A text_delta arrived — latch the first DEFINED eventId as the message handle (eventId is optional on the stream). */
168
214
  onText(eventId: string | undefined): void;
package/dist/runs.js CHANGED
@@ -76,8 +76,12 @@ const PREPARE_REFUSAL_HTTP_STATUS = {
76
76
  "config.skill_listing_budget_invalid": 400,
77
77
  "config.user_invoked_skill_unknown": 400,
78
78
  "config.image_invalid": 400,
79
+ "config.user_attachment_invalid": 400,
79
80
  "config.tool_face_control": 400,
81
+ "config.agent.unknown_tool": 400,
82
+ "config.legacy_tool_name": 400,
80
83
  "config.memory_capture_unsupported": 409,
84
+ "prompt.assembled_role_refused": 400,
81
85
  };
82
86
  export function resumeAtHttpStatus(result) {
83
87
  const code = failureCodeOf(result.terminal);
@@ -105,24 +109,75 @@ export function syncRefusalHttpStatus(result) {
105
109
  return undefined;
106
110
  return resumeAtHttpStatus(result);
107
111
  }
112
+ const ZERO_WORK_REFUSAL_DETAIL_CODES = new Set(["config.user_attachment_invalid"]);
113
+ export function zeroWorkRefusalFacts(terminal) {
114
+ if (terminal === null || typeof terminal !== "object")
115
+ return {};
116
+ const t = terminal;
117
+ if (t.kind !== "failed" || typeof t.code !== "string" || !ZERO_WORK_REFUSAL_DETAIL_CODES.has(t.code))
118
+ return {};
119
+ const cause = t.detail;
120
+ if (cause === null || typeof cause !== "object")
121
+ return {};
122
+ const c = cause;
123
+ return c.kind === "coded" && c.detail !== undefined ? { detail: c.detail } : {};
124
+ }
108
125
  export async function releaseKeyIfZeroWorkRefusal(runStore, taskId, result) {
109
126
  if (syncRefusalHttpStatus(result) !== undefined)
110
127
  await runStore.releaseIdemKey(taskId);
111
128
  }
129
+ export function createPromptCommitReader() {
130
+ let attachmentWindow;
131
+ return (ev) => {
132
+ if (ev.type === "human_input") {
133
+ const h = ev;
134
+ if (h.delivery !== "applied")
135
+ return undefined;
136
+ attachmentWindow = Array.isArray(h.attachments) && h.attachments.length > 0 ? { frames: h.attachments.length, commits: [] } : undefined;
137
+ return undefined;
138
+ }
139
+ if (ev.type === "turn_end") {
140
+ attachmentWindow = undefined;
141
+ return undefined;
142
+ }
143
+ if (ev.type !== "message_committed")
144
+ return undefined;
145
+ const c = ev;
146
+ const entryId = typeof c.entryId === "string" && c.entryId.length > 0 ? c.entryId : undefined;
147
+ if (c.role === "user") {
148
+ if (attachmentWindow !== undefined) {
149
+ attachmentWindow.commits.push(entryId);
150
+ return undefined;
151
+ }
152
+ return entryId;
153
+ }
154
+ const closed = attachmentWindow;
155
+ attachmentWindow = undefined;
156
+ if (closed === undefined || closed.commits.length < closed.frames + 1)
157
+ return undefined;
158
+ return closed.commits[closed.commits.length - 1 - closed.frames];
159
+ };
160
+ }
112
161
  export class TurnAnchorCapture {
113
162
  capture;
114
163
  onCaptureFail;
115
164
  captureUser;
116
165
  firstTextEventId;
117
166
  turnHadTool = false;
167
+ promptCommit = createPromptCommitReader();
118
168
  constructor(capture, onCaptureFail, captureUser) {
119
169
  this.capture = capture;
120
170
  this.onCaptureFail = onCaptureFail;
121
171
  this.captureUser = captureUser;
122
172
  }
173
+ onHumanInput(ev) {
174
+ this.promptCommit(ev);
175
+ }
123
176
  async onMessageCommitted(role, entryId) {
124
- if (role === "user" && entryId && this.captureUser) {
125
- await this.captureUser(entryId).catch(() => this.onCaptureFail?.());
177
+ const committed = { type: "message_committed", role, entryId };
178
+ const prompt = this.promptCommit(committed);
179
+ if (prompt !== undefined && this.captureUser) {
180
+ await this.captureUser(prompt).catch(() => this.onCaptureFail?.());
126
181
  }
127
182
  }
128
183
  onText(eventId) {
@@ -135,6 +190,7 @@ export class TurnAnchorCapture {
135
190
  this.turnHadTool = true;
136
191
  }
137
192
  async onTurnEnd() {
193
+ this.promptCommit({ type: "turn_end" });
138
194
  if (this.firstTextEventId && !this.turnHadTool && this.capture) {
139
195
  await this.capture(this.firstTextEventId).catch(() => this.onCaptureFail?.());
140
196
  }
@@ -373,6 +429,9 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
373
429
  case "status":
374
430
  metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
375
431
  break;
432
+ case "human_input":
433
+ anchor.onHumanInput(ev);
434
+ break;
376
435
  case "message_committed":
377
436
  await anchor.onMessageCommitted(ev.role, ev.entryId);
378
437
  break;