@sema-agent/server 1.313.0 → 1.315.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 (83) hide show
  1. package/dist/approval-hmac.d.ts +17 -0
  2. package/dist/approval-hmac.js +27 -0
  3. package/dist/auth-bridge.d.ts +3 -2
  4. package/dist/bench/s1/live-deps.js +2 -2
  5. package/dist/budget.d.ts +4 -76
  6. package/dist/budget.js +4 -83
  7. package/dist/capabilities/sandbox-file-send.d.ts +2 -1
  8. package/dist/config-center/apply-effective.d.ts +22 -0
  9. package/dist/config-center/apply-effective.js +283 -0
  10. package/dist/config-center/http-client.d.ts +23 -0
  11. package/dist/config-center/http-client.js +109 -0
  12. package/dist/config-center/restart-signal.d.ts +12 -0
  13. package/dist/config-center/restart-signal.js +70 -0
  14. package/dist/config-center/skills-mcp.d.ts +13 -0
  15. package/dist/config-center/skills-mcp.js +113 -0
  16. package/dist/config-center/types.d.ts +143 -0
  17. package/dist/config-center/types.js +2 -0
  18. package/dist/config-types.d.ts +1 -1
  19. package/dist/elicitation.d.ts +5 -3
  20. package/dist/elicitation.js +3 -2
  21. package/dist/fleet/fleet-bus.js +92 -83
  22. package/dist/fleet-lease.d.ts +1 -1
  23. package/dist/fleet-lease.js +1 -1
  24. package/dist/hooks/hook-llm.d.ts +5 -4
  25. package/dist/hooks/hook-runner.d.ts +1 -1
  26. package/dist/hooks/hook-runner.js +19 -10
  27. package/dist/http/server.d.ts +95 -69
  28. package/dist/http/server.js +75 -86
  29. package/dist/index.d.ts +1 -1
  30. package/dist/key-resolver.d.ts +1 -1
  31. package/dist/key-resolver.js +1 -1
  32. package/dist/leader/grader-env-factory.d.ts +1 -1
  33. package/dist/leader/grader-env-factory.js +2 -2
  34. package/dist/leader/leader.js +5 -2
  35. package/dist/leader/wire.d.ts +1 -1
  36. package/dist/leader/wire.js +170 -166
  37. package/dist/main.js +487 -476
  38. package/dist/memory-scope.d.ts +18 -0
  39. package/dist/memory-scope.js +38 -0
  40. package/dist/observability/cost-taxonomy.d.ts +34 -0
  41. package/dist/observability/cost-taxonomy.js +26 -0
  42. package/dist/observability/prompt-manifest.d.ts +75 -0
  43. package/dist/observability/prompt-manifest.js +82 -0
  44. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  45. package/dist/plugins/checkpoint-store-sql.js +224 -0
  46. package/dist/plugins/image-bake-store-sql.d.ts +53 -0
  47. package/dist/plugins/image-bake-store-sql.js +463 -0
  48. package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
  49. package/dist/plugins/k8s-bg-scripts.js +129 -0
  50. package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
  51. package/dist/plugins/k8s-exec-protocol.js +87 -0
  52. package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
  53. package/dist/plugins/pg-checkpoint-store.js +1 -189
  54. package/dist/plugins/pg-cost-quota.js +3 -143
  55. package/dist/plugins/pg-image-bake.d.ts +1 -40
  56. package/dist/plugins/pg-image-bake.js +1 -420
  57. package/dist/plugins/pg-rate-limiter.d.ts +2 -32
  58. package/dist/plugins/pg-rate-limiter.js +4 -147
  59. package/dist/plugins/remote-env-k8s.d.ts +5 -38
  60. package/dist/plugins/remote-env-k8s.js +7 -213
  61. package/dist/plugins/sql-driver.d.ts +25 -0
  62. package/dist/plugins/sql-driver.js +59 -0
  63. package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
  64. package/dist/plugins/tidb-checkpoint-store.js +1 -191
  65. package/dist/plugins/tidb-image-bake.d.ts +1 -38
  66. package/dist/plugins/tidb-image-bake.js +1 -348
  67. package/dist/plugins/web-search.d.ts +4 -3
  68. package/dist/plugins/write-behind-counter.d.ts +14 -3
  69. package/dist/plugins/write-behind-counter.js +39 -12
  70. package/dist/principal-jwt.d.ts +44 -0
  71. package/dist/principal-jwt.js +95 -0
  72. package/dist/question.d.ts +2 -1
  73. package/dist/question.js +3 -2
  74. package/dist/run-local.js +2 -2
  75. package/dist/runtime-caps-resolver.d.ts +7 -7
  76. package/dist/runtime-caps-resolver.js +3 -3
  77. package/dist/security.d.ts +4 -74
  78. package/dist/security.js +6 -155
  79. package/dist/sema-registry.d.ts +5 -200
  80. package/dist/sema-registry.js +4 -567
  81. package/dist/tool-approval.d.ts +2 -1
  82. package/dist/tool-approval.js +3 -2
  83. package/package.json +1 -1
@@ -30,11 +30,17 @@ import { projectEvents, runSummary, mapTraceEvent, turnEndEventData, contextUsag
30
30
  import { createLedgerSink } from "../trace/ledger-sink.js";
31
31
  import { cacheFamilyOfMirror } from "../budget.js";
32
32
  import { projectArtifacts } from "../trace/artifacts.js";
33
- import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../finance/cost-taxonomy.js";
33
+ import { composeSupervisorCost, infraCost, infraUsageFromEvents, hasInfraPricing } from "../observability/cost-taxonomy.js";
34
34
  import { redactSecrets, redactDeep, redactedPreview } from "../trace/redact.js";
35
35
  import { usageSummary, usageSeries, usageBreakdown } from "../usage-analytics.js";
36
36
  import { IdempotencyCache } from "./idempotency.js";
37
37
  import { pgHasUnstorable } from "../plugins/pg-safe-json.js";
38
+ export function flattenServiceDeps(deps) {
39
+ const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
40
+ if (!(stores || coordinators || seams || observability || governance || deployment || knobs))
41
+ return deps;
42
+ return { ...stores, ...coordinators, ...seams, ...observability, ...governance, ...deployment, ...knobs, ...flat };
43
+ }
38
44
  export const MAX_USER_SKILLS = 10;
39
45
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
40
46
  export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
@@ -195,7 +201,8 @@ function bearerPresentedButUnverified(req, config) {
195
201
  return false;
196
202
  return true;
197
203
  }
198
- export function createHttpServer(deps) {
204
+ export function createHttpServer(rawDeps) {
205
+ const deps = flattenServiceDeps(rawDeps);
199
206
  const idemCache = new IdempotencyCache();
200
207
  let sseConnections = 0;
201
208
  let sseOwnerLookups = 0;
@@ -634,7 +641,7 @@ export function createHttpServer(deps) {
634
641
  }
635
642
  if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url)) {
636
643
  res.setHeader("retry-after", "15");
637
- sendJson(res, 503, { error: "draining", errorCode: "draining", message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" });
644
+ sendError(res, 503, "draining", "draining", { message: "this instance is draining for shutdown/upgrade — retry against the replacement instance" });
638
645
  return;
639
646
  }
640
647
  if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url)) {
@@ -655,7 +662,7 @@ export function createHttpServer(deps) {
655
662
  const name = url.slice("/v1/capabilities/scenarios/".length).split("?")[0];
656
663
  const detail = deps.scenarioDetails?.[name];
657
664
  if (!detail) {
658
- sendJson(res, 404, { error: "scenario not found", errorCode: "scenario_not_found" });
665
+ sendError(res, 404, "scenario_not_found", "scenario not found");
659
666
  return;
660
667
  }
661
668
  sendJson(res, 200, detail);
@@ -1718,7 +1725,7 @@ export function createHttpServer(deps) {
1718
1725
  }
1719
1726
  catch (e) {
1720
1727
  if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
1721
- sendJson(res, 422, { errorCode: "steering.invalid_content", error: e.message });
1728
+ sendError(res, 422, "steering.invalid_content", e.message);
1722
1729
  return;
1723
1730
  }
1724
1731
  throw e;
@@ -1747,7 +1754,7 @@ export function createHttpServer(deps) {
1747
1754
  return (await cs.setPendingSteer(token, scope, { text, trusted })) ? "parked" : "no-checkpoint";
1748
1755
  };
1749
1756
  const sendParked = () => sendJson(res, 202, { taskId, status: "suspended", delivery: "queued", messageId, ...(priority ? { priority } : {}), note: "steer parked on the checkpoint — injected when the run resumes" });
1750
- const sendNotRunning = (error) => sendJson(res, 409, { errorCode: "steering.not_running", error });
1757
+ const sendNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
1751
1758
  const live = steerableRuns.get(taskId);
1752
1759
  if (live) {
1753
1760
  try {
@@ -1758,7 +1765,7 @@ export function createHttpServer(deps) {
1758
1765
  catch (e) {
1759
1766
  const code = e.code;
1760
1767
  if (code === "steering.invalid_content") {
1761
- sendJson(res, 422, { errorCode: "steering.invalid_content", error: e instanceof Error ? e.message : "invalid steering content" });
1768
+ sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
1762
1769
  return;
1763
1770
  }
1764
1771
  if (code !== "steering.not_running")
@@ -1896,7 +1903,7 @@ export function createHttpServer(deps) {
1896
1903
  const compactInstructions = compactBody.instructions;
1897
1904
  const live = steerableRuns.get(taskId);
1898
1905
  if (!live) {
1899
- sendJson(res, 409, { errorCode: "compact.not_running", error: run.status === "running" ? "run is active on another replica — manual compact is replica-local" : `run is ${run.status} — not accepting compaction` });
1906
+ sendError(res, 409, "compact.not_running", run.status === "running" ? "run is active on another replica — manual compact is replica-local" : `run is ${run.status} — not accepting compaction`);
1900
1907
  return;
1901
1908
  }
1902
1909
  void live.compact(compactInstructions !== undefined ? { instructions: compactInstructions } : undefined).then((outcome) => {
@@ -1953,7 +1960,7 @@ export function createHttpServer(deps) {
1953
1960
  return;
1954
1961
  const live = steerableRuns.get(taskId);
1955
1962
  if (!live) {
1956
- sendJson(res, 409, { errorCode: "detach.not_running", error: run.status === "running" ? "run is active on another replica — detach is replica-local" : `run is ${run.status} — no running tool call to detach` });
1963
+ sendError(res, 409, "detach.not_running", run.status === "running" ? "run is active on another replica — detach is replica-local" : `run is ${run.status} — no running tool call to detach`);
1957
1964
  return;
1958
1965
  }
1959
1966
  live.detach(body.toolCallId);
@@ -1992,10 +1999,7 @@ export function createHttpServer(deps) {
1992
1999
  return;
1993
2000
  }
1994
2001
  if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
1995
- sendJson(res, 413, {
1996
- errorCode: "steer.content_too_large",
1997
- error: `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`,
1998
- });
2002
+ sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
1999
2003
  return;
2000
2004
  }
2001
2005
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -2007,11 +2011,11 @@ export function createHttpServer(deps) {
2007
2011
  }
2008
2012
  if (wfRun && !runSessionAcceptOk(req, res, { sessionId: wfRun.originatingSessionId ?? null }, "workflow.agent-steer", "workflow not found"))
2009
2013
  return;
2010
- const sendNotRunningWf = (error) => sendJson(res, 409, { errorCode: "steering.not_running", error });
2014
+ const sendNotRunningWf = (error) => sendError(res, 409, "steering.not_running", error);
2011
2015
  const redacted = redactSteerIn(body.content, label);
2012
2016
  const resolution = deps.workflowAgentRegistry?.resolve(wfRunId, label);
2013
2017
  if (resolution && resolution.count > 1) {
2014
- sendJson(res, 409, { errorCode: "steering.ambiguous_label", error: `${resolution.count} live agents share label '${label}' in this run — steer target is ambiguous` });
2018
+ sendError(res, 409, "steering.ambiguous_label", `${resolution.count} live agents share label '${label}' in this run — steer target is ambiguous`);
2015
2019
  return;
2016
2020
  }
2017
2021
  const handle = resolution?.handle;
@@ -2195,27 +2199,23 @@ export function createHttpServer(deps) {
2195
2199
  const stillParked = details?.error === "parked_pending_approval" || details?.status === "parked";
2196
2200
  if (stopVerb && details?.error !== undefined && (details.status === "running" || stillParked || parkResumeWon || parkArbiterUnreachable)) {
2197
2201
  const notLocal = details.error === "not_local";
2198
- sendJson(res, 409, {
2199
- errorCode: notLocal
2200
- ? "stop.not_local"
2201
- : parkArbiterUnreachable
2202
- ? "stop.park_arbiter_unreachable"
2203
- : parkResumeWon
2204
- ? "stop.park_resume_won"
2205
- : stillParked
2206
- ? "stop.parked"
2207
- : "stop.not_landed",
2208
- error: notLocal
2209
- ? "this task is not attached to this instance cannot stop it here (its durable record is still marked running; if its host process is gone, the retention sweep settles it as interrupted)"
2210
- : parkArbiterUnreachable
2211
- ? "the approval arbitration store was unreachable this task's row stays parked (not stopped); retry the stop"
2212
- : parkResumeWon
2213
- ? "the pending tool-approval was resolved (resumed) concurrently with this stop attemptthe resume won the race, so nothing was stopped; re-check the task's current status"
2214
- : stillParked
2215
- ? "this task is durably parked awaiting a tool-approval decision, not actively running — there is no live process to interrupt; resolve (or let expire) the pending approval instead"
2216
- : `stop did not land (${details.error}) — the process may still be running`,
2217
- taskId: runId, target, content: out.content, output: out.details,
2218
- });
2202
+ sendError(res, 409, notLocal
2203
+ ? "stop.not_local"
2204
+ : parkArbiterUnreachable
2205
+ ? "stop.park_arbiter_unreachable"
2206
+ : parkResumeWon
2207
+ ? "stop.park_resume_won"
2208
+ : stillParked
2209
+ ? "stop.parked"
2210
+ : "stop.not_landed", notLocal
2211
+ ? "this task is not attached to this instance — cannot stop it here (its durable record is still marked running; if its host process is gone, the retention sweep settles it as interrupted)"
2212
+ : parkArbiterUnreachable
2213
+ ? "the approval arbitration store was unreachable this task's row stays parked (not stopped); retry the stop"
2214
+ : parkResumeWon
2215
+ ? "the pending tool-approval was resolved (resumed) concurrently with this stop attempt the resume won the race, so nothing was stopped; re-check the task's current status"
2216
+ : stillParked
2217
+ ? "this task is durably parked awaiting a tool-approval decision, not actively running there is no live process to interrupt; resolve (or let expire) the pending approval instead"
2218
+ : `stop did not land (${details.error}) — the process may still be running`, { taskId: runId, target, content: out.content, output: out.details });
2219
2219
  return;
2220
2220
  }
2221
2221
  const g14 = (() => {
@@ -2264,10 +2264,7 @@ export function createHttpServer(deps) {
2264
2264
  return;
2265
2265
  }
2266
2266
  if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
2267
- sendJson(res, 413, {
2268
- errorCode: "steer.content_too_large",
2269
- error: `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`,
2270
- });
2267
+ sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
2271
2268
  return;
2272
2269
  }
2273
2270
  const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
@@ -2278,7 +2275,7 @@ export function createHttpServer(deps) {
2278
2275
  }
2279
2276
  if (!runSessionAcceptOk(req, res, run, `subagent.${verb}`))
2280
2277
  return;
2281
- const send409 = (errorCode, error) => sendJson(res, 409, { errorCode, error });
2278
+ const send409 = (errorCode, error) => sendError(res, 409, errorCode, error);
2282
2279
  const redacted = redactSteerIn(body.content, target);
2283
2280
  const resolution = deps.subagentSteerRegistry?.resolve(runId, target);
2284
2281
  if (resolution && resolution.count > 1) {
@@ -2964,13 +2961,13 @@ export function createHttpServer(deps) {
2964
2961
  return;
2965
2962
  }
2966
2963
  if (typeof body.reason === "string" && body.reason.length > MAX_APPROVAL_REASON_CHARS) {
2967
- sendJson(res, 413, { error: `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`, errorCode: "reason_too_large" });
2964
+ sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
2968
2965
  return;
2969
2966
  }
2970
2967
  const run = await deps.runStore.getRun(taskId);
2971
2968
  if (!run) {
2972
2969
  if (deps.config.directDoorActive) {
2973
- sendJson(res, 401, { error: "principal proof required", errorCode: "principal_unverified" });
2970
+ sendError(res, 401, "principal_unverified", "principal proof required");
2974
2971
  return;
2975
2972
  }
2976
2973
  sendJson(res, 404, { error: "task not found" });
@@ -2981,7 +2978,7 @@ export function createHttpServer(deps) {
2981
2978
  const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
2982
2979
  const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") }, { sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
2983
2980
  if (!proof.ok) {
2984
- sendJson(res, proof.status, { error: proof.error, errorCode: proof.errorCode });
2981
+ sendError(res, proof.status, proof.errorCode, proof.error);
2985
2982
  return;
2986
2983
  }
2987
2984
  deciderPrincipal = proof.principal;
@@ -3019,7 +3016,7 @@ export function createHttpServer(deps) {
3019
3016
  return;
3020
3017
  }
3021
3018
  if (typeof body.reason === "string" && body.reason.length > MAX_APPROVAL_REASON_CHARS) {
3022
- sendJson(res, 413, { error: `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`, errorCode: "reason_too_large" });
3019
+ sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
3023
3020
  return;
3024
3021
  }
3025
3022
  let remember = false;
@@ -3033,7 +3030,7 @@ export function createHttpServer(deps) {
3033
3030
  return;
3034
3031
  }
3035
3032
  if (deps.config.directDoorActive) {
3036
- sendJson(res, 400, { error: "remember is not supported on a direct-door worker (not covered by the decision proof)", errorCode: "remember_not_in_proof" });
3033
+ sendError(res, 400, "remember_not_in_proof", "remember is not supported on a direct-door worker (not covered by the decision proof)");
3037
3034
  return;
3038
3035
  }
3039
3036
  if (!deps.approvalExemptionStore) {
@@ -3041,7 +3038,7 @@ export function createHttpServer(deps) {
3041
3038
  return;
3042
3039
  }
3043
3040
  if (!body.checkpointToken && !body.boundCallId) {
3044
- sendJson(res, 400, { error: "remember requires the decision binding (echo checkpointToken and/or boundCallId from the pending approval)", errorCode: "remember_requires_binding" });
3041
+ sendError(res, 400, "remember_requires_binding", "remember requires the decision binding (echo checkpointToken and/or boundCallId from the pending approval)");
3045
3042
  return;
3046
3043
  }
3047
3044
  remember = true;
@@ -3061,10 +3058,7 @@ export function createHttpServer(deps) {
3061
3058
  }
3062
3059
  }
3063
3060
  if (deps.config.directDoorActive && body.updatedInput !== undefined) {
3064
- sendJson(res, 400, {
3065
- error: "updatedInput is not supported on a direct-door worker (not covered by the decision proof)",
3066
- errorCode: "updated_input_not_in_proof",
3067
- });
3061
+ sendError(res, 400, "updated_input_not_in_proof", "updatedInput is not supported on a direct-door worker (not covered by the decision proof)");
3068
3062
  return;
3069
3063
  }
3070
3064
  const binding = {
@@ -3081,7 +3075,7 @@ export function createHttpServer(deps) {
3081
3075
  };
3082
3076
  const proof = verifyDirectDoorProof({ jwt: hdr("x-approval-principal-token"), mac: hdr("x-approval-mac"), kid: hdr("x-approval-mac-kid") }, { sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
3083
3077
  if (!proof.ok) {
3084
- sendJson(res, proof.status, { error: proof.error, errorCode: proof.errorCode });
3078
+ sendError(res, proof.status, proof.errorCode, proof.error);
3085
3079
  return;
3086
3080
  }
3087
3081
  deciderPrincipal = proof.principal;
@@ -3133,7 +3127,7 @@ export function createHttpServer(deps) {
3133
3127
  if (deps.approvalStore && url.startsWith("/v1/approvals")) {
3134
3128
  const store = deps.approvalStore;
3135
3129
  if (deps.config.directApprovalDoor) {
3136
- sendJson(res, 503, { error: "approvals are served by the direct door on this worker", errorCode: "approval_door_unconfigured" });
3130
+ sendError(res, 503, "approval_door_unconfigured", "approvals are served by the direct door on this worker");
3137
3131
  return;
3138
3132
  }
3139
3133
  if (req.method === "GET" && url === "/v1/approvals") {
@@ -3291,11 +3285,11 @@ export function createHttpServer(deps) {
3291
3285
  scope = decodeURIComponent(url.slice("/v1/memory/sync/".length));
3292
3286
  }
3293
3287
  catch {
3294
- sendJson(res, 400, { error: "malformed percent-encoding in :scope", code: "memory_sync_invalid_scope", errorCode: "memory_sync_invalid_scope" });
3288
+ sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope", { code: "memory_sync_invalid_scope" });
3295
3289
  return;
3296
3290
  }
3297
3291
  if (scope.length === 0) {
3298
- sendJson(res, 400, { error: "missing :scope", code: "memory_sync_invalid_scope", errorCode: "memory_sync_invalid_scope" });
3292
+ sendError(res, 400, "memory_sync_invalid_scope", "missing :scope", { code: "memory_sync_invalid_scope" });
3299
3293
  return;
3300
3294
  }
3301
3295
  if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
@@ -3305,7 +3299,7 @@ export function createHttpServer(deps) {
3305
3299
  const body = await readJson(req);
3306
3300
  const parsed = parseMemorySyncRequest(body, scope);
3307
3301
  if (!parsed.ok) {
3308
- sendJson(res, 422, { error: parsed.error, code: "memory_sync_invalid_body", errorCode: "memory_sync_invalid_body" });
3302
+ sendError(res, 422, "memory_sync_invalid_body", parsed.error, { code: "memory_sync_invalid_body" });
3309
3303
  return;
3310
3304
  }
3311
3305
  try {
@@ -3931,7 +3925,7 @@ export function createHttpServer(deps) {
3931
3925
  }
3932
3926
  catch (e) {
3933
3927
  if (e instanceof SessionPolicyError) {
3934
- sendJson(res, e.code === "conflict" ? 409 : 403, { error: e.message, errorCode: e.code });
3928
+ sendError(res, e.code === "conflict" ? 409 : 403, e.code, e.message);
3935
3929
  return;
3936
3930
  }
3937
3931
  throw e;
@@ -4047,7 +4041,7 @@ export function createHttpServer(deps) {
4047
4041
  if (typeof fs.blobSizes === "function") {
4048
4042
  const known = (await fs.blobSizes([hash])).get(hash);
4049
4043
  if (known !== undefined && known > cap) {
4050
- sendJson(res, 413, { error: "file exceeds the single-file read limit — download it via the archive endpoint", code: "workspace_file_too_large", errorCode: "workspace_file_too_large", sizeBytes: known, limit: cap });
4044
+ sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { code: "workspace_file_too_large", sizeBytes: known, limit: cap });
4051
4045
  return;
4052
4046
  }
4053
4047
  }
@@ -4057,7 +4051,7 @@ export function createHttpServer(deps) {
4057
4051
  return;
4058
4052
  }
4059
4053
  if (bytes.byteLength > cap) {
4060
- sendJson(res, 413, { error: "file exceeds the single-file read limit — download it via the archive endpoint", code: "workspace_file_too_large", errorCode: "workspace_file_too_large", sizeBytes: bytes.byteLength, limit: cap });
4054
+ sendError(res, 413, "workspace_file_too_large", "file exceeds the single-file read limit — download it via the archive endpoint", { code: "workspace_file_too_large", sizeBytes: bytes.byteLength, limit: cap });
4061
4055
  return;
4062
4056
  }
4063
4057
  const ct = workspaceContentType(pathParam);
@@ -4260,11 +4254,7 @@ export function createHttpServer(deps) {
4260
4254
  }
4261
4255
  const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
4262
4256
  if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
4263
- sendJson(res, 413, {
4264
- code: "blob_too_large_for_sql",
4265
- errorCode: "blob_too_large_for_sql",
4266
- error: `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`,
4267
- });
4257
+ sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`, { code: "blob_too_large_for_sql" });
4268
4258
  return;
4269
4259
  }
4270
4260
  if (createHash("sha256").update(body).digest("hex") !== hash) {
@@ -4273,7 +4263,7 @@ export function createHttpServer(deps) {
4273
4263
  }
4274
4264
  const put = await fs.putBlob(hash, new Uint8Array(body));
4275
4265
  if (!put.ok) {
4276
- sendJson(res, 502, { error: "blob store write failed", code: put.error.code, errorCode: put.error.code });
4266
+ sendError(res, 502, put.error.code, "blob store write failed", { code: put.error.code });
4277
4267
  return;
4278
4268
  }
4279
4269
  res.writeHead(204).end();
@@ -4325,7 +4315,7 @@ export function createHttpServer(deps) {
4325
4315
  const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
4326
4316
  const relA = classifySyncRelationshipByIds(entryIds, dstIds);
4327
4317
  if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
4328
- sendJson(res, 409, { error: "sync conflict", errorCode: "conflict", relation: relA });
4318
+ sendError(res, 409, "conflict", "sync conflict", { relation: relA });
4329
4319
  return;
4330
4320
  }
4331
4321
  let payloadVerified = false;
@@ -4349,7 +4339,7 @@ export function createHttpServer(deps) {
4349
4339
  for (const snap of snapshots) {
4350
4340
  const r = await fss.importManifest(sessionId, snap.key, new Map(snap.manifest), getBlobA);
4351
4341
  if (!r.ok) {
4352
- sendJson(res, 422, { error: `snapshot import failed (${r.error.code})`, errorCode: r.error.code === "restore_failed" ? "restore_failed" : "missing_blob" });
4342
+ sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
4353
4343
  return;
4354
4344
  }
4355
4345
  }
@@ -4371,7 +4361,7 @@ export function createHttpServer(deps) {
4371
4361
  }
4372
4362
  const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
4373
4363
  if (activeTaskId) {
4374
- sendJson(res, 409, { error: "session has an active run; sync after it settles", errorCode: "session_active", activeTaskId });
4364
+ sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
4375
4365
  return;
4376
4366
  }
4377
4367
  const heldLease = importLeases.get(sessionId);
@@ -4387,12 +4377,8 @@ export function createHttpServer(deps) {
4387
4377
  }
4388
4378
  else {
4389
4379
  const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - ageMs) / 1000)) : undefined;
4390
- sendJson(res, 409, {
4391
- error: "an import is already in flight for this session"
4392
- + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""),
4393
- errorCode: "import_in_flight",
4394
- ...(retryAfterSec !== undefined ? { retryAfterSec } : {}),
4395
- });
4380
+ sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
4381
+ + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
4396
4382
  return;
4397
4383
  }
4398
4384
  }
@@ -4408,14 +4394,14 @@ export function createHttpServer(deps) {
4408
4394
  const present = await fss.hasBlobs(all);
4409
4395
  const missing = all.find((h) => !present.has(h));
4410
4396
  if (missing !== undefined) {
4411
- sendJson(res, 422, { error: "missing blob; PUT all referenced blobs before import", errorCode: "missing_blob" });
4397
+ sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
4412
4398
  return;
4413
4399
  }
4414
4400
  }
4415
4401
  else {
4416
4402
  for (const hash of all) {
4417
4403
  if (!(await deps.fileSnapshotStore.getBlob(hash))) {
4418
- sendJson(res, 422, { error: "missing blob; PUT all referenced blobs before import", errorCode: "missing_blob" });
4404
+ sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
4419
4405
  return;
4420
4406
  }
4421
4407
  }
@@ -4521,13 +4507,13 @@ export function createHttpServer(deps) {
4521
4507
  if (e instanceof PrecheckError) {
4522
4508
  await pending.handle.abort().catch(() => undefined);
4523
4509
  cleanupStaging(stagingId);
4524
- sendJson(res, e.status, { error: e.message, errorCode: "invalid_entries" });
4510
+ sendError(res, e.status, "invalid_entries", e.message);
4525
4511
  return;
4526
4512
  }
4527
4513
  if (e instanceof SessionError && e.code === "invalid_session") {
4528
4514
  await pending.handle.abort().catch(() => undefined);
4529
4515
  cleanupStaging(stagingId);
4530
- sendJson(res, 422, { error: "staged session log failed validation", errorCode: "invalid_entries" });
4516
+ sendError(res, 422, "invalid_entries", "staged session log failed validation");
4531
4517
  return;
4532
4518
  }
4533
4519
  await pending.handle.abort().catch(() => undefined);
@@ -4538,7 +4524,7 @@ export function createHttpServer(deps) {
4538
4524
  if (activeAtCommit) {
4539
4525
  await pending.handle.abort().catch(() => undefined);
4540
4526
  cleanupStaging(stagingId);
4541
- sendJson(res, 409, { error: "session started an active run while the import streamed; sync after it settles", errorCode: "session_active", activeTaskId: activeAtCommit });
4527
+ sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
4542
4528
  return;
4543
4529
  }
4544
4530
  try {
@@ -4548,7 +4534,7 @@ export function createHttpServer(deps) {
4548
4534
  for (const snap of pending.snapshots) {
4549
4535
  const r = await importManifest(sessionId, snap.key, new Map(snap.manifest), getBlob);
4550
4536
  if (!r.ok) {
4551
- sendJson(res, 422, { error: `snapshot import failed (${r.error.code})`, errorCode: r.error.code === "restore_failed" ? "restore_failed" : "missing_blob" });
4537
+ sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
4552
4538
  cleanupStaging(stagingId);
4553
4539
  return;
4554
4540
  }
@@ -4570,12 +4556,12 @@ export function createHttpServer(deps) {
4570
4556
  if (e instanceof SyncConflictError) {
4571
4557
  await pending.handle.abort().catch(() => undefined);
4572
4558
  cleanupStaging(stagingId);
4573
- sendJson(res, 409, { error: "sync conflict", errorCode: "conflict", relation: e.relation });
4559
+ sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
4574
4560
  return;
4575
4561
  }
4576
4562
  if (e instanceof SessionPolicyError) {
4577
4563
  cleanupStaging(stagingId);
4578
- sendJson(res, e.code === "loosen_forbidden" ? 403 : 409, { error: e.message, errorCode: e.code });
4564
+ sendError(res, e.code === "loosen_forbidden" ? 403 : 409, e.code, e.message);
4579
4565
  return;
4580
4566
  }
4581
4567
  cleanupStaging(stagingId);
@@ -4640,7 +4626,7 @@ export function createHttpServer(deps) {
4640
4626
  }
4641
4627
  const allow = deps.config.attachmentMimeAllowlist;
4642
4628
  if (allow && !allow.some((a) => a === mime || (a.endsWith("/*") && mime.startsWith(a.slice(0, -1))))) {
4643
- sendJson(res, 415, { code: "attachment.mime_not_allowed", errorCode: "attachment.mime_not_allowed", error: `mime '${mime}' is not in this deployment's allowlist`, allowed: allow });
4629
+ sendError(res, 415, "attachment.mime_not_allowed", `mime '${mime}' is not in this deployment's allowlist`, { code: "attachment.mime_not_allowed", allowed: allow });
4644
4630
  return;
4645
4631
  }
4646
4632
  const max = deps.config.attachmentMaxBytes ?? 32 * 1024 * 1024;
@@ -4650,7 +4636,7 @@ export function createHttpServer(deps) {
4650
4636
  }
4651
4637
  catch (err) {
4652
4638
  if (err instanceof HttpError && err.status === 413) {
4653
- sendJson(res, 413, { code: "attachment.too_large", errorCode: "attachment.too_large", error: `attachment exceeds the per-file limit (${max} bytes)`, maxBytes: max });
4639
+ sendError(res, 413, "attachment.too_large", `attachment exceeds the per-file limit (${max} bytes)`, { code: "attachment.too_large", maxBytes: max });
4654
4640
  return;
4655
4641
  }
4656
4642
  throw err;
@@ -4772,7 +4758,7 @@ export function createHttpServer(deps) {
4772
4758
  catch (e) {
4773
4759
  const code = e.code;
4774
4760
  if (typeof code === "string" && code.startsWith("notify.")) {
4775
- sendJson(res, 400, { errorCode: code, error: e instanceof Error ? e.message : "invalid notification" });
4761
+ sendError(res, 400, code, e instanceof Error ? e.message : "invalid notification");
4776
4762
  return;
4777
4763
  }
4778
4764
  }
@@ -5902,7 +5888,7 @@ export function createHttpServer(deps) {
5902
5888
  return false;
5903
5889
  deps.metrics?.inc("fleet_lease_rejected_total");
5904
5890
  res.setHeader("retry-after", String(adm.retryAfterSec));
5905
- sendJson(res, 429, { error: "budget exhausted (quota lease)", code: "quota_exhausted", errorCode: "quota_exhausted", retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
5891
+ sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { code: "quota_exhausted", retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
5906
5892
  return true;
5907
5893
  }
5908
5894
  function rateLimited(req, res) {
@@ -6879,6 +6865,9 @@ function sendJson(res, status, body) {
6879
6865
  res.writeHead(status, { "content-type": "application/json" });
6880
6866
  res.end(JSON.stringify(body));
6881
6867
  }
6868
+ function sendError(res, status, errorCode, message, extra) {
6869
+ sendJson(res, status, { error: message, errorCode, ...extra });
6870
+ }
6882
6871
  function msg(e) {
6883
6872
  return e instanceof Error ? e.message : String(e);
6884
6873
  }
package/dist/index.d.ts CHANGED
@@ -22,5 +22,5 @@ export { createCouncilTool } from "./capabilities/code-review-council.js";
22
22
  export { loadSkills, skillsForScenario, type LoadedSkill } from "./capabilities/skills.js";
23
23
  export { buildScenarios, selectScenario, type Scenario, type ScenarioBundle, type ScenarioDeps, type ScenarioRequest, } from "./capabilities/scenarios.js";
24
24
  export { createAuthorizer, principalFrom, memoryScopeFor, memoryEngineBackendFor, HttpError, type AuthContext, type OwnerAwareSessionStore, } from "./security.js";
25
- export { createHttpServer, type ServiceDeps, type TaskRequestBody, type RequestAuth, } from "./http/server.js";
25
+ export { createHttpServer, type ServiceDeps, type TaskRequestBody, type RequestAuth, type FlatServiceDeps, type ServiceDepGroups, type ServiceCoreDeps, type ServiceStoreDeps, type ServiceCoordinatorDeps, type ServiceSeamDeps, type ServiceObservabilityDeps, type ServiceGovernanceDeps, type ServiceDeploymentDeps, type ServiceKnobDeps, } from "./http/server.js";
26
26
  //# sourceMappingURL=index.d.ts.map
@@ -1,6 +1,6 @@
1
1
  import type { Model } from "@sema-agent/core";
2
2
  import { type SealedKeyPoison } from "./sealed-key.js";
3
- export declare function buildKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: Model) => Promise<{
3
+ export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: Model) => Promise<{
4
4
  apiKey: string;
5
5
  } | undefined>) | undefined;
6
6
  //# sourceMappingURL=key-resolver.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { isSealedKeyPoison, SealedKeyPoisonedError } from "./sealed-key.js";
2
- export function buildKeyResolver(modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
2
+ export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
3
3
  if (Object.keys(modelApiKeyEnv).length === 0 && Object.keys(modelApiKeys).length === 0)
4
4
  return undefined;
5
5
  return async (model) => {
@@ -32,5 +32,5 @@ export interface MintedGrader extends GraderHandle {
32
32
  immutableOraclePaths: string[];
33
33
  destroy: () => Promise<void>;
34
34
  }
35
- export declare function buildGraderEnvFactory(_cfg: GraderEnvFactoryCfg): () => Promise<MintedGrader>;
35
+ export declare function createGraderEnvFactory(_cfg: GraderEnvFactoryCfg): () => Promise<MintedGrader>;
36
36
  //# sourceMappingURL=grader-env-factory.d.ts.map
@@ -63,9 +63,9 @@ export function changedFilesOutOfAllowlist(changedFiles, allowlist) {
63
63
  export function changedFilesWithinAllowlist(changedFiles, allowlist) {
64
64
  return changedFilesOutOfAllowlist(changedFiles, allowlist).length === 0;
65
65
  }
66
- export function buildGraderEnvFactory(_cfg) {
66
+ export function createGraderEnvFactory(_cfg) {
67
67
  return async () => {
68
- throw new Error("buildGraderEnvFactory: the standalone control-plane grader is the auto-accept slice (LEADER_GRADER_FACTORY) — " +
68
+ throw new Error("createGraderEnvFactory: the standalone control-plane grader is the auto-accept slice (LEADER_GRADER_FACTORY) — " +
69
69
  "NOT wired in v1 (which reuses the integration sandbox for R1 and re-seeds it for R2). " +
70
70
  "See docs/LEADER-REPAIRLOOP-INTEGRATION.md §3.2/§7.");
71
71
  };
@@ -161,7 +161,9 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
161
161
  }));
162
162
  return out;
163
163
  };
164
- if (deps.moduleGate) {
164
+ const runModuleGate = async () => {
165
+ if (!deps.moduleGate)
166
+ return;
165
167
  const subById = new Map(subs.map((s) => [s.workerId, s]));
166
168
  const maxR = deps.moduleGate.maxRepairs ?? 1;
167
169
  await Promise.all(reports.filter((r) => r.status === "completed").map(async (r) => {
@@ -194,7 +196,8 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
194
196
  }
195
197
  }
196
198
  }));
197
- }
199
+ };
200
+ await runModuleGate();
198
201
  const mergeDeps = {
199
202
  pullWorkerDiff: async (r) => workerDiff(byId.get(r.workerId)),
200
203
  provisionIntegrationSandbox: deps.provisionIntegrationSandbox,
@@ -61,6 +61,6 @@ export declare function leaderResourceConfig(env?: NodeJS.ProcessEnv): {
61
61
  maxSuspends: number;
62
62
  };
63
63
  };
64
- export declare function buildLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
64
+ export declare function createLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
65
65
  export {};
66
66
  //# sourceMappingURL=wire.d.ts.map