@sema-agent/server 7.131.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 (256) hide show
  1. package/MIGRATION.md +309 -0
  2. package/README.md +5 -1
  3. package/README.zh-CN.md +3 -1
  4. package/USAGE.md +163 -54
  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/auth-bridge.d.ts +4 -0
  9. package/dist/auth-bridge.js +10 -11
  10. package/dist/boot/deployment-mods.d.ts +31 -0
  11. package/dist/boot/deployment-mods.js +26 -0
  12. package/dist/boot/execution-env.d.ts +1 -1
  13. package/dist/boot/execution-env.js +19 -6
  14. package/dist/boot/memory-boundary.d.ts +2 -2
  15. package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
  16. package/dist/boot/read-deny-anchor-audit.js +36 -0
  17. package/dist/boot/resolve-spec.d.ts +1 -5
  18. package/dist/boot/resolve-spec.js +19 -47
  19. package/dist/boot/runner-deps.d.ts +19 -8
  20. package/dist/boot/runner-deps.js +14 -2
  21. package/dist/boot/session-faces.d.ts +1 -7
  22. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  23. package/dist/boot/stage-03-store-backend.js +2 -2
  24. package/dist/boot/stage-04-stores.d.ts +1 -1
  25. package/dist/boot/stage-05-execution-env.d.ts +4 -4
  26. package/dist/boot/stage-05-execution-env.js +7 -10
  27. package/dist/boot/stage-06-runners.d.ts +2 -2
  28. package/dist/boot/stage-06-runners.js +5 -4
  29. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  30. package/dist/boot/stage-07-capability-layer.js +3 -3
  31. package/dist/boot/stage-08-reapers.d.ts +2 -2
  32. package/dist/boot/stage-09-leader.d.ts +3 -9
  33. package/dist/boot/stage-10-http-server.d.ts +3 -9
  34. package/dist/boot/stage-10-http-server.js +6 -7
  35. package/dist/boot/stores.d.ts +1 -1
  36. package/dist/boot/stores.js +5 -6
  37. package/dist/boot/web-admin.d.ts +0 -3
  38. package/dist/boot/web-admin.js +35 -35
  39. package/dist/brain.js +17 -22
  40. package/dist/budget.js +5 -4
  41. package/dist/caller-text-echo.d.ts +20 -0
  42. package/dist/caller-text-echo.js +24 -0
  43. package/dist/capabilities/brand-identity.d.ts +26 -0
  44. package/dist/capabilities/brand-identity.js +27 -0
  45. package/dist/capabilities/center-plugins.js +1 -1
  46. package/dist/capabilities/center-prompts.d.ts +32 -59
  47. package/dist/capabilities/center-prompts.js +18 -32
  48. package/dist/capabilities/engine-capabilities.js +2 -1
  49. package/dist/capabilities/hands-lane.d.ts +9 -6
  50. package/dist/capabilities/hands-lane.js +1 -1
  51. package/dist/capabilities/mcp-probe.d.ts +14 -1
  52. package/dist/capabilities/mcp-probe.js +22 -133
  53. package/dist/capabilities/scenarios.d.ts +34 -40
  54. package/dist/capabilities/scenarios.js +16 -33
  55. package/dist/capabilities/skills.d.ts +19 -4
  56. package/dist/capabilities/skills.js +30 -10
  57. package/dist/config-catalog.js +6 -6
  58. package/dist/config-lkg.d.ts +9 -9
  59. package/dist/config-lkg.js +20 -4
  60. package/dist/config-provider.d.ts +3 -1
  61. package/dist/config-provider.js +12 -4
  62. package/dist/config-types.d.ts +7 -9
  63. package/dist/config.js +17 -4
  64. package/dist/device-ws-protocol.d.ts +1 -1
  65. package/dist/device-ws-protocol.js +1 -0
  66. package/dist/env-name-allowlist-knobs.d.ts +12 -2
  67. package/dist/env-name-allowlist-knobs.js +5 -4
  68. package/dist/exec-bounds.d.ts +15 -3
  69. package/dist/exec-bounds.js +4 -0
  70. package/dist/execution-lane-caps.d.ts +9 -2
  71. package/dist/execution-lane-caps.js +11 -0
  72. package/dist/hooks/classic-stations.d.ts +86 -0
  73. package/dist/hooks/classic-stations.js +27 -0
  74. package/dist/hooks/hook-llm.d.ts +8 -5
  75. package/dist/hooks/hook-llm.js +78 -35
  76. package/dist/hooks/hook-runner.d.ts +232 -80
  77. package/dist/hooks/hook-runner.js +218 -312
  78. package/dist/http/admission.d.ts +8 -2
  79. package/dist/http/admission.js +101 -57
  80. package/dist/http/approval-sweeps.js +57 -16
  81. package/dist/http/checkpoint-ctx.d.ts +25 -8
  82. package/dist/http/checkpoint-ctx.js +11 -7
  83. package/dist/http/claimed-run.d.ts +13 -0
  84. package/dist/http/claimed-run.js +15 -0
  85. package/dist/http/device-placement.js +5 -10
  86. package/dist/http/dispatch.d.ts +0 -4
  87. package/dist/http/dispatch.js +5 -9
  88. package/dist/http/durable-run-leg.d.ts +2 -1
  89. package/dist/http/durable-run-leg.js +5 -3
  90. package/dist/http/health-self-report.d.ts +7 -5
  91. package/dist/http/health-self-report.js +4 -18
  92. package/dist/http/principal-gate.d.ts +23 -6
  93. package/dist/http/principal-gate.js +24 -5
  94. package/dist/http/refusal-body.d.ts +21 -0
  95. package/dist/http/refusal-body.js +4 -0
  96. package/dist/http/request-guards.d.ts +27 -0
  97. package/dist/http/request-guards.js +24 -3
  98. package/dist/http/resume-legs.d.ts +25 -0
  99. package/dist/http/resume-legs.js +43 -22
  100. package/dist/http/routes/capabilities.js +1 -1
  101. package/dist/http/routes/diagnostics.js +1 -1
  102. package/dist/http/routes/rules.js +2 -2
  103. package/dist/http/routes/runs.js +7 -7
  104. package/dist/http/routes/session-sync.js +4 -2
  105. package/dist/http/routes/sessions.js +1 -1
  106. package/dist/http/routes/shared-memory.js +2 -13
  107. package/dist/http/routes/side-query.js +2 -3
  108. package/dist/http/routes/tasks.js +19 -9
  109. package/dist/http/send.d.ts +9 -7
  110. package/dist/http/send.js +7 -2
  111. package/dist/http/server.d.ts +21 -11
  112. package/dist/http/server.js +28 -2
  113. package/dist/http/submit-ctx.d.ts +25 -0
  114. package/dist/http/submit-ctx.js +39 -0
  115. package/dist/http/wire-types.d.ts +1 -1
  116. package/dist/index.d.ts +1 -1
  117. package/dist/index.js +1 -1
  118. package/dist/lane-home-probe.d.ts +43 -0
  119. package/dist/lane-home-probe.js +35 -0
  120. package/dist/limit-refusal.d.ts +47 -0
  121. package/dist/limit-refusal.js +12 -0
  122. package/dist/main.js +1 -1
  123. package/dist/mcp-assertion-header.d.ts +1 -9
  124. package/dist/mcp-assertion-header.js +0 -6
  125. package/dist/mcp-tool-faces.d.ts +5 -0
  126. package/dist/mcp-tool-faces.js +5 -1
  127. package/dist/memory-scope.d.ts +1 -1
  128. package/dist/model-provider.d.ts +39 -12
  129. package/dist/model-provider.js +19 -2
  130. package/dist/model-route-endpoint.d.ts +1 -1
  131. package/dist/model-route-endpoint.js +5 -5
  132. package/dist/observability/cost-taxonomy.d.ts +7 -7
  133. package/dist/observability/fail-open.d.ts +42 -6
  134. package/dist/observability/fail-open.js +42 -6
  135. package/dist/observability/metrics.js +3 -1
  136. package/dist/observability/prompt-manifest.d.ts +47 -82
  137. package/dist/observability/prompt-manifest.js +119 -17
  138. package/dist/observability/run-terminal.js +14 -6
  139. package/dist/observability/secret-env-scrub.d.ts +13 -1
  140. package/dist/observability/secret-env-scrub.js +13 -1
  141. package/dist/observability/tool-trace.d.ts +16 -21
  142. package/dist/observability/tool-trace.js +51 -42
  143. package/dist/per-task-image.d.ts +7 -1
  144. package/dist/per-task-image.js +3 -3
  145. package/dist/plain-string-record.d.ts +25 -0
  146. package/dist/plain-string-record.js +32 -0
  147. package/dist/plugins/blob-backend.d.ts +68 -13
  148. package/dist/plugins/blob-backend.js +53 -10
  149. package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
  150. package/dist/plugins/checkpoint-store-sql.js +39 -26
  151. package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
  152. package/dist/plugins/k8s-bg-scripts.js +22 -0
  153. package/dist/plugins/local-checkpoint-store.js +5 -2
  154. package/dist/plugins/local-session-store.js +1 -1
  155. package/dist/plugins/one-time-migrations.d.ts +137 -90
  156. package/dist/plugins/one-time-migrations.js +356 -207
  157. package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
  158. package/dist/plugins/permission-rule-store-sql.js +4 -18
  159. package/dist/plugins/pg-session-storage.d.ts +4 -5
  160. package/dist/plugins/pg-session-storage.js +7 -17
  161. package/dist/plugins/remote-env-adb.d.ts +6 -0
  162. package/dist/plugins/remote-env-adb.js +9 -4
  163. package/dist/plugins/remote-env-device.js +3 -0
  164. package/dist/plugins/remote-env-e2b.d.ts +1 -1
  165. package/dist/plugins/remote-env-e2b.js +21 -9
  166. package/dist/plugins/remote-env-host.d.ts +9 -9
  167. package/dist/plugins/remote-env-host.js +10 -32
  168. package/dist/plugins/remote-env-k8s.d.ts +95 -26
  169. package/dist/plugins/remote-env-k8s.js +265 -90
  170. package/dist/plugins/remote-env-local-docker.d.ts +20 -1
  171. package/dist/plugins/remote-env-local-docker.js +77 -9
  172. package/dist/plugins/remote-env-ssh.d.ts +15 -0
  173. package/dist/plugins/remote-env-ssh.js +109 -3
  174. package/dist/plugins/remote-shell.d.ts +0 -1
  175. package/dist/plugins/remote-shell.js +1 -2
  176. package/dist/plugins/retention-store-sql.js +6 -2
  177. package/dist/plugins/schema-consistency.d.ts +54 -0
  178. package/dist/plugins/schema-consistency.js +52 -0
  179. package/dist/plugins/session-epoch-anchor.d.ts +13 -0
  180. package/dist/plugins/session-epoch-anchor.js +23 -0
  181. package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
  182. package/dist/plugins/shared-memory-store-sql.js +5 -16
  183. package/dist/plugins/sql-driver.d.ts +141 -0
  184. package/dist/plugins/sql-driver.js +115 -1
  185. package/dist/plugins/sql-errors.d.ts +41 -0
  186. package/dist/plugins/sql-errors.js +36 -0
  187. package/dist/plugins/store-backend.js +2 -1
  188. package/dist/plugins/tidb-session-storage.d.ts +7 -11
  189. package/dist/plugins/tidb-session-storage.js +6 -16
  190. package/dist/plugins/tidb-session-store.js +1 -1
  191. package/dist/prompts-domain-validate.d.ts +17 -61
  192. package/dist/prompts-domain-validate.js +9 -119
  193. package/dist/request-key-closure.d.ts +2 -2
  194. package/dist/request-key-closure.js +2 -4
  195. package/dist/rule-entry-list.d.ts +12 -9
  196. package/dist/rule-entry-list.js +37 -21
  197. package/dist/run-local.js +19 -19
  198. package/dist/runs.d.ts +6 -0
  199. package/dist/runs.js +16 -0
  200. package/dist/server-secret-env.d.ts +44 -18
  201. package/dist/server-secret-env.js +49 -8
  202. package/dist/session-incarnation.d.ts +15 -5
  203. package/dist/session-incarnation.js +16 -9
  204. package/dist/session-titler.js +12 -2
  205. package/dist/spec-fields.d.ts +28 -16
  206. package/dist/spec-fields.js +46 -53
  207. package/dist/sql-engine-posture.d.ts +29 -4
  208. package/dist/sql-engine-posture.js +36 -1
  209. package/dist/stored-request-tool-axes-m19.d.ts +19 -4
  210. package/dist/stored-request-tool-axes-m19.js +30 -2
  211. package/dist/task-a2a.d.ts +9 -1
  212. package/dist/task-a2a.js +73 -47
  213. package/dist/task-mcp.d.ts +46 -58
  214. package/dist/task-mcp.js +99 -65
  215. package/dist/task-settings.d.ts +7 -21
  216. package/dist/task-settings.js +7 -36
  217. package/dist/text-bidi.d.ts +3 -46
  218. package/dist/text-bidi.js +3 -3
  219. package/dist/tool-approval.d.ts +1 -1
  220. package/dist/trace/core-keyset-guard.d.ts +24 -4
  221. package/dist/trace/engine-notice-wire.d.ts +11 -3
  222. package/dist/trace/engine-notice-wire.js +8 -1
  223. package/dist/trace/ledger-events.d.ts +3 -1
  224. package/dist/trace/ledger-sink.js +4 -1
  225. package/dist/trace/model-segment.js +1 -1
  226. package/dist/trace/project.d.ts +19 -6
  227. package/dist/trace/project.js +57 -22
  228. package/dist/trace/wire-projection-faces.d.ts +79 -1
  229. package/dist/trace/wire-projection-faces.js +22 -0
  230. package/dist/web-admin-client/candidate-gate.d.ts +6 -6
  231. package/dist/web-admin-client/candidate-gate.js +3 -3
  232. package/dist/web-admin-client/http-client.js +0 -6
  233. package/dist/web-admin-client/read-warnings.d.ts +13 -0
  234. package/dist/web-admin-client/read-warnings.js +10 -0
  235. package/dist/web-admin-client/skills-mcp.d.ts +3 -0
  236. package/dist/web-admin-client/skills-mcp.js +9 -6
  237. package/dist/web-admin-client/types.d.ts +2 -3
  238. package/dist/workspace-cache.d.ts +35 -0
  239. package/dist/workspace-cache.js +36 -0
  240. package/dist/workspace-space.d.ts +3 -2
  241. package/dist/workspace-space.js +2 -0
  242. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
  243. package/package.json +4 -4
  244. package/scripts/drain-check-local.mjs +34 -1
  245. package/scripts/drain-check.mysql.sql +30 -2
  246. package/scripts/drain-check.pg.sql +29 -2
  247. package/dist/capabilities/memory-notice.d.ts +0 -138
  248. package/dist/capabilities/memory-notice.js +0 -64
  249. package/dist/capabilities/prompt.d.ts +0 -17
  250. package/dist/capabilities/prompt.js +0 -6
  251. package/dist/mcp-content-origin.d.ts +0 -34
  252. package/dist/mcp-content-origin.js +0 -10
  253. package/dist/tool-axis-words.d.ts +0 -27
  254. package/dist/tool-axis-words.js +0 -9
  255. package/dist/wire-name-bound.d.ts +0 -14
  256. package/dist/wire-name-bound.js +0 -13
package/dist/task-mcp.js CHANGED
@@ -1,22 +1,19 @@
1
1
  import { mcpInjectionDroppedNotice, readMcpToolAxes, resolveComplianceDenies } from "@sema-agent/core";
2
- import { describeBadWireName, isUsableWireName } from "./wire-name-bound.js";
2
+ import { MCP_CONTENT_ORIGINS, describeNotStringArray, describeValueShape, isContentOriginWord, isStringArray, isUsableWireName, readMcpIdentityHeaders, } from "@sema-agent/settings-schema";
3
+ import { describeBadContentOrigin, describeBadWireName } from "./caller-text-echo.js";
3
4
  import { HttpError, createCodedHttpError } from "./security.js";
4
5
  import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
5
- import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
6
6
  import { MCP_SOURCE_LABEL_MAX, isMcpSourceLabel, readMcpSourceLabel } from "./mcp-source-label.js";
7
- import { declaresMcpAssertionHeader, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, readMcpIdentityHeaders, toCoreMcpServerSpec } from "./mcp-assertion-header.js";
8
- import { echoKeyName } from "./request-key-closure.js";
7
+ import { declaresMcpAssertionHeader, describeMcpAssertionHeaderNotAString, describeMcpIdentityHeadersConflict, mcpIdentityHeadersIssue, noteMcpAssertionHeaderIgnored, noteMcpAssertionHeaderNotSent, toCoreMcpServerSpec } from "./mcp-assertion-header.js";
8
+ import { MAX_KEY_ECHO, echoKeyName, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
9
9
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
10
10
  import { readMcpToolFaces } from "./mcp-tool-faces.js";
11
+ import { readPlainStringRecord } from "./plain-string-record.js";
11
12
  export const MAX_REQUEST_MCP_SERVERS = 32;
12
13
  export const MAX_REQUEST_MCP_DROP_NAMES = MAX_REQUEST_MCP_SERVERS;
13
- export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "toolAxes", "toolFaces", "transport"];
14
+ export const MCP_SERVER_ENTRY_KEYS = ["allowTools", "contentOrigin", "elicitation", "name", "source", "startupTimeoutMs", "toolAxes", "toolFaces", "transport"];
14
15
  export const STDIO_MCP_TRANSPORT_KEYS = ["args", "command", "env", "kind"];
15
16
  export const HTTP_MCP_TRANSPORT_KEYS = ["assertionHeader", "headers", "kind", "principalHeader", "url"];
16
- export const MCP_ENTRY_EMPTY_RECORD_KEYS = ["toolAxes", "toolFaces"];
17
- export const MCP_ENTRY_EMPTY_ARRAY_KEYS = ["allowTools"];
18
- export const MCP_TRANSPORT_EMPTY_RECORD_KEYS = ["env", "headers"];
19
- export const MCP_TRANSPORT_EMPTY_ARRAY_KEYS = ["args"];
20
17
  export function mcpInjectionHonored(config) {
21
18
  if (config.lockedConfigKeys?.includes("mcp") === true)
22
19
  return false;
@@ -24,57 +21,31 @@ export function mcpInjectionHonored(config) {
24
21
  return false;
25
22
  return config.requirePrincipal !== true;
26
23
  }
27
- function isStringRecord(v) {
28
- return !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
29
- }
30
24
  function isRecord(v) {
31
25
  return v !== null && typeof v === "object" && !Array.isArray(v);
32
26
  }
33
- function isStringArray(v) {
34
- return Array.isArray(v) && v.every((a) => typeof a === "string");
35
- }
36
27
  function isElicitationFlag(v) {
37
28
  return typeof v === "boolean";
38
29
  }
39
- function describeShape(v) {
40
- if (v === undefined)
41
- return "nothing (the key is absent)";
42
- if (v === null)
43
- return "null";
44
- if (Array.isArray(v))
45
- return "an array";
46
- if (typeof v === "string")
47
- return v.length === 0 ? "an empty string" : `a string of ${v.length} characters`;
48
- if (typeof v === "object")
49
- return "an object";
50
- return `a ${typeof v}`;
51
- }
52
- function describeNotStringArray(v) {
53
- return Array.isArray(v) ? "an array with a non-string element" : describeShape(v);
54
- }
55
- function describeNotStringRecord(v) {
56
- return isRecord(v) ? "an object with a non-string value" : describeShape(v);
57
- }
58
30
  const transportBad = (path, reason) => ({ ok: false, issue: { path, reason } });
59
31
  function readRequestTransport(t) {
60
32
  if (!t || typeof t !== "object")
61
- return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeShape(t)}`);
33
+ return transportBad("transport", `must be an object with a "kind" of "stdio" or "http" — got ${describeValueShape(t)}`);
62
34
  const tr = t;
63
35
  const lost = [];
64
36
  if (tr.kind === "stdio") {
65
37
  if (typeof tr.command !== "string" || tr.command.length === 0)
66
- return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeShape(tr.command)}`);
38
+ return transportBad("transport.command", `must be a non-empty string (the command to run) — got ${describeValueShape(tr.command)}`);
67
39
  if (tr.args !== undefined && !isStringArray(tr.args))
68
40
  lost.push({ path: "transport.args", reason: `must be an array of strings — got ${describeNotStringArray(tr.args)}` });
69
- if (tr.env !== undefined && !isStringRecord(tr.env))
70
- lost.push({ path: "transport.env", reason: `must be an object of string values — got ${describeNotStringRecord(tr.env)}` });
41
+ const env = readTransportRecord("transport.env", tr.env, lost);
71
42
  return {
72
43
  ok: true,
73
44
  transport: {
74
45
  kind: "stdio",
75
46
  command: tr.command,
76
47
  ...(isStringArray(tr.args) ? { args: tr.args } : {}),
77
- ...(isStringRecord(tr.env) ? { env: tr.env } : {}),
48
+ ...(env !== undefined ? { env } : {}),
78
49
  },
79
50
  lost,
80
51
  };
@@ -83,23 +54,34 @@ function readRequestTransport(t) {
83
54
  if (typeof tr.url !== "string" || !/^https?:\/\//i.test(tr.url))
84
55
  return transportBad("transport.url", "must be an absolute http(s) URL (put credentials in transport.headers, never in the URL)");
85
56
  const ids = readMcpIdentityHeaders(tr);
86
- if (tr.headers !== undefined && !isStringRecord(tr.headers))
87
- lost.push({ path: "transport.headers", reason: `must be an object of string values — got ${describeNotStringRecord(tr.headers)}` });
57
+ const headers = readTransportRecord("transport.headers", tr.headers, lost);
88
58
  if (tr.principalHeader !== undefined && ids.principalHeader === undefined) {
89
- lost.push({ path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeShape(tr.principalHeader)}` });
59
+ lost.push({ path: "transport.principalHeader", reason: `must be a string (the HTTP header NAME the end-user principal is injected into) — got ${describeValueShape(tr.principalHeader)}` });
90
60
  }
91
61
  return {
92
62
  ok: true,
93
63
  transport: {
94
64
  kind: "http",
95
65
  url: tr.url,
96
- ...(isStringRecord(tr.headers) ? { headers: tr.headers } : {}),
66
+ ...(headers !== undefined ? { headers } : {}),
97
67
  ...ids,
98
68
  },
99
69
  lost,
100
70
  };
101
71
  }
102
- return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeShape(tr.kind)}`);
72
+ return transportBad("transport.kind", `must be "stdio" or "http" — got ${describeValueShape(tr.kind)}`);
73
+ }
74
+ function readTransportRecord(path, raw, lost) {
75
+ if (raw === undefined)
76
+ return undefined;
77
+ const read = readPlainStringRecord(raw);
78
+ if (!read.ok) {
79
+ lost.push({ path, reason: `must be a plain object of string values — ${read.member === undefined ? read.fault : `its member "${echoKeyName("", read.member)}" ${read.fault}`}` });
80
+ return undefined;
81
+ }
82
+ for (const m of read.stripped)
83
+ lost.push({ path: echoKeyName(`${path}.`, m.name), reason: m.fault });
84
+ return read.record;
103
85
  }
104
86
  const bad = (field) => ({ ok: false, field });
105
87
  const MALFORMED_NAME = "<malformed>";
@@ -129,7 +111,7 @@ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
129
111
  let judged = axes;
130
112
  let toolAxesReread;
131
113
  if (!axes.ok && storedToolAxes !== undefined) {
132
- const again = readMcpToolAxes(storedToolAxes(o.toolAxes));
114
+ const again = readMcpToolAxes(storedToolAxes.mcp(o.toolAxes));
133
115
  if (again.ok) {
134
116
  judged = again;
135
117
  toolAxesReread = axes.problem.replace(CONTROL_AND_BIDI_STRIP_RE, "");
@@ -146,12 +128,14 @@ function normalizeServer(e, noteBadOrigin, storedToolAxes) {
146
128
  noteBadOrigin(spec.name, o.contentOrigin);
147
129
  }
148
130
  Object.assign(spec, readMcpSourceLabel(spec.name, o.source));
131
+ if (o.startupTimeoutMs !== undefined)
132
+ spec.startupTimeoutMs = o.startupTimeoutMs;
149
133
  const faces = readMcpToolFaces(o.toolFaces);
150
134
  if (!faces.ok)
151
135
  return bad("toolFaces");
152
136
  if (faces.faces !== undefined)
153
137
  spec.toolFaces = faces.faces;
154
- return { ok: true, spec, ...(toolAxesReread !== undefined ? { toolAxesReread } : {}) };
138
+ return { ok: true, spec, ...(toolAxesReread !== undefined ? { toolAxesReread } : {}), transportLost: read.lost };
155
139
  }
156
140
  export function validateRequestMcp(raw) {
157
141
  if (raw !== undefined && raw !== null && !Array.isArray(raw))
@@ -164,19 +148,22 @@ function validateRequestMcpEntries(raw, storedToolAxes) {
164
148
  const dropped = [];
165
149
  const badContentOrigin = [];
166
150
  const toolAxesReread = [];
151
+ const transportLost = [];
167
152
  for (const e of raw.slice(0, MAX_REQUEST_MCP_SERVERS)) {
168
153
  const v = normalizeServer(e, (server, value) => badContentOrigin.push({ server, value: describeBadContentOrigin(value) }), storedToolAxes);
169
154
  if (v.ok) {
170
155
  ok.push(v.spec);
171
156
  if (v.toolAxesReread !== undefined)
172
157
  toolAxesReread.push({ server: v.spec.name, problem: v.toolAxesReread });
158
+ if (v.transportLost.length > 0)
159
+ transportLost.push({ server: v.spec.name, lost: v.transportLost.map((i) => i.path) });
173
160
  }
174
161
  else
175
162
  dropped.push({ server: declaredName(e), reason: "malformed_entry", field: v.field });
176
163
  }
177
164
  for (const e of raw.slice(MAX_REQUEST_MCP_SERVERS, MAX_REQUEST_MCP_SERVERS + MAX_REQUEST_MCP_DROP_NAMES))
178
165
  dropped.push({ server: declaredName(e), reason: "over_cap" });
179
- return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin, toolAxesReread };
166
+ return { ok, dropped, droppedTotal: Math.max(0, raw.length - ok.length), badContentOrigin, toolAxesReread, transportLost };
180
167
  }
181
168
  export function assertRequestMcpUnlocked(bodyMcp, lockedKeys) {
182
169
  if (!lockedKeys.has("mcp"))
@@ -195,7 +182,7 @@ export function requestEntryIdentityIssue(entry) {
195
182
  return undefined;
196
183
  return mcpIdentityHeadersIssue(t);
197
184
  }
198
- export function requestEntryIdentityRefusal(entry, at) {
185
+ function requestEntryIdentityRefusal(entry, at) {
199
186
  const issue = requestEntryIdentityIssue(entry);
200
187
  if (issue === undefined || !isRecord(entry))
201
188
  return undefined;
@@ -205,14 +192,44 @@ export function requestEntryIdentityRefusal(entry, at) {
205
192
  const value = isRecord(transport) ? transport.assertionHeader : undefined;
206
193
  return entryFieldRefusal(entry, at, { path: "transport.assertionHeader", reason: describeMcpAssertionHeaderNotAString(value) });
207
194
  }
208
- export function assertRequestMcpEntries(bodyMcp) {
209
- if (!Array.isArray(bodyMcp))
210
- return;
211
- for (let i = 0; i < Math.min(bodyMcp.length, MAX_REQUEST_MCP_SERVERS); i++) {
212
- const refusal = requestEntryRefusal(bodyMcp[i], `mcpServers[${i}]`);
195
+ export function requestMcpDeclarationRefusal(list) {
196
+ if (list === undefined || list === null)
197
+ return undefined;
198
+ if (!Array.isArray(list))
199
+ return listRefusal(`mcpServers is ${describeValueShape(list)}, not an array of MCP server declarations`);
200
+ if (list.length > MAX_REQUEST_MCP_SERVERS)
201
+ return listRefusal(`mcpServers carries ${list.length} declarations, above this deployment's ceiling of ${MAX_REQUEST_MCP_SERVERS}`);
202
+ for (let i = 0; i < list.length; i++) {
203
+ const refusal = requestEntryRefusal(list[i], `mcpServers[${i}]`);
213
204
  if (refusal !== undefined)
214
- throw createCodedHttpError(400, refusal.errorCode, refusal.message);
205
+ return refusal;
215
206
  }
207
+ return undefined;
208
+ }
209
+ export function assertRequestMcpEntries(bodyMcp) {
210
+ const refusal = requestMcpDeclarationRefusal(bodyMcp);
211
+ if (refusal !== undefined)
212
+ throw createCodedHttpError(400, refusal.errorCode, refusal.message, refusal.unknownKeys === undefined ? undefined : { unknownKeys: [...refusal.unknownKeys] });
213
+ }
214
+ const LIST_REFUSAL_TAIL = "The request is refused rather than acting on part of the list: declarations that silently did not apply would read as in effect to whoever sent them. " +
215
+ "Fix the list (an empty list declares none).";
216
+ function listRefusal(head) {
217
+ return { errorCode: "request.field_invalid", message: `${head}. ${LIST_REFUSAL_TAIL}` };
218
+ }
219
+ const ENTRY_KEY_SET = new Set(MCP_SERVER_ENTRY_KEYS);
220
+ const STDIO_TRANSPORT_KEY_SET = new Set(STDIO_MCP_TRANSPORT_KEYS);
221
+ const HTTP_TRANSPORT_KEY_SET = new Set(HTTP_MCP_TRANSPORT_KEYS);
222
+ const NO_TOLERATED_KEYS = new Set();
223
+ function entryUnknownKeys(entry, at) {
224
+ const buckets = { unknown: [], unsupported: [] };
225
+ scanKeyClosure(entry, `${at}.`, ENTRY_KEY_SET, NO_TOLERATED_KEYS, buckets);
226
+ const t = entry.transport;
227
+ if (isRecord(t)) {
228
+ const keys = t.kind === "stdio" ? STDIO_TRANSPORT_KEY_SET : t.kind === "http" ? HTTP_TRANSPORT_KEY_SET : undefined;
229
+ if (keys !== undefined)
230
+ scanKeyClosure(t, `${at}.transport.`, keys, NO_TOLERATED_KEYS, buckets);
231
+ }
232
+ return buckets.unknown.sort();
216
233
  }
217
234
  const ENTRY_KEY_JUDGES = {
218
235
  allowTools: (o) => o.allowTools === undefined || isStringArray(o.allowTools)
@@ -222,14 +239,15 @@ const ENTRY_KEY_JUDGES = {
222
239
  ? undefined
223
240
  : {
224
241
  path: "contentOrigin",
225
- reason: `must be one of ${CONTENT_ORIGIN_WORDS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(o.contentOrigin)}` +
242
+ reason: `must be one of ${MCP_CONTENT_ORIGINS.map((w) => `"${w}"`).join(" | ")} — got ${describeBadContentOrigin(o.contentOrigin)}` +
226
243
  ` (omit the key for the default: this server's tools classify as external content)`,
227
244
  },
228
- elicitation: (o) => (o.elicitation === undefined || isElicitationFlag(o.elicitation) ? undefined : { path: "elicitation", reason: `must be a boolean — got ${describeShape(o.elicitation)}` }),
245
+ elicitation: (o) => (o.elicitation === undefined || isElicitationFlag(o.elicitation) ? undefined : { path: "elicitation", reason: `must be a boolean — got ${describeValueShape(o.elicitation)}` }),
229
246
  name: (o) => (isUsableWireName(o.name) ? undefined : { path: "name", reason: `must be a non-empty string within the wire-name length bound — got ${describeBadWireName(o.name)}` }),
230
247
  source: (o) => o.source === undefined || isMcpSourceLabel(o.source)
231
248
  ? undefined
232
- : { path: "source", reason: `must be a non-empty string of at most ${MCP_SOURCE_LABEL_MAX} characters (a display label) — got ${describeShape(o.source)}` },
249
+ : { path: "source", reason: `must be a non-empty string of at most ${MCP_SOURCE_LABEL_MAX} characters (a display label) — got ${describeValueShape(o.source)}` },
250
+ startupTimeoutMs: () => undefined,
233
251
  toolAxes: (o) => {
234
252
  const read = readMcpToolAxes(o.toolAxes);
235
253
  return read.ok ? undefined : { path: "toolAxes", reason: read.problem };
@@ -254,7 +272,17 @@ function entryFieldRefusal(entry, at, issue) {
254
272
  }
255
273
  function requestEntryRefusal(entry, at) {
256
274
  if (!isRecord(entry))
257
- return undefined;
275
+ return listRefusal(`${at} is ${describeValueShape(entry)}, not an MCP server declaration (an object)`);
276
+ const unknownKeys = entryUnknownKeys(entry, at);
277
+ if (unknownKeys.length > 0) {
278
+ return {
279
+ errorCode: "request.body_shape",
280
+ message: `body: unrecognized key(s) on an MCP declaration — refused rather than dropped, because a dropped key reads as "in effect" to the caller. ` +
281
+ `${keyClosureClause(unknownKeys, "unknown (not an mcpServers[] key — check the spelling)")}. ` +
282
+ `Accepted per entry: ${[...MCP_SERVER_ENTRY_KEYS].sort().join(",")} (transport.kind "stdio": ${[...STDIO_MCP_TRANSPORT_KEYS].sort().join(",")}; "http": ${[...HTTP_MCP_TRANSPORT_KEYS].sort().join(",")})`,
283
+ unknownKeys: unknownKeys.slice(0, MAX_KEY_ECHO),
284
+ };
285
+ }
258
286
  const identity = requestEntryIdentityRefusal(entry, at);
259
287
  if (identity !== undefined)
260
288
  return identity;
@@ -265,12 +293,6 @@ function requestEntryRefusal(entry, at) {
265
293
  }
266
294
  return undefined;
267
295
  }
268
- export function requestEntryToolAxesRefusal(entry, at) {
269
- if (!isRecord(entry))
270
- return undefined;
271
- const issue = ENTRY_KEY_JUDGES.toolAxes(entry);
272
- return issue === undefined ? undefined : entryFieldRefusal(entry, at, issue);
273
- }
274
296
  export function deploymentClosedMcpNames(baseline, reservedNames) {
275
297
  return new Set([...(baseline ?? []).map((s) => s.name), ...reservedNames]);
276
298
  }
@@ -311,7 +333,7 @@ function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
311
333
  if (v.badContentOrigin.length > 0) {
312
334
  logger?.warn("mcp_content_class_dropped", {
313
335
  entries: v.badContentOrigin,
314
- allowed: CONTENT_ORIGIN_WORDS,
336
+ allowed: MCP_CONTENT_ORIGINS,
315
337
  note: "declaration DROPPED — these servers keep the pre-378 default (external content, session gets marked); the servers themselves still mount",
316
338
  });
317
339
  }
@@ -337,6 +359,18 @@ function classifyHonored(baseline, entries, config, logger, storedToolAxes) {
337
359
  note: "this replayed request was stored before the upgrade and its toolAxes declaration does not pass the engine's reader; while one-time migration M19 is open it is read the way the pre-upgrade server read it (members that did not fit are dropped, a declaration that is not an object counts as none) and the server stays mounted — once the window closes the same request leaves this server unmounted",
338
360
  });
339
361
  }
362
+ for (const t of v.transportLost) {
363
+ if (!mounted.has(t.server))
364
+ continue;
365
+ logger?.warn("mcp_transport_members_dropped", {
366
+ server: t.server,
367
+ lost: t.lost.slice(0, MAX_KEY_ECHO),
368
+ ...(t.lost.length > MAX_KEY_ECHO ? { lostTotal: t.lost.length } : {}),
369
+ note: "this replayed request carries parts of this server's transport declaration that cannot be handed to the engine — an env / headers record that is not a plain object of string values (left out whole, as the pre-upgrade server read it), " +
370
+ "a member named \"__proto__\" in env / headers (only that member is left out: the engine refuses the name), args that are not a list of strings, a principalHeader that is not a string; " +
371
+ "the server mounts with the rest. A new submission with the same entry is refused (400)",
372
+ });
373
+ }
340
374
  return {
341
375
  mcp: mergeRequestMcp(baseline, v.ok.map(toCoreMcpServerSpec), reservedNames),
342
376
  drops: [...v.dropped, ...blocked.map((server) => ({ server, reason: "name_reserved_by_deployment" }))],
@@ -47,7 +47,7 @@
47
47
  * - (`hooks` NO LONGER deferred either — 轴B #2 注释保鲜:applied 形已接线(本文件 hooks 分片 +
48
48
  * main.ts 单用户闸),仅 malformed 才报 deferred;旧「需要 remote hook-runner」是接线前拍照。)
49
49
  */
50
- import { type PathRuleBases, type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PromptProvider, type PermissionMode } from "@sema-agent/core";
50
+ import { type PathRuleBases, type ExecutionEnv, type TaskSpec, type ThinkingLevel, type ToolPolicy, type PermissionMode } from "@sema-agent/core";
51
51
  import { type KeyClosureIssue } from "./request-key-closure.js";
52
52
  import { type RunnableHooksConfig } from "./hooks/hook-runner.js";
53
53
  import type { StoredHooksRereadM20 } from "./stored-request-hooks-m20.js";
@@ -216,7 +216,7 @@ export interface ParsedTaskSettings {
216
216
  ultracode?: boolean;
217
217
  /** hooks hook-runner(阶段一):经 `parseHooksConfig`(契约包分类器 + 服务侧上限 + 本仓会跑的条目类型)判过的 hooks 配置。像 shellEnv 一样
218
218
  * 这里只 PARSE;应用点(main.ts resolveSpec)按单用户闸(`requirePrincipal !== true`)决定翻不翻成
219
- * core `TaskSpec.hooks` 回调——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */
219
+ * 任务座 `TaskSpec.mods` 上的一张注册 `settings.hooks`——多租户 lane 收到只警告忽略(hook 命令跑在 worker host = class ② 能力授予)。 */
220
220
  hooks?: RunnableHooksConfig;
221
221
  /** [ref](7.77.0)—— CC settings 同名键:上下文窗口里**留给技能清单**的比例((0,1];CC 默认 0.01 = 1%)。
222
222
  * 超预算时引擎把描述裁短、再不够退成 name-only(`cli259.js` 的清单渲染算法),**从不**拒掉一条技能 ——
@@ -324,7 +324,7 @@ export declare function taskSettingsKeyIssue(raw: unknown): KeyClosureIssue | nu
324
324
  * 🔒 **`settings.hooks` 会不会被采纳** —— 全仓**唯一**的车道谓词(S-995,主会话裁):hook 命令跑在 WORKER HOST([ref] class ② 能力授予),所以只有单用户车道
325
325
  * (`requirePrincipal !== true`,与 scheduler / backgroundShell / lspHost / MCP 注入同一姿势)采纳;多租户车道收到 hooks **整只忽略**(`task_hooks_ignored`),绝不执行。
326
326
  * 三处读它,谁也不许内联 `requirePrincipal`(各写各的 = 迟早漂):
327
- * · `boot/resolve-spec.ts` —— 装配:不采纳 ⇒ 不装 `TaskSpec.hooks` + 一条 `task_hooks_ignored`;
327
+ * · `boot/resolve-spec.ts` —— 装配:不采纳 ⇒ 不铸 `TaskSpec.mods` 的 `settings.hooks` 注册 + 一条 `task_hooks_ignored`;
328
328
  * · `http/routes/capabilities.ts` —— 能力位 `taskSettings.hooks`(「says yes ⟺ route works」);
329
329
  * · `http/admission.ts` —— fresh 受理面的「别名 matcher 永不命中」判据的**求值深度**:采纳 ⇒ `"regex"`(运行期同一只匹配函数,带资源闸);不采纳 ⇒ `"literal"`
330
330
  * (7.112.0 的字面判:零调用方正则执行 —— 受理面是所有车道,租户不能借一只灾难回溯的 matcher 占满事件循环)。
@@ -350,24 +350,10 @@ export declare function parseTaskSettings(raw: unknown, seats?: ParseTaskSetting
350
350
  * value must not become active prompt content after rollout (context exhaustion / spend hole). Over-cap ⇒ dropped +
351
351
  * warned — the rider was inert pre-upgrade, so dropping preserves the stored task's prior behavior; a 4xx here would
352
352
  * permanently brick that task's resume instead (same defensive posture as parseTaskSettings' outputStyle drop).
353
- * Cap value = the submit gate's (16384; MAX_SETTINGS_OUTPUT_STYLE_CHARS — both land in the same spec field). */
354
- /** TRUE when `text` is an ALREADY-ASSEMBLED system prompt — core's migration guard detects the same three
355
- * constitution anchors and takes the pass-through assembler arm, whose section subset (OPAQUE_KEPT, dist-read)
356
- * has NO `core/role.append`: any append rider (top-level or settings.outputStyle) would be SILENTLY discarded
357
- * there. Shared predicate for the submit 400 gate, the resolveSpec resume mirror, and the center-declaration
358
- * probe — one definition so the three gates can't drift. */
359
- export declare function hasConstitutionAnchors(text: unknown): boolean;
360
- /** codex R9 (provider-aware append-less detection): mirrors core's ACTUAL pass-through triggers instead of a raw
361
- * body-text heuristic — the assembler's migration guard probes (a) `stableBlocks` identity declarations and
362
- * (b) `stableSystem` OUTPUT for the three anchors; a plain `userSystemPrompt` under the DEFAULT provider (no
363
- * scenario provider) never triggers it and `core/role.append` is retained, so an anchored systemPrompt there
364
- * must NOT block/strip a rider (R9 false-positive: unbranded default / scan rejected valid combos). Probing
365
- * calls the provider hooks with a minimal ctx — server providers (and centerPromptProvider) are pure functions
366
- * of ctx, and the same hooks run again inside core's assembly; a hook throw = conservative false (core will
367
- * fail the task loudly on its own). `replaceAll` owns the whole prompt(legacy free-form `system()` 腿已随 core 2.0.0 删除)
368
- * (REPLACE_ALL_EXCLUDED strips role.append) → append-less true. */
369
- export declare function providerDropsAppend(provider: PromptProvider | undefined, userSystemPrompt: string | undefined): boolean;
370
- export declare function acceptAppendSystemPrompt(v: unknown, warn?: (detail: string) => void, packDropsAppend?: boolean): string | undefined;
353
+ * Cap value = the submit gate's (16384; MAX_SETTINGS_OUTPUT_STYLE_CHARS — both land in the same spec field).
354
+ * A well-formed rider always mounts: the engine (core 7.52.0) lands it in its own task-append band whatever the role
355
+ * text is — there is no "already-assembled" pass-through arm left that could drop it. */
356
+ export declare function acceptAppendSystemPrompt(v: unknown, warn?: (detail: string) => void): string | undefined;
371
357
  /** [ref]②/codex F2 — what the workflow ask leg needs from the deployment, on EVERY lane. It is name-keyed and
372
358
  * fs-independent, so it never had the lane split {@link FsWriteGateWiring} carried (that split is itself gone
373
359
  * since [ref] — the sandbox lanes wire a gate of their own shape instead of no gate). */
@@ -1,7 +1,8 @@
1
- import { combinePolicies, createFsWriteGatePolicy, parseRuleText, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, isPermissionMode, ruleToolGrammarOf, } from "@sema-agent/core";
1
+ import { combinePolicies, createFsWriteGatePolicy, parseRuleText, resolveReasoningProfile, rankOf, isThinkingLevel, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, isPermissionMode, ruleToolGrammarOf, } from "@sema-agent/core";
2
2
  import { unmatchableToolNamesRefusal } from "./config.js";
3
3
  import { MAX_PERMISSION_RULES_PER_LIST, createRuleListPolicy } from "./permission-rule-list.js";
4
4
  import { allowLayerEntryCountIssue, describeAllowLayerOverCap, ruleEntryListShapeOk } from "./rule-entry-list.js";
5
+ import { limitRefusal } from "./limit-refusal.js";
5
6
  import { tightenGoverned } from "./governance-overrides.js";
6
7
  import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
7
8
  import { parseHooksConfig } from "./hooks/hook-runner.js";
@@ -93,8 +94,9 @@ function bucketIssueMessage(f, rule) {
93
94
  case "over_cap":
94
95
  switch (rule) {
95
96
  case "rule_policy":
96
- return (`${f.field} has ${f.count} entries — at most ${f.cap} are accepted. Refused rather than truncated: a truncated list silently drops the rules past the cap, ` +
97
- `and a dropped deny / ask rule is a tightening that never happened. Merge rules into broader patterns or trim the list`);
97
+ return limitRefusal(f.field, "entries", f.count, f.cap, {
98
+ tail: "Refused rather than truncated: a truncated list silently drops the rules past the cap, and a dropped deny / ask rule is a tightening that never happened. Merge rules into broader patterns or trim the list",
99
+ }).message;
98
100
  case "allow_layer":
99
101
  return describeAllowLayerOverCap(f.field, f.count);
100
102
  }
@@ -138,7 +140,7 @@ export function settingsPermissionBucketIssue(perms) {
138
140
  case "allow_layer": {
139
141
  const over = allowLayerEntryCountIssue(field, v);
140
142
  if (over !== undefined)
141
- fields = { field, reason: "over_cap", count: over.count, cap: over.cap };
143
+ fields = { field, reason: "over_cap", count: over.facts.count, cap: over.facts.cap };
142
144
  else if (!ruleEntryListShapeOk(v))
143
145
  fields = { field, reason: "bad_entry", index: v.findIndex((x) => typeof x !== "string") };
144
146
  break;
@@ -262,34 +264,7 @@ export function parseTaskSettings(raw, seats = {}) {
262
264
  out.skillListingMaxDescChars = maxDesc;
263
265
  return { settings: Object.keys(out).length > 0 ? out : undefined, deferred, ...(hooksReread !== undefined ? { hooksReread } : {}) };
264
266
  }
265
- export function hasConstitutionAnchors(text) {
266
- return typeof text === "string" && text.includes(CYBER_RISK) && text.includes(URL_SAFETY) && text.includes(HARNESS_SECTION_ANCHOR);
267
- }
268
- export function providerDropsAppend(provider, userSystemPrompt) {
269
- if (!provider)
270
- return false;
271
- if (provider.replaceAll === true)
272
- return true;
273
- const ctx = { userSystemPrompt, tools: [], memoryEnabled: false, consolidationEnabled: false };
274
- if (provider.stableBlocks) {
275
- try {
276
- return provider.stableBlocks(ctx).some((d) => d.slot === "identity" && hasConstitutionAnchors(d.text));
277
- }
278
- catch {
279
- return false;
280
- }
281
- }
282
- if (provider.stableSystem) {
283
- try {
284
- return hasConstitutionAnchors(provider.stableSystem(ctx));
285
- }
286
- catch {
287
- return false;
288
- }
289
- }
290
- return false;
291
- }
292
- export function acceptAppendSystemPrompt(v, warn, packDropsAppend) {
267
+ export function acceptAppendSystemPrompt(v, warn) {
293
268
  if (v === undefined)
294
269
  return undefined;
295
270
  if (typeof v !== "string" || v.length === 0) {
@@ -300,10 +275,6 @@ export function acceptAppendSystemPrompt(v, warn, packDropsAppend) {
300
275
  warn?.(`appendSystemPrompt dropped: ${v.length} chars exceeds the ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} cap (pre-cap stored body on a resume leg?)`);
301
276
  return undefined;
302
277
  }
303
- if (packDropsAppend === true) {
304
- warn?.("appendSystemPrompt dropped: the effective prompt pack cannot mount the append rider (assembled-identity/stableSystem pass-through, replaceAll, or legacy provider — no core/role.append section)");
305
- return undefined;
306
- }
307
278
  return v;
308
279
  }
309
280
  const RULE_SOURCE_BY_BEHAVIOR = { ask: "settings.permissions.ask", deny: "settings.permissions.deny" };
@@ -1,50 +1,7 @@
1
- /**
2
- * `text-bidi.ts` —— 双向控制符(bidi)与 C0 控制符字符类的**单一属主**。
3
- *
4
- * ## 为什么有这个文件
5
- * 同一条字符类此前在本仓有两份逐字手抄(`session-titler.ts` 的 `sanitizeTitle`、
6
- * `plugins/send-user-file.ts` 的 `sanitizeSendFileName`),第三份还要给审批卡的 bidi **在场检测**用。
7
- * 这是一条安全语义上的词表(它划定「人眼看到的顺序可能不是字节顺序」那一族码点),三份手抄必然漂移 ——
8
- * 单一属主的纪律与 `APPROVAL_GATE_KINDS` 同源(CLAUDE.md 硬条款:词表是闭集,不许散落副本)。
9
- *
10
- * ## 两条类刻意分开(**别合并**)
11
- * · {@link BIDI_CONTROL_CHARS} —— **只有** bidi 一族(LRM/RLM、嵌入与覆写、隔离符)。它是**检测**用的:
12
- * 问的是「这串文本里有没有能让人眼读到与执行顺序不同的东西」。C0 控制符不属于这一问 —— 一条 Bash
13
- * 命令里出现换行、一段 diff 里出现制表符都是**正常**的,把它们算进来会让检测位恒为真、从而毫无判别力。
14
- * · {@link CONTROL_AND_BIDI_CHARS} —— bidi ∪ C0/DEL,**清洗**用的(标题、文件名这类要落一行的短串)。
15
- * 清洗面比检测面宽是对的:那两处要的是「只留可打印的一行」。
16
- *
17
- * ## 案锚
18
- * Trojan Source([ref]②;`docs/DISEASE-SHAPES.md` 的不可见字符条):字面控制符让**人审与机器门看到
19
- * 两份不同的文本**。审批卡是本仓唯一一处「把模型写的命令交给人眼判断」的面,所以它需要的是**披露**
20
- * (告诉壳「这串里有 bidi」),不是清洗 —— 清洗会改掉即将被执行的那串字节,让卡上显示的与真正跑的不是
21
- * 同一个东西,那比不披露更坏。
22
- */
23
- /**
24
- * bidi 一族的字符类**内容**(不含方括号,便于拼进更大的类)。
25
- *
26
- * 逐字 = Unicode 的 `Bidi_Control=Yes` **全集**(12 个码点),不是「常见的那几个」:
27
- * · U+061C = ALM(Arabic Letter Mark);U+200E/200F = LRM/RLM;
28
- * · U+202A-202E = 嵌入/覆写族(含 RLO);U+2066-2069 = 隔离符族。
29
- *
30
- * 🔴 **U+061C 是 codex 交叉复审 [medium] 验真后补的**:首版漏了它 —— 而漏一个码点在**检测**面上不是
31
- * 「覆盖率差一点」,是一条完整的跳过路(只用 ALM 构造的输入让在场位缺席,壳于是照常按干净渲染)。
32
- * 交叉锚:core 自己的 `APPROVER_REFUSED_CHARS_RE`(`dist/core/tool-policy.js`)同样含 `\\u061c`。
33
- *
34
- * ⚠️ 与 core 那只**更宽**的集合的差额如实登记(刻意,不是漏):core 还含 C1(`\\u0080-\\u009f`)与
35
- * U+2028/U+2029(行/段分隔符)。那两族**不是 bidi**(不影响阅读方向),属于「不可打印 / 换行伪装」的
36
- * 另一问;本类是 bidi 专用,混进来会让 {@link hasBidiControls} 的语义从「方向可能骗你」滑成「有奇怪
37
- * 字符」。清洗面要不要跟着加宽是另一件(见 {@link CONTROL_AND_BIDI_CHARS} 的注)。
38
- */
39
- export declare const BIDI_CONTROL_CHARS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
40
- /** bidi ∪ C0/DEL —— **清洗**面用(见文件头注:清洗面比检测面宽是刻意的)。
41
- * ⚠️ 仍**不含** C1(`€-Ÿ`)与 U+2028/U+2029:加宽是行为变更、要各自的判据支撑,不搭本批的车
42
- * (登记在此,别当成漏)。两处存量消费方(标题、文件名)较其手抄前身**多剥一枚 U+061C**
43
- * (经 {@link BIDI_CONTROL_CHARS} 内插带入)—— 对本清洗类是有意收紧,merge-rescan 745 验真后
44
- * 钉在两消费方各自的测试里;除此之外与手抄前身逐字同集。 */
45
- export declare const CONTROL_AND_BIDI_CHARS = "\\u0000-\\u001f\\u007f\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
1
+ /** bidi ∪ C0/DEL —— **清洗**面用(见文件头注:清洗面比检测面宽是刻意的)。bidi 半场内插 core 的类,C0/DEL 半场是本仓清洗面自己的。 */
2
+ export declare const CONTROL_AND_BIDI_CHARS = "\\u0000-\\u001f\\u007f\\u061C\\u200E\\u200F\\u202A-\\u202E\\u2066-\\u2069";
46
3
  /** 清洗用的全局正则(`String.replace` 会在返回前把 `lastIndex` 归零,故模块级常量可安全复用)。 */
47
4
  export declare const CONTROL_AND_BIDI_STRIP_RE: RegExp;
48
- /** 这串文本里**有没有** bidi 控制符。纯谓词:不改一个字节、不做任何清洗(理由见文件头注)。 */
5
+ /** 这串文本里**有没有** bidi 控制符(判官 = core 的 {@link carriesBidiControls})。纯谓词:不改一个字节、不做任何清洗(理由见文件头注)。 */
49
6
  export declare function hasBidiControls(text: string): boolean;
50
7
  //# sourceMappingURL=text-bidi.d.ts.map
package/dist/text-bidi.js CHANGED
@@ -1,8 +1,8 @@
1
- export const BIDI_CONTROL_CHARS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
1
+ import { BIDI_CONTROL_CHARS, carriesBidiControls } from "@sema-agent/core";
2
2
  export const CONTROL_AND_BIDI_CHARS = `\\u0000-\\u001f\\u007f${BIDI_CONTROL_CHARS}`;
3
3
  export const CONTROL_AND_BIDI_STRIP_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`, "g");
4
- const BIDI_DETECT_RE = new RegExp(`[${BIDI_CONTROL_CHARS}]`);
4
+ const WHOLE_TEXT_SCAN = { maxNodes: 1, maxChars: Number.MAX_SAFE_INTEGER };
5
5
  export function hasBidiControls(text) {
6
- return BIDI_DETECT_RE.test(text);
6
+ return carriesBidiControls(text, WHOLE_TEXT_SCAN);
7
7
  }
8
8
  //# sourceMappingURL=text-bidi.js.map
@@ -509,7 +509,7 @@ export interface ToolApprovalFrame {
509
509
  readRootCandidate?: ReadRootGrantCandidate;
510
510
  /**
511
511
  * **E-14**([ref]② Trojan Source 族;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**工具输入**
512
- * (`AskRequest.args`)里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符;字符类属主 `text-bidi.ts`)。
512
+ * (`AskRequest.args`)里含 bidi 控制符(LRM/RLM、嵌入/覆写、隔离符;字符类与在场谓词的属主是 core 的 `BIDI_CONTROL_CHARS` / `carriesBidiControls`,经 `text-bidi.ts` 的 `hasBidiControls` 取)。
513
513
  *
514
514
  * 🔴 **真才带,缺席绝不编 `false`**(与 `governanceForced`/`requiresRealApproval` 同形)。缺席的含义是
515
515
  * 「没扫到」——它同时覆盖「真的没有」与「args 序列化不了(循环引用)所以扫不了」两形,消费端**禁**把
@@ -50,7 +50,7 @@ type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded,
50
50
  type AskFrameServerMinted = "type" | "approvalId" | "argsOmitted" | "outcome" | "governanceForced" | "expiresAtMs" | "expiresInMs" | "serverNowMs" | "parked" | "cause" | "inputHasBidi" | "rememberRefused";
51
51
  type _GuardAskFrame = AssertAllKeysHandled<Exclude<keyof ToolApprovalFrame, AskProjected | AskFrameServerMinted>>;
52
52
  type _GuardAskFrameReverse = AssertAllKeysHandled<Exclude<AskProjected | AskFrameServerMinted, keyof ToolApprovalFrame>>;
53
- type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end" | "tool_roster_delta" | "tool_progress" | "tool_disclosure" | "task_started";
53
+ type TaskEventHandled = "text_delta" | "reasoning_delta" | "tool_start" | "tool_end" | "turn_end" | "compacted" | "diagnostics" | "message_committed" | "status" | "task_notification" | "task_progress" | "steering_injected" | "workspace_changed" | "done" | "context_usage" | "compaction_outcome" | "human_input" | "wiring_manifest" | "text_end" | "tool_roster_delta" | "tool_progress" | "tool_disclosure" | "task_started" | "compaction_started";
54
54
  type _GuardTaskEvent = AssertAllKeysHandled<Exclude<TaskEvent["type"], TaskEventHandled>>;
55
55
  type _GuardTaskEventReverse = AssertAllKeysHandled<Exclude<TaskEventHandled, TaskEvent["type"]>>;
56
56
  type MailboxProjected = "seq" | "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
@@ -59,7 +59,7 @@ type _GuardMailboxReverse = AssertAllKeysHandled<Exclude<MailboxProjected, keyof
59
59
  type MailboxAppendProjected = "from" | "content" | "sentAt" | "hopChain" | "peerMeta";
60
60
  type _GuardMailboxAppend = AssertAllKeysHandled<Exclude<keyof MailboxAppendMessage, MailboxAppendProjected>>;
61
61
  type _GuardMailboxAppendReverse = AssertAllKeysHandled<Exclude<MailboxAppendProjected, keyof MailboxAppendMessage>>;
62
- type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "hooks" | "lsp" | "hands" | "writeProtection" | "teamRole" | "fileHistory";
62
+ type WiringSectionProjected = "schemaVersion" | "leg" | "ask" | "question" | "interaction" | "elicit" | "parkLane" | "session" | "fleet" | "permissionRules" | "modelGate" | "autoMode" | "mcp" | "tools" | "mods" | "lsp" | "hands" | "writeProtection" | "teamRole" | "fileHistory";
63
63
  type WiringSectionExcluded = (typeof import("./wire-projection-faces.js").WIRE_PROJECTION_FACES)["wiring_manifest_tenant"]["excluded"][number];
64
64
  type _GuardWiringSections = AssertAllKeysHandled<Exclude<keyof WiringManifest, WiringSectionProjected | WiringSectionExcluded>>;
65
65
  type _GuardWiringSectionsReverse = AssertAllKeysHandled<Exclude<WiringSectionProjected | WiringSectionExcluded, keyof WiringManifest>>;
@@ -67,6 +67,16 @@ type WiringSection<K extends keyof WiringManifest> = NonNullable<WiringManifest[
67
67
  type WiringLegHandled = "kind";
68
68
  type _GuardWiringLeg = AssertAllKeysHandled<Exclude<keyof WiringSection<"leg">, WiringLegHandled>>;
69
69
  type _GuardWiringLegReverse = AssertAllKeysHandled<Exclude<WiringLegHandled, keyof WiringSection<"leg">>>;
70
+ type WiringModRowKeys = WiringSection<"mods">[number] extends infer R ? (R extends unknown ? keyof R : never) : never;
71
+ type WiringModRowHandled = "name" | "seat" | "tier" | "events" | "observe" | "skipped" | "refusedBy" | "reason";
72
+ type _GuardWiringModRow = AssertAllKeysHandled<Exclude<WiringModRowKeys, WiringModRowHandled>>;
73
+ type _GuardWiringModRowReverse = AssertAllKeysHandled<Exclude<WiringModRowHandled, WiringModRowKeys>>;
74
+ type WiringModSkipKeys = keyof NonNullable<Extract<WiringSection<"mods">[number], {
75
+ events: unknown;
76
+ }>["skipped"]>[number];
77
+ type WiringModSkipHandled = "registration" | "cause" | "reason";
78
+ type _GuardWiringModSkip = AssertAllKeysHandled<Exclude<WiringModSkipKeys, WiringModSkipHandled>>;
79
+ type _GuardWiringModSkipReverse = AssertAllKeysHandled<Exclude<WiringModSkipHandled, WiringModSkipKeys>>;
70
80
  type WiringAskHandled = "form" | "provenance" | "effective";
71
81
  type _GuardWiringAsk = AssertAllKeysHandled<Exclude<keyof WiringSection<"ask">, WiringAskHandled>>;
72
82
  type _GuardWiringAskReverse = AssertAllKeysHandled<Exclude<WiringAskHandled, keyof WiringSection<"ask">>>;
@@ -136,6 +146,11 @@ type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTrace
136
146
  /** `T` 非 `never` 时收敛到 `never`(空集 ⇒ 残留 `"EMPTY"` ⇒ tsc 红)。 */
137
147
  type AssertNonEmpty<T> = [T] extends [never] ? "EMPTY" : never;
138
148
  type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<PermissionTraceKind>>;
149
+ type PromptAssembledFrame = Extract<TraceEvent, {
150
+ kind: "prompt.assembled";
151
+ }>;
152
+ type PromptManifestNotOnRow = (typeof import("./wire-projection-faces.js").WIRE_PROJECTION_FACES)["prompt_manifest"]["excluded"][number];
153
+ type _GuardPromptManifestNotOnRowReverse = AssertAllKeysHandled<Exclude<PromptManifestNotOnRow, keyof PromptAssembledFrame>>;
139
154
  type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "mandated" | "mandate" | "boundGrantDigest" | "boundGrants" | "classifierNoRuling" | "boundGrantAbsence";
140
155
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "approvalCard" | "restoreMode" | "checkpointId" | "previewWithheld" | "authoredBy";
141
156
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
@@ -148,9 +163,9 @@ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCal
148
163
  type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
149
164
  type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
150
165
  type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
151
- type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptProvider" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "hooks" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "outputStyle" | "allowedTools" | "allowTools" | "userAttachments";
166
+ type TaskSpecWired = "objective" | "principal" | "images" | "sessionId" | "requireExistingSession" | "oneShot" | "clientContext" | "resumeAt" | "resumeAtMode" | "model" | "compactionModel" | "thinking" | "systemPrompt" | "appendSystemPrompt" | "promptRole" | "promptBinding" | "memory" | "suggestNextPrompts" | "restoreFiles" | "acceptPartialRestore" | "rewindFilesTo" | "tools" | "excludeTools" | "deferTools" | "toolMaterializeStrategy" | "promptProfile" | "toolPolicy" | "onAsk" | "onQuestion" | "mods" | "durableApproval" | "resourceSuspend" | "resilience" | "finalVerification" | "checkpointStore" | "handsReadOnly" | "memoryPersistenceCapable" | "additionalDirectories" | "additionalReadDirectories" | "enablePlanMode" | "interactiveTools" | "enableFork" | "shellGate" | "approvalReach" | "writeFace" | "mcp" | "a2a" | "skills" | "backgroundScope" | "envFacts" | "limits" | "configOverrides" | "forwardSubagentEvents" | "retainSubagentSessions" | "retainBackgroundProcesses" | "outputSchema" | "outputRetries" | "compaction" | "attachments" | "selfOrchestration" | "getApiKeyAndHeaders" | "interactionPosture" | "agents" | "permissionMode" | "taskId" | "signal" | "preemptSignal" | "maxSuspends" | "skillListingBudgetFraction" | "skillListingMaxDescChars" | "excludeAllTools" | "fileHistory" | "userInvokedSkills" | "outputStyle" | "allowedTools" | "allowTools" | "userAttachments";
152
167
  /** 导出:请求体键闭集门(`request-key-closure.ts`)据它锁「本仓记为未接线的引擎座,提交口不许受理」—— 两处分类只能同向。 */
153
- export type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "writeProtectedAdditions" | "complianceAdditionalDenies" | "modelRole" | "enableBlockedReport";
168
+ export type TaskSpecUnwired = "actor" | "roles" | "lspManager" | "tracer" | "readFace" | "readDenyPatterns" | "interactiveQuestionFallback" | "alwaysLoadTools" | "restoreGatedTools" | "deferSelfResolve" | "basePolicyForResumeEdit" | "lspDiagnostics" | "streamingToolExecution" | "toolResultThresholdChars" | "peerPresence" | "writeProtectedAdditions" | "complianceAdditionalDenies" | "modelRole" | "enableBlockedReport" | "promptForm";
154
169
  type _GuardTaskSpec = AssertAllKeysHandled<Exclude<keyof TaskSpec, TaskSpecWired | TaskSpecUnwired>>;
155
170
  type _GuardTaskSpecReverse = AssertAllKeysHandled<Exclude<TaskSpecWired | TaskSpecUnwired, keyof TaskSpec>>;
156
171
  type TaskResultForwarded = "taskId" | "runId" | "sessionId" | "terminal" | "model" | "effectiveReasoning" | "result" | "salvagedOutput" | "remoteEnvFailures" | "apiFailure" | "retryAfterMs" | "degraded" | "structuredOutput" | "rewindNotes" | "haltedOnUserRejection" | "haltedByUser" | "strandedHumanAnswers" | "pendingApproval" | "approvalWaitedMs" | "effectiveReadFace" | "effectiveReadDenyPatterns" | "effectiveWriteFace" | "effectiveMemoryScopes" | "stats" | "editedFiles" | "responseModel" | "compaction";
@@ -294,6 +309,10 @@ export type CoreKeysetGuards = [
294
309
  _GuardWiringFleetReverse,
295
310
  _GuardSubagentTranscriptTierWords,
296
311
  _GuardSubagentTranscriptTierWordsReverse,
312
+ _GuardWiringModRow,
313
+ _GuardWiringModRowReverse,
314
+ _GuardWiringModSkip,
315
+ _GuardWiringModSkipReverse,
297
316
  _GuardWiringMcp,
298
317
  _GuardWiringMcpReverse,
299
318
  _GuardWiringRules,
@@ -306,6 +325,7 @@ export type CoreKeysetGuards = [
306
325
  _GuardWiringGovernanceReverse,
307
326
  _GuardPermissionTrace,
308
327
  _GuardPermissionTraceReverse,
328
+ _GuardPromptManifestNotOnRowReverse,
309
329
  _GuardPermissionTraceNonEmpty,
310
330
  _GuardInboxSummary,
311
331
  _GuardInboxSummaryReverse,