@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,10 +1,11 @@
1
- import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
2
- import { matchCatalogModel, refusedCatalogEntry } from "../model-select.js";
1
+ import { admitRouteRequest, BrainError, joinRequestUrl, resolveRouteCredential, routeKeyOf } from "@sema-agent/core";
2
+ import { matchCatalogModel, refusedCatalogEntry, servedModelTableLeg } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { resolveOpenaiRouteEndpoint } from "../model-route-endpoint.js";
5
- import { dispatchBrainLane } from "../model-provider.js";
5
+ import { anthropicRouteRoot, brainLaneFor, routeTableOf } from "../model-provider.js";
6
6
  import { egressRedirectRefusal } from "../egress-redirect.js";
7
7
  import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
8
+ const unresolvedPick = (error) => ({ ok: false, error, code: "model_unresolved" });
8
9
  export function buildHookFailureText(raw) {
9
10
  if (raw === "")
10
11
  return raw;
@@ -15,68 +16,86 @@ export function buildHookFailureText(raw) {
15
16
  return `[hook failure text withheld: redactor threw ${err instanceof Error ? err.name : typeof err}]`;
16
17
  }
17
18
  }
19
+ const ROUTE_REFUSAL_CODES = {
20
+ "route.credential_mismatch": true,
21
+ "route.credential_missing": true,
22
+ "route.credential_origin_mismatch": true,
23
+ "route.credential_answer_conflict": true,
24
+ };
25
+ const ROUTE_REFUSAL_LEAD = "[invalid_request] ";
26
+ function gateRefusal(pick, e, declaredRoot) {
27
+ const raw = e instanceof Error ? e.message : String(e);
28
+ const text = e instanceof BrainError && raw.startsWith(ROUTE_REFUSAL_LEAD) ? raw.slice(ROUTE_REFUSAL_LEAD.length) : raw;
29
+ const code = Object.keys(ROUTE_REFUSAL_CODES).find((c) => text.startsWith(`${c}:`)) ?? "unclassified";
30
+ return {
31
+ ok: false,
32
+ code: "route_refused",
33
+ error: `hook model "${pick.catalogRef}" cannot be routed (${code}) — ` + buildDisplaySafeRefusal(text, [pick.baseUrl, declaredRoot ?? ""]),
34
+ };
35
+ }
18
36
  export function createHookLlm(deps) {
19
37
  const { config, metrics } = deps;
20
- const anthropicRouteBaseUrl = () => config.anthropic?.baseUrl || "https://api.anthropic.com";
21
- const shapeFrom = (catalogRef, m, providerModelId) => {
22
- const lane = dispatchBrainLane(m?.provider, config.anthropic !== undefined);
38
+ const anthropicRouteBaseUrl = () => anthropicRouteRoot(config.anthropic ?? {});
39
+ const shapeFrom = (m, providerModelId = m.id) => {
40
+ const catalogRef = m.name;
41
+ const lane = brainLaneFor(m.provider, routeTableOf(config));
23
42
  switch (lane) {
24
43
  case "anthropic":
25
- return { ok: true, catalogRef, providerModelId, baseUrl: m?.baseUrl || anthropicRouteBaseUrl(), api: "anthropic-messages" };
44
+ return { ok: true, catalogRef, providerModelId, baseUrl: m.baseUrl || anthropicRouteBaseUrl(), api: "anthropic-messages" };
26
45
  case "gateway": {
27
- const endpoint = resolveOpenaiRouteEndpoint(catalogRef, m?.baseUrl, config.gatewayBaseUrl);
28
- return endpoint.ok ? { ok: true, catalogRef, providerModelId, baseUrl: endpoint.baseUrl, api: "openai-completions" } : { ok: false, error: endpoint.refusal };
46
+ const endpoint = resolveOpenaiRouteEndpoint(catalogRef, m, config.gatewayBaseUrl, servedModelTableLeg(config));
47
+ return endpoint.ok ? { ok: true, catalogRef, providerModelId, baseUrl: endpoint.baseUrl, api: "openai-completions" } : unresolvedPick(endpoint.refusal);
29
48
  }
30
49
  }
31
50
  };
32
51
  const hookModelFor = (requested) => {
33
- const shape = (ref) => shapeFrom(ref, config.models[ref], config.models[ref]?.id ?? ref);
52
+ const ownEntry = (ref) => (Object.hasOwn(config.models, ref) ? config.models[ref] : undefined);
34
53
  const unavailable = (field, ref) => {
35
- if (config.models[ref] !== undefined)
54
+ if (ownEntry(ref) !== undefined)
36
55
  return undefined;
37
56
  const hit = refusedCatalogEntry(ref, config.modelRefusals);
38
- return hit === undefined ? undefined : { ok: false, error: modelUnavailableMessage(field, ref, hit.name, hit.refusal) };
57
+ return hit === undefined ? undefined : unresolvedPick(modelUnavailableMessage(field, ref, hit.name, hit.refusal));
39
58
  };
40
59
  if (requested === undefined) {
41
60
  const sum = config.roles.summarize;
42
61
  const roleModel = typeof sum === "string" ? sum : sum && typeof sum === "object" && "model" in sum && typeof sum.model === "string" ? sum.model : undefined;
43
- if (roleModel !== undefined)
44
- return unavailable("summarize role model", roleModel) ?? (config.models[roleModel] ? shape(roleModel) : shapeFrom(config.model.id, config.model, roleModel));
45
- return shapeFrom(config.model.id, config.model, config.model.id);
62
+ if (roleModel !== undefined) {
63
+ const entry = ownEntry(roleModel);
64
+ return unavailable("summarize role model", roleModel) ?? (entry ? shapeFrom(entry) : shapeFrom(config.model, roleModel));
65
+ }
66
+ return shapeFrom(config.model);
46
67
  }
47
- if (config.models[requested])
48
- return shape(requested);
68
+ const matchedKey = matchCatalogModel(requested, config.models, config.modelRefusals);
69
+ if (matchedKey !== undefined)
70
+ return shapeFrom(config.models[matchedKey]);
49
71
  if (Object.hasOwn(config.modelRefusals ?? {}, requested))
50
72
  return unavailable("hook model", requested);
51
- const byId = matchCatalogModel(requested, config.models, config.modelRefusals);
52
- if (byId !== undefined)
53
- return shape(byId);
54
73
  if (requested === config.model.id)
55
- return shapeFrom(requested, config.model, requested);
56
- return unavailable("hook model", requested) ?? { ok: false, error: `hook model "${requested}" is not in this deployment's catalog` };
74
+ return shapeFrom(config.model);
75
+ return unavailable("hook model", requested) ?? unresolvedPick(`hook model "${requested}" is not in this deployment's catalog`);
57
76
  };
58
77
  const judgeRoute = (pick, perModelKey, deploymentKey, declaredRoot) => {
59
78
  const credential = resolveRouteCredential({
60
79
  ...(perModelKey !== undefined ? { optionsApiKey: perModelKey } : {}),
61
80
  ...(deploymentKey !== undefined ? { configApiKey: deploymentKey } : {}),
62
81
  });
63
- const verdict = adjudicateModelRoute({ id: pick.catalogRef, baseUrl: pick.baseUrl }, credential, { ...(declaredRoot !== undefined ? { baseUrl: declaredRoot } : {}) });
64
- if (!verdict.ok) {
65
- return {
66
- ok: false,
67
- error: `hook model "${pick.catalogRef}" cannot be routed (${verdict.code}) — ` +
68
- buildDisplaySafeRefusal(routeRefusalText(verdict), [pick.baseUrl, declaredRoot ?? ""]),
69
- };
82
+ let admission;
83
+ try {
84
+ admission = admitRouteRequest({ id: pick.catalogRef, baseUrl: pick.baseUrl }, credential, { ...(declaredRoot !== undefined ? { baseUrl: declaredRoot } : {}) });
70
85
  }
71
- if (verdict.posture === "keyless" && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
86
+ catch (e) {
87
+ return gateRefusal(pick, e, declaredRoot);
88
+ }
89
+ if (credential.source === "absent" && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
72
90
  return {
73
91
  ok: false,
92
+ code: "route_refused",
74
93
  error: `hook model "${pick.catalogRef}" points at ${pick.baseUrl}, which this deployment did not declare as its own endpoint, ` +
75
94
  `and no per-model credential resolved — refusing to send the prompt there (it embeds user content; an unauthenticated ` +
76
95
  `request still hands that content to whoever operates that URL)`,
77
96
  };
78
97
  }
79
- return { ok: true, credential };
98
+ return { ok: true, credential, admission };
80
99
  };
81
100
  const fetchImpl = deps.fetchImpl ?? fetch;
82
101
  const callLlm = async ({ prompt, model, timeoutMs, system, maxOutputTokens }) => {
@@ -85,6 +104,7 @@ export function createHookLlm(deps) {
85
104
  return pick;
86
105
  const ac = new AbortController();
87
106
  const t = setTimeout(() => ac.abort(), timeoutMs);
107
+ let phase = "assemble";
88
108
  try {
89
109
  const keyResolver = deps.getKeyResolver();
90
110
  let perModelKey;
@@ -93,17 +113,21 @@ export function createHookLlm(deps) {
93
113
  perModelKey = (await keyResolver({ name: pick.catalogRef }))?.apiKey;
94
114
  }
95
115
  catch (e) {
96
- return { ok: false, error: `per-model key resolution for "${pick.catalogRef}" failed — refusing to fall back to the shared route credential (${String(e)})` };
116
+ return { ok: false, error: `per-model key resolution for "${pick.catalogRef}" failed — refusing to fall back to the shared route credential (${String(e)})`, code: "credential_custody_failed" };
97
117
  }
98
118
  }
99
119
  let url;
100
120
  let headers;
101
121
  let body;
122
+ let admitted;
123
+ let declaredRoot;
102
124
  if (pick.api === "anthropic-messages") {
103
125
  const routeBase = anthropicRouteBaseUrl();
126
+ declaredRoot = routeBase;
104
127
  const routed = judgeRoute(pick, perModelKey, config.anthropic?.authToken ?? config.anthropic?.apiKey, routeBase);
105
128
  if (!routed.ok)
106
129
  return routed;
130
+ admitted = routed.admission;
107
131
  url = joinRequestUrl(pick.baseUrl, "/v1/messages");
108
132
  headers = {
109
133
  "content-type": "application/json",
@@ -121,29 +145,47 @@ export function createHookLlm(deps) {
121
145
  body = JSON.stringify({ model: pick.providerModelId, max_tokens: maxTokens, ...(system ? { system } : {}), messages: [{ role: "user", content: prompt }] });
122
146
  }
123
147
  else {
148
+ declaredRoot = config.gatewayBaseUrl;
124
149
  const routed = judgeRoute(pick, perModelKey, config.gatewayApiKey, config.gatewayBaseUrl);
125
150
  if (!routed.ok)
126
151
  return routed;
152
+ admitted = routed.admission;
127
153
  const hookKey = routed.credential.apiKey;
128
154
  url = joinRequestUrl(pick.baseUrl, "/chat/completions");
129
155
  headers = { "content-type": "application/json", ...(hookKey ? { authorization: `Bearer ${hookKey}` } : {}) };
130
156
  body = JSON.stringify({ model: pick.providerModelId, ...(maxOutputTokens !== undefined ? { max_tokens: maxOutputTokens } : {}), messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }], stream: false });
131
157
  }
158
+ try {
159
+ admitted();
160
+ }
161
+ catch (e) {
162
+ return gateRefusal(pick, e, declaredRoot);
163
+ }
164
+ phase = "transport";
132
165
  const r = await fetchImpl(url, { method: "POST", headers, body, signal: ac.signal, redirect: "manual" });
133
166
  const redirected = egressRedirectRefusal(r, "declare the final endpoint as this route's base URL");
134
167
  if (redirected !== undefined)
135
168
  return { ok: false, error: `hook llm ${redirected}`, code: "redirect_refused" };
136
169
  if (!r.ok)
137
- return { ok: false, error: `hook llm HTTP ${r.status}` };
170
+ return { ok: false, error: `hook llm HTTP ${r.status}`, code: "bad_response" };
171
+ let answered;
172
+ try {
173
+ answered = await r.json();
174
+ }
175
+ catch (e) {
176
+ if (ac.signal.aborted || e?.name !== "SyntaxError")
177
+ throw e;
178
+ return { ok: false, error: "hook llm answered with a body that is not JSON", code: "bad_response" };
179
+ }
138
180
  let text;
139
181
  if (pick.api === "anthropic-messages") {
140
- const j = (await r.json());
141
- const parts = (Array.isArray(j.content) ? j.content : []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text);
182
+ const j = answered;
183
+ const parts = (Array.isArray(j?.content) ? j.content : []).filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text);
142
184
  text = parts.length > 0 ? parts.join("") : undefined;
143
185
  }
144
186
  else {
145
- const j = (await r.json());
146
- text = j.choices?.[0]?.message?.content ?? undefined;
187
+ const j = answered;
188
+ text = j?.choices?.[0]?.message?.content ?? undefined;
147
189
  }
148
190
  metrics.inc("hook_llm_calls_total", { type: "prompt" });
149
191
  return typeof text === "string" ? { ok: true, text } : { ok: false, error: "hook llm returned no content", code: "no_content" };
@@ -153,6 +195,7 @@ export function createHookLlm(deps) {
153
195
  return {
154
196
  ok: false,
155
197
  error: ac.signal.aborted ? "hook llm timeout" : `hook llm request to ${pick.baseUrl} failed: ${String(e)}${cause ? ` (${String(cause)})` : ""}`,
198
+ code: ac.signal.aborted || phase === "transport" ? "unreachable" : "route_refused",
156
199
  };
157
200
  }
158
201
  finally {