@sema-agent/server 1.314.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 (52) hide show
  1. package/dist/approval-hmac.d.ts +17 -0
  2. package/dist/approval-hmac.js +27 -0
  3. package/dist/config-center/apply-effective.d.ts +22 -0
  4. package/dist/config-center/apply-effective.js +283 -0
  5. package/dist/config-center/http-client.d.ts +23 -0
  6. package/dist/config-center/http-client.js +109 -0
  7. package/dist/config-center/restart-signal.d.ts +12 -0
  8. package/dist/config-center/restart-signal.js +70 -0
  9. package/dist/config-center/skills-mcp.d.ts +13 -0
  10. package/dist/config-center/skills-mcp.js +113 -0
  11. package/dist/config-center/types.d.ts +143 -0
  12. package/dist/config-center/types.js +2 -0
  13. package/dist/fleet/fleet-bus.js +92 -83
  14. package/dist/hooks/hook-runner.js +18 -9
  15. package/dist/http/server.d.ts +95 -69
  16. package/dist/http/server.js +8 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/leader/leader.js +5 -2
  19. package/dist/leader/wire.js +169 -165
  20. package/dist/main.js +462 -451
  21. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  22. package/dist/plugins/checkpoint-store-sql.js +224 -0
  23. package/dist/plugins/image-bake-store-sql.d.ts +53 -0
  24. package/dist/plugins/image-bake-store-sql.js +463 -0
  25. package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
  26. package/dist/plugins/k8s-bg-scripts.js +129 -0
  27. package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
  28. package/dist/plugins/k8s-exec-protocol.js +87 -0
  29. package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
  30. package/dist/plugins/pg-checkpoint-store.js +1 -189
  31. package/dist/plugins/pg-cost-quota.js +3 -143
  32. package/dist/plugins/pg-image-bake.d.ts +1 -40
  33. package/dist/plugins/pg-image-bake.js +1 -420
  34. package/dist/plugins/pg-rate-limiter.d.ts +2 -32
  35. package/dist/plugins/pg-rate-limiter.js +4 -147
  36. package/dist/plugins/remote-env-k8s.d.ts +5 -38
  37. package/dist/plugins/remote-env-k8s.js +7 -213
  38. package/dist/plugins/sql-driver.d.ts +25 -0
  39. package/dist/plugins/sql-driver.js +59 -0
  40. package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
  41. package/dist/plugins/tidb-checkpoint-store.js +1 -191
  42. package/dist/plugins/tidb-image-bake.d.ts +1 -38
  43. package/dist/plugins/tidb-image-bake.js +1 -348
  44. package/dist/plugins/write-behind-counter.d.ts +14 -3
  45. package/dist/plugins/write-behind-counter.js +39 -12
  46. package/dist/principal-jwt.d.ts +44 -0
  47. package/dist/principal-jwt.js +95 -0
  48. package/dist/security.d.ts +2 -58
  49. package/dist/security.js +3 -118
  50. package/dist/sema-registry.d.ts +5 -200
  51. package/dist/sema-registry.js +4 -567
  52. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -58,7 +58,7 @@ import { createRegistryJwtVerifier } from "./auth-bridge.js";
58
58
  import { createMetrics } from "./observability/metrics.js";
59
59
  import { setRedactionObserver, redactSecrets } from "./trace/redact.js";
60
60
  import { RateLimiter } from "./observability/rate-limit.js";
61
- import { createHttpServer, explicitOperator } from "./http/server.js";
61
+ import { createHttpServer, explicitOperator, } from "./http/server.js";
62
62
  import { exportMemoryScope } from "./memory-export.js";
63
63
  import { performMemorySync } from "./memory-sync.js";
64
64
  import { createMemorySyncRunner, createMemorySyncTransport } from "./memory-sync-client.js";
@@ -1834,73 +1834,77 @@ async function main() {
1834
1834
  if (registryJwtVerifier)
1835
1835
  logger.info("auth_bridge_enabled", { issuer: authBridgeIssuer });
1836
1836
  const hookWakeBus = {};
1837
- const server = createHttpServer({
1838
- runner,
1839
- config,
1840
- authorize,
1841
- sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
1842
- taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined,
1843
- snapshotBlobSqlCapBytes: backend && backend.kind !== "local" && !config.snapshotBlobStore
1844
- ? (config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
1845
- : undefined,
1846
- modelReady: () => modelReadyState.ready,
1847
- scenarioDetails,
1848
- registryJwtVerifier: registryJwtVerifier ? registryJwtVerifier : undefined,
1849
- hookWakeBus,
1837
+ const stores = {
1850
1838
  runStore,
1851
- fleetBus,
1852
- workflowsCapable,
1853
1839
  resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
1840
+ approvalStore,
1854
1841
  approvalExemptionStore: approvalExemptionStore ? approvalExemptionStore : undefined,
1855
- sessionTitler: sessionTitler ? sessionTitler : undefined,
1842
+ checkpointStore,
1856
1843
  sessionPolicyStore: sessionPolicyStore ? sessionPolicyStore : undefined,
1857
1844
  fileSnapshotStore: fileSnapshotStore ? fileSnapshotStore : undefined,
1858
- backend: backend ? backend : undefined,
1859
- sessionMirrorRuling: principalCaps
1860
- ? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
1861
- : undefined,
1862
- approvalStore,
1863
- checkpointStore,
1845
+ taskAttachmentStore: taskAttachmentStore ? taskAttachmentStore : undefined,
1864
1846
  backgroundAgentStore: backgroundAgentStore ? backgroundAgentStore : undefined,
1865
- parkedReviveTool: parkedReviveTool ? parkedReviveTool : undefined,
1866
- parkedReviveInheritedGate: parkedReviveInheritedGate ? parkedReviveInheritedGate : undefined,
1867
- imageIndex,
1868
- imageBakes,
1869
- leaderEndpoint,
1847
+ sessionStorage: ownerAware,
1870
1848
  workflowRunStore: workflowRunStore ? workflowRunStore : undefined,
1871
1849
  workflowJournalStore: workflowJournalStore ? workflowJournalStore : undefined,
1850
+ imageIndex,
1851
+ imageBakes,
1852
+ outcomeSink: outcomeSink ? outcomeSink : undefined,
1853
+ sendFileLedger: sendFileLedger ? sendFileLedger : undefined,
1854
+ backend: backend ? backend : undefined,
1855
+ };
1856
+ const coordinators = {
1857
+ elicitation: elicitation ? elicitation : undefined,
1858
+ question: question ? question : undefined,
1859
+ toolApproval: toolApproval ? toolApproval : undefined,
1872
1860
  workflowAgentRegistry: workflowAgentRegistry ? workflowAgentRegistry : undefined,
1873
1861
  subagentSteerRegistry,
1874
- subagentTaskOutput: (handle, access) => backgroundAgentOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
1875
- taskHandleOutput: (handle, access) => taskHandleOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
1876
- taskHandleStop: (handle, access) => taskHandleStop(defaultTaskRegistry, handle, access, backgroundAgentStore),
1877
1862
  workflowCompletionInbox: workflowCompletionInbox ? workflowCompletionInbox : undefined,
1878
- sessionAudit,
1879
- sessionStorage: ownerAware,
1863
+ fleetBus,
1880
1864
  sessionWatch: sessionWatchRegistry ? sessionWatchRegistry : undefined,
1881
- sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
1865
+ hookWakeBus,
1866
+ sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined,
1867
+ leaderEndpoint,
1868
+ sessionTitler: sessionTitler ? sessionTitler : undefined,
1869
+ };
1870
+ const seams = {
1871
+ sessionAudit,
1882
1872
  purgeSession: purgeSession ? purgeSession : undefined,
1873
+ subagentTaskOutput: (handle, access) => backgroundAgentOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
1874
+ taskHandleOutput: (handle, access) => taskHandleOutput(defaultTaskRegistry, handle, access, backgroundAgentStore),
1875
+ taskHandleStop: (handle, access) => taskHandleStop(defaultTaskRegistry, handle, access, backgroundAgentStore),
1876
+ memoryExport: memoryExportBackend ? (scope) => exportMemoryScope(memoryExportBackend, scope) : undefined,
1877
+ memorySync: memoryExportBackend && memorySyncCursors
1878
+ ? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
1879
+ : undefined,
1880
+ sessionMirrorRuling: principalCaps
1881
+ ? async (p) => (await principalCaps.executionRuling(p))?.sessionMirror
1882
+ : undefined,
1883
1883
  instrumentDegenerate,
1884
- planCacheProbe,
1885
- modelUsage: modelUsageTracker,
1886
- promptManifests: promptManifestTracker,
1887
- elicitation: elicitation ? elicitation : undefined,
1888
- question: question ? question : undefined,
1889
- toolApproval: toolApproval ? toolApproval : undefined,
1890
- sendUserFile: sendUserFileEmitter ? sendUserFileEmitter : undefined,
1891
- sendFileLedger: sendFileLedger ? sendFileLedger : undefined,
1892
- instanceId,
1884
+ sideQueryAccounting,
1885
+ parkedReviveTool: parkedReviveTool ? parkedReviveTool : undefined,
1886
+ parkedReviveInheritedGate: parkedReviveInheritedGate ? parkedReviveInheritedGate : undefined,
1887
+ };
1888
+ const observability = {
1893
1889
  logger,
1894
1890
  metrics,
1891
+ modelUsage: modelUsageTracker,
1892
+ promptManifests: promptManifestTracker,
1893
+ planCacheProbe,
1894
+ };
1895
+ const governance = {
1896
+ authorize,
1897
+ registryJwtVerifier: registryJwtVerifier ? registryJwtVerifier : undefined,
1895
1898
  rateLimiter,
1896
1899
  costQuota,
1897
1900
  fleetLease: fleetLease ? fleetLease : undefined,
1898
- sideQueryAccounting,
1899
- outcomeSink: outcomeSink ? outcomeSink : undefined,
1900
- memoryExport: memoryExportBackend ? (scope) => exportMemoryScope(memoryExportBackend, scope) : undefined,
1901
- memorySync: memoryExportBackend && memorySyncCursors
1902
- ? (scope, syncReq) => performMemorySync(memoryExportBackend, memorySyncCursors, scope, syncReq)
1903
- : undefined,
1901
+ };
1902
+ const deployment = {
1903
+ sessionStoreLabel: config.sessionBackend === "tidb" && backend ? `durable(${backend.kind})` : config.sessionBackend,
1904
+ modelReady: () => modelReadyState.ready,
1905
+ scenarioDetails,
1906
+ workflowsCapable,
1907
+ instanceId,
1904
1908
  capabilities: {
1905
1909
  version: serviceVersion(),
1906
1910
  scenarios: Object.keys(scenarios),
@@ -1910,431 +1914,438 @@ async function main() {
1910
1914
  planeDeferredState: () => planeDeferredNoHandoff,
1911
1915
  drainState,
1912
1916
  storeDegraded: storeBackendDegraded,
1913
- resolveSpec: async (body, _req, auth, opts) => {
1914
- const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario);
1915
- gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process");
1916
- const scenarioName = scenarios[requested] ? requested : "default";
1917
- if (scenarioName === "autonomous")
1918
- logger.warn("scenario_autonomous_deprecated", { alias: "code", note: "renamed by [891]; alias keeps finalVerification pinned — pass scenario:'code' (+ explicit finalVerification if wanted)" });
1919
- const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal);
1920
- const centerDecls = centerPrompts?.declarations;
1921
- const appendLessPack = centerDecls
1922
- ? centerIdentityAssembled(centerDecls, scenarioName, hasConstitutionAnchors)
1923
- : providerDropsAppend(cap.promptProvider, typeof body.systemPrompt === "string" ? body.systemPrompt : undefined);
1924
- const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
1925
- const objective = typeof body.objective === "string" ? body.objective : "";
1926
- const parsedSettings = parseTaskSettings(body.settings);
1927
- if (opts?.leg === "fresh" && appendLessPack) {
1928
- const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
1929
- if (riderPresent || parsedSettings.settings?.outputStyle) {
1930
- throw new HttpError(400, `${riderPresent ? "appendSystemPrompt" : "settings.outputStyle"} is not supported when the effective prompt is already assembled (constitution anchors in systemPrompt, or a center assembled-identity pack for this scenario): the assembler's pass-through path cannot mount the append slot — fold it into the assembled prompt instead`);
1931
- }
1932
- }
1933
- if (parsedSettings.deferred.length > 0) {
1934
- logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
1917
+ };
1918
+ const knobs = {
1919
+ snapshotBlobSqlCapBytes: backend && backend.kind !== "local" && !config.snapshotBlobStore
1920
+ ? (config.snapshotBlobSqlMaxBytes ?? (backend.kind === "mysql" ? SQL_BLOB_DEFAULT_MAX_BYTES : undefined))
1921
+ : undefined,
1922
+ sessionEventsMaxConnections: sessionWatchRegistry ? posIntEnv(process.env.SESSION_EVENTS_MAX_CONNS, 256, 100_000) : undefined,
1923
+ };
1924
+ const resolveSpec = async (body, _req, auth, opts) => {
1925
+ const requested = gateScenarioRequest(await principalCaps?.scenarioRuling(auth?.principal), body.scenario, config.defaultScenario);
1926
+ gateExecutionLane(await principalCaps?.executionRuling(auth?.principal), config.remoteExec?.provider ?? "in-process");
1927
+ const scenarioName = scenarios[requested] ? requested : "default";
1928
+ if (scenarioName === "autonomous")
1929
+ logger.warn("scenario_autonomous_deprecated", { alias: "code", note: "renamed by [891]; alias keeps finalVerification pinned — pass scenario:'code' (+ explicit finalVerification if wanted)" });
1930
+ const cap = selectScenario(scenarios, scenarioName)(body, auth?.principal);
1931
+ const centerDecls = centerPrompts?.declarations;
1932
+ const appendLessPack = centerDecls
1933
+ ? centerIdentityAssembled(centerDecls, scenarioName, hasConstitutionAnchors)
1934
+ : providerDropsAppend(cap.promptProvider, typeof body.systemPrompt === "string" ? body.systemPrompt : undefined);
1935
+ const acceptedAppend = acceptAppendSystemPrompt(body.appendSystemPrompt, (detail) => logger.warn("append_system_prompt_dropped", { detail, sessionId: auth?.sessionId ?? null }), appendLessPack);
1936
+ const objective = typeof body.objective === "string" ? body.objective : "";
1937
+ const parsedSettings = parseTaskSettings(body.settings);
1938
+ if (opts?.leg === "fresh" && appendLessPack) {
1939
+ const riderPresent = typeof body.appendSystemPrompt === "string" && body.appendSystemPrompt.length > 0;
1940
+ if (riderPresent || parsedSettings.settings?.outputStyle) {
1941
+ throw new HttpError(400, `${riderPresent ? "appendSystemPrompt" : "settings.outputStyle"} is not supported when the effective prompt is already assembled (constitution anchors in systemPrompt, or a center assembled-identity pack for this scenario): the assembler's pass-through path cannot mount the append slot — fold it into the assembled prompt instead`);
1935
1942
  }
1936
- let attachmentNotice;
1937
- {
1938
- const reqAtt = body.attachmentIds;
1939
- if (reqAtt !== undefined) {
1940
- if (!Array.isArray(reqAtt) || !reqAtt.every((x) => typeof x === "string" && x.length > 0 && x.length <= 64)) {
1941
- throw new HttpError(400, "`attachmentIds` must be an array of attachment ids (strings)");
1942
- }
1943
- if (reqAtt.length > 16)
1944
- throw new HttpError(400, "`attachmentIds` exceeds the per-task limit (16)");
1945
- if (reqAtt.length > 0) {
1946
- if (!taskAttachmentStore)
1947
- throw new HttpError(501, "attachmentIds require a store backend (DB_BACKEND=tidb|pg|local)");
1948
- if (!auth?.sessionId)
1949
- throw new HttpError(400, "`attachmentIds` requires a session-resolving deployment (no authorizer session)");
1950
- const sid = auth.sessionId;
1951
- const r = await bindAttachmentsForTask({
1952
- store: taskAttachmentStore,
1953
- owner: auth?.principal ?? "default",
1954
- ids: reqAtt,
1955
- sessionId: sid,
1956
- leg: opts?.leg === "fresh" ? "fresh" : "resume",
1957
- nowMs: Date.now(),
1958
- onMissing: (id) => {
1959
- if (opts?.leg === "fresh")
1960
- throw new HttpError(400, `attachment not found: ${id}`);
1961
- logger.warn("attachment_missing_on_resume", { sessionId: sid, id });
1962
- },
1963
- });
1964
- attachmentNotice = r.notice;
1965
- }
1943
+ }
1944
+ if (parsedSettings.deferred.length > 0) {
1945
+ logger.warn("task_settings_deferred", { fields: parsedSettings.deferred, sessionId: auth?.sessionId ?? null });
1946
+ }
1947
+ let attachmentNotice;
1948
+ {
1949
+ const reqAtt = body.attachmentIds;
1950
+ if (reqAtt !== undefined) {
1951
+ if (!Array.isArray(reqAtt) || !reqAtt.every((x) => typeof x === "string" && x.length > 0 && x.length <= 64)) {
1952
+ throw new HttpError(400, "`attachmentIds` must be an array of attachment ids (strings)");
1953
+ }
1954
+ if (reqAtt.length > 16)
1955
+ throw new HttpError(400, "`attachmentIds` exceeds the per-task limit (16)");
1956
+ if (reqAtt.length > 0) {
1957
+ if (!taskAttachmentStore)
1958
+ throw new HttpError(501, "attachmentIds require a store backend (DB_BACKEND=tidb|pg|local)");
1959
+ if (!auth?.sessionId)
1960
+ throw new HttpError(400, "`attachmentIds` requires a session-resolving deployment (no authorizer session)");
1961
+ const sid = auth.sessionId;
1962
+ const r = await bindAttachmentsForTask({
1963
+ store: taskAttachmentStore,
1964
+ owner: auth?.principal ?? "default",
1965
+ ids: reqAtt,
1966
+ sessionId: sid,
1967
+ leg: opts?.leg === "fresh" ? "fresh" : "resume",
1968
+ nowMs: Date.now(),
1969
+ onMissing: (id) => {
1970
+ if (opts?.leg === "fresh")
1971
+ throw new HttpError(400, `attachment not found: ${id}`);
1972
+ logger.warn("attachment_missing_on_resume", { sessionId: sid, id });
1973
+ },
1974
+ });
1975
+ attachmentNotice = r.notice;
1966
1976
  }
1967
1977
  }
1968
- const execLane = config.remoteExec?.provider ?? "in-process";
1969
- if (typeof body.cwd === "string" && body.cwd.length > 0 && auth?.sessionId) {
1970
- if (cwdHonored(config) && isValidCwd(body.cwd))
1971
- setSessionCwd(auth.sessionId, body.cwd);
1972
- else if (inProcessSingleUserLane(config) && isValidCwd(body.cwd) && satisfiedByProcessCwd(body.cwd, realpathSync))
1973
- logger.debug("task_cwd_inherited", { lane: execLane, sessionId: auth.sessionId });
1978
+ }
1979
+ const execLane = config.remoteExec?.provider ?? "in-process";
1980
+ if (typeof body.cwd === "string" && body.cwd.length > 0 && auth?.sessionId) {
1981
+ if (cwdHonored(config) && isValidCwd(body.cwd))
1982
+ setSessionCwd(auth.sessionId, body.cwd);
1983
+ else if (inProcessSingleUserLane(config) && isValidCwd(body.cwd) && satisfiedByProcessCwd(body.cwd, realpathSync))
1984
+ logger.debug("task_cwd_inherited", { lane: execLane, sessionId: auth.sessionId });
1985
+ else
1986
+ logger.warn("task_cwd_ignored", { honored: cwdHonored(config), lane: execLane, sessionId: auth.sessionId });
1987
+ }
1988
+ if (parsedSettings.settings?.shellEnv && auth?.sessionId) {
1989
+ if (cwdHonored(config))
1990
+ setSessionShellEnv(auth.sessionId, parsedSettings.settings.shellEnv);
1991
+ else if (inProcessSingleUserLane(config)) {
1992
+ const requested = parsedSettings.settings.shellEnv;
1993
+ const mismatched = shellEnvMismatchCount(requested, process.env);
1994
+ if (mismatched === 0)
1995
+ logger.debug("task_shell_env_inherited", { lane: execLane, keys: Object.keys(requested).length, sessionId: auth.sessionId });
1974
1996
  else
1975
- logger.warn("task_cwd_ignored", { honored: cwdHonored(config), lane: execLane, sessionId: auth.sessionId });
1997
+ logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, mismatchedKeys: mismatched, sessionId: auth.sessionId });
1976
1998
  }
1977
- if (parsedSettings.settings?.shellEnv && auth?.sessionId) {
1978
- if (cwdHonored(config))
1979
- setSessionShellEnv(auth.sessionId, parsedSettings.settings.shellEnv);
1980
- else if (inProcessSingleUserLane(config)) {
1981
- const requested = parsedSettings.settings.shellEnv;
1982
- const mismatched = shellEnvMismatchCount(requested, process.env);
1983
- if (mismatched === 0)
1984
- logger.debug("task_shell_env_inherited", { lane: execLane, keys: Object.keys(requested).length, sessionId: auth.sessionId });
1985
- else
1986
- logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, mismatchedKeys: mismatched, sessionId: auth.sessionId });
1987
- }
1988
- else
1989
- logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, sessionId: auth.sessionId });
1999
+ else
2000
+ logger.warn("task_shell_env_ignored", { honored: false, lane: execLane, sessionId: auth.sessionId });
2001
+ }
2002
+ const taskHooks = (() => {
2003
+ const hc = parsedSettings.settings?.hooks;
2004
+ if (!hc)
2005
+ return undefined;
2006
+ if (config.requirePrincipal === true) {
2007
+ logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2008
+ return undefined;
1990
2009
  }
1991
- const taskHooks = (() => {
1992
- const hc = parsedSettings.settings?.hooks;
1993
- if (!hc)
2010
+ const effMode = coercePermissionMode(body.permissionMode) ?? parsedSettings.settings?.permissions?.defaultMode;
2011
+ return createTaskHooks(hc, {
2012
+ logger,
2013
+ sessionId: auth?.sessionId ?? "",
2014
+ cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
2015
+ ...(effMode ? { permissionMode: effMode } : {}),
2016
+ ...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
2017
+ hookLlm,
2018
+ hookAgent,
2019
+ onHookNotice: (n) => fleetBus.publishHookNotice({
2020
+ ...n,
2021
+ ...(auth?.principal ? { ownerScope: auth.principal } : {}),
2022
+ ...(auth?.sessionId ? { ownerSessionId: auth.sessionId } : {}),
2023
+ }),
2024
+ ...(auth?.sessionId
2025
+ ? {
2026
+ wake: async (text) => hookWakeBus.deliver
2027
+ ?
2028
+ hookWakeBus.deliver(auth.sessionId, `[hook asyncRewake] ${redactSecrets(text)}`)
2029
+ : false,
2030
+ }
2031
+ : {}),
2032
+ });
2033
+ })();
2034
+ const rawAddDirs = body.additionalDirectories;
2035
+ const additionalDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddDirs) : undefined;
2036
+ if (rawAddDirs !== undefined && !cwdHonored(config)) {
2037
+ logger.warn("task_additional_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2038
+ }
2039
+ const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
2040
+ const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog);
2041
+ if (picked.unknownExplicit !== undefined) {
2042
+ metrics.inc("task_model_unknown_fallback_total");
2043
+ logger.warn("task_model_unknown_fallback", { requested: picked.unknownExplicit.slice(0, 120), fallback: picked.model, sessionId: auth?.sessionId ?? null });
2044
+ }
2045
+ if (Array.isArray(body.images) && body.images.length > 0 && !modelSupportsImages(wireCatalog[picked.model])) {
2046
+ metrics.inc("images_omitted_total", { model: picked.model });
2047
+ logger.warn("images_omitted_no_vision_model", { model: picked.model, count: body.images.length });
2048
+ }
2049
+ let resumeAtEntryId;
2050
+ if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
2051
+ if (!auth?.sessionId)
2052
+ throw new HttpError(422, "resumeAt requires a session to branch (resume_at.no_session)");
2053
+ if (!resumeAnchorStore || !ownerAware.getLeafId)
2054
+ throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
2055
+ resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
2056
+ if (resumeAtEntryId === undefined)
2057
+ throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)");
2058
+ }
2059
+ let rewindFilesToEntryId;
2060
+ if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
2061
+ if (!auth?.sessionId)
2062
+ throw new HttpError(422, "rewindFilesTo requires a session (rewind_files_to.no_session)");
2063
+ if (!resumeAnchorStore || !ownerAware.getLeafId)
2064
+ throw new HttpError(501, "rewind-files-to is not available on this worker (no session-store backend for the anchor map)");
2065
+ rewindFilesToEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.rewindFilesTo, auth.principal ?? null);
2066
+ if (rewindFilesToEntryId === undefined)
2067
+ throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)");
2068
+ }
2069
+ const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId : undefined);
2070
+ const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
2071
+ const spec = {
2072
+ objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
2073
+ systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
2074
+ appendSystemPrompt: acceptedAppend,
2075
+ sessionId: auth?.sessionId,
2076
+ principal: auth?.principal,
2077
+ images: body.images,
2078
+ ...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
2079
+ ...(() => {
2080
+ const ct = body.compaction?.clampTolerance;
2081
+ return typeof ct === "number" && Number.isFinite(ct) && ct >= 0 && ct <= 1 ? { compaction: { clampTolerance: ct } } : {};
2082
+ })(),
2083
+ outputSchema: body.outputSchema,
2084
+ ...(typeof body.outputRetries === "number" && Number.isFinite(body.outputRetries) && body.outputRetries >= 1
2085
+ ? { outputRetries: Math.min(10, Math.floor(body.outputRetries)) }
2086
+ : {}),
2087
+ resumeAt: resumeAtEntryId,
2088
+ resumeAtMode: normalizeResumeAtMode(body.resumeAtMode, resumeAtEntryId !== undefined),
2089
+ requireExistingSession: body.requireExistingSession === true ? true : undefined,
2090
+ enableFork: enableForkFromBody(body, config, Boolean(centerRuntimeCapsResolver)),
2091
+ suggestNextPrompts: normalizeSuggestNextPrompts(body.suggestNextPrompts),
2092
+ rewindFiles: body.rewindFiles === true ? true : undefined,
2093
+ rewindFilesTo: rewindFilesToEntryId,
2094
+ additionalDirectories,
2095
+ enablePlanMode: config.planModeEnabled ? true : undefined,
2096
+ selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
2097
+ forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
2098
+ retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
2099
+ ...taskAgentsSpecFragment(body.agents, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })),
2100
+ ...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
2101
+ ? { retainBackgroundProcesses: true }
2102
+ : {}),
2103
+ ...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
2104
+ ...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
2105
+ ...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
2106
+ ...(typeof body.interactiveTools === "boolean"
2107
+ ? { interactiveTools: body.interactiveTools }
2108
+ : {}),
2109
+ resilience: normalizeResilience(body.resilience, explicitOperator(auth?.principal, config.operatorPrincipals)),
2110
+ finalVerification: cap.finalVerification === true || body.finalVerification === true ? true : undefined,
2111
+ attachments: normalizeAttachments(body.attachments),
2112
+ ...(taskHooks ? { hooks: composeHooks(deploymentHooks, taskHooks) } : {}),
2113
+ model: picked.model,
2114
+ ...((() => {
2115
+ if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0)
2116
+ return {};
2117
+ const cm = matchCatalogModel(body.compactionModel, wireCatalog);
2118
+ if (cm === undefined) {
2119
+ if (opts?.leg === "fresh") {
2120
+ throw new HttpError(400, `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id; a catalog refresh may have removed it mid-request)`);
2121
+ }
2122
+ metrics.inc("task_model_unknown_fallback_total");
2123
+ logger.warn("compaction_model_unknown_fallback", { requested: body.compactionModel.slice(0, 120), sessionId: auth?.sessionId ?? null });
2124
+ return {};
2125
+ }
2126
+ return { compactionModel: cm };
2127
+ })()),
2128
+ thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
2129
+ getApiKeyAndHeaders: keyResolver,
2130
+ ...(() => {
2131
+ const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
2132
+ const declarations = [];
2133
+ if (taskLimitCaps.timeoutSec !== undefined)
2134
+ declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling on caller limits.timeoutSec)" });
2135
+ if (taskLimitCaps.maxOutputTokens !== undefined)
2136
+ declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
2137
+ if (taskLimitCaps.maxTurns !== undefined)
2138
+ declarations.push({ key: "server.limits.maxTurnsCap", value: String(taskLimitCaps.maxTurns), reason: "env TASK_MAX_TURNS_MAX (operator ceiling)" });
2139
+ if (config.maxTaskCostUsd > 0)
2140
+ declarations.push({ key: "server.budget.maxCostUsdCap", value: String(config.maxTaskCostUsd), reason: "env MAX_TASK_COST_USD (operator ceiling; becomes the effective value when the caller sends none)" });
2141
+ if (config.maxTaskTokens > 0)
2142
+ declarations.push({ key: "server.budget.maxTokensCap", value: String(config.maxTaskTokens), reason: "env MAX_TASK_TOKENS_MAX (operator ceiling; becomes the effective value when the caller sends none)" });
2143
+ if (config.requirePrincipal === true)
2144
+ declarations.push({ key: "server.limits.wallClockBaseSec", value: "2400/3600", reason: "tenancy wall-clock base (multi-tenant; big tasks 3600) — TASK_TIMEOUT_SEC raises, never shrinks" });
2145
+ return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
2146
+ })(),
2147
+ ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
2148
+ maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
2149
+ maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
2150
+ degrade: (() => {
2151
+ if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
1994
2152
  return undefined;
1995
- if (config.requirePrincipal === true) {
1996
- logger.warn("task_hooks_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2153
+ if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
2154
+ metrics.inc("degrade_dropped_total", { reason: "vision_target" });
2155
+ logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
1997
2156
  return undefined;
1998
2157
  }
1999
- const effMode = coercePermissionMode(body.permissionMode) ?? parsedSettings.settings?.permissions?.defaultMode;
2000
- return createTaskHooks(hc, {
2001
- logger,
2002
- sessionId: auth?.sessionId ?? "",
2003
- cwd: (auth?.sessionId ? perSessionCwd.get(auth.sessionId) : undefined) ?? process.cwd(),
2004
- ...(effMode ? { permissionMode: effMode } : {}),
2005
- ...(parsedSettings.settings?.shellEnv ? { shellEnv: parsedSettings.settings.shellEnv } : {}),
2006
- hookLlm,
2007
- hookAgent,
2008
- onHookNotice: (n) => fleetBus.publishHookNotice({
2009
- ...n,
2010
- ...(auth?.principal ? { ownerScope: auth.principal } : {}),
2011
- ...(auth?.sessionId ? { ownerSessionId: auth.sessionId } : {}),
2012
- }),
2013
- ...(auth?.sessionId
2158
+ return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
2159
+ })(),
2160
+ memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes) : undefined,
2161
+ tools: ((base) => {
2162
+ const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
2163
+ const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
2164
+ return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
2165
+ })(cap.tools),
2166
+ skills: mergeUserSkills(cap.skills, body.skills, logger),
2167
+ mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
2168
+ promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
2169
+ toolPolicy: durableEnabled
2170
+ ? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
2171
+ requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
2172
+ ...(approvalExemptionStore && auth?.sessionId
2014
2173
  ? {
2015
- wake: async (text) => hookWakeBus.deliver
2016
- ?
2017
- hookWakeBus.deliver(auth.sessionId, `[hook asyncRewake] ${redactSecrets(text)}`)
2018
- : false,
2174
+ exempt: (toolName) => approvalExemptionStore.has(auth.sessionId, toolName),
2175
+ onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: auth.sessionId, toolName, rawToolName }),
2019
2176
  }
2020
2177
  : {}),
2021
- });
2022
- })();
2023
- const rawAddDirs = body.additionalDirectories;
2024
- const additionalDirectories = cwdHonored(config) ? parseAdditionalDirectories(rawAddDirs) : undefined;
2025
- if (rawAddDirs !== undefined && !cwdHonored(config)) {
2026
- logger.warn("task_additional_directories_ignored", { honored: false, sessionId: auth?.sessionId ?? null });
2027
- }
2028
- const wireCatalog = expandTiers(config.models, config.tiers) ?? config.models;
2029
- const picked = resolveTaskModel(body.model ?? parsedSettings.settings?.model, objective, wireCatalog);
2030
- if (picked.unknownExplicit !== undefined) {
2031
- metrics.inc("task_model_unknown_fallback_total");
2032
- logger.warn("task_model_unknown_fallback", { requested: picked.unknownExplicit.slice(0, 120), fallback: picked.model, sessionId: auth?.sessionId ?? null });
2033
- }
2034
- if (Array.isArray(body.images) && body.images.length > 0 && !modelSupportsImages(wireCatalog[picked.model])) {
2035
- metrics.inc("images_omitted_total", { model: picked.model });
2036
- logger.warn("images_omitted_no_vision_model", { model: picked.model, count: body.images.length });
2037
- }
2038
- let resumeAtEntryId;
2039
- if (typeof body.resumeAt === "string" && body.resumeAt.length > 0) {
2040
- if (!auth?.sessionId)
2041
- throw new HttpError(422, "resumeAt requires a session to branch (resume_at.no_session)");
2042
- if (!resumeAnchorStore || !ownerAware.getLeafId)
2043
- throw new HttpError(501, "resume-at is not available on this worker (no session-store backend for the anchor map)");
2044
- resumeAtEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.resumeAt, auth.principal ?? null);
2045
- if (resumeAtEntryId === undefined)
2046
- throw new HttpError(404, "resumeAt: no such message in this session (resume_at.unknown_event)");
2047
- }
2048
- let rewindFilesToEntryId;
2049
- if (resumeAtEntryId === undefined && typeof body.rewindFilesTo === "string" && body.rewindFilesTo.length > 0) {
2050
- if (!auth?.sessionId)
2051
- throw new HttpError(422, "rewindFilesTo requires a session (rewind_files_to.no_session)");
2052
- if (!resumeAnchorStore || !ownerAware.getLeafId)
2053
- throw new HttpError(501, "rewind-files-to is not available on this worker (no session-store backend for the anchor map)");
2054
- rewindFilesToEntryId = await resumeAnchorStore.resolve(auth.sessionId, body.rewindFilesTo, auth.principal ?? null);
2055
- if (rewindFilesToEntryId === undefined)
2056
- throw new HttpError(404, "rewindFilesTo: no such message in this session (rewind_files_to.unknown_event)");
2057
- }
2058
- const s4ProjectId = auth?.resolvedProjectId ?? (typeof body.projectId === "string" && body.projectId ? body.projectId : undefined);
2059
- const s4DefaultScopes = s4ProjectId ? config.projects[s4ProjectId]?.defaultScopes : undefined;
2060
- const spec = {
2061
- objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
2062
- systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
2063
- appendSystemPrompt: acceptedAppend,
2064
- sessionId: auth?.sessionId,
2065
- principal: auth?.principal,
2066
- images: body.images,
2067
- ...(body.clientContext ? { clientContext: { timeZone: body.clientContext.timeZone, userEmail: body.clientContext.userEmail } } : {}),
2068
- ...(() => {
2069
- const ct = body.compaction?.clampTolerance;
2070
- return typeof ct === "number" && Number.isFinite(ct) && ct >= 0 && ct <= 1 ? { compaction: { clampTolerance: ct } } : {};
2071
- })(),
2072
- outputSchema: body.outputSchema,
2073
- ...(typeof body.outputRetries === "number" && Number.isFinite(body.outputRetries) && body.outputRetries >= 1
2074
- ? { outputRetries: Math.min(10, Math.floor(body.outputRetries)) }
2075
- : {}),
2076
- resumeAt: resumeAtEntryId,
2077
- resumeAtMode: normalizeResumeAtMode(body.resumeAtMode, resumeAtEntryId !== undefined),
2078
- requireExistingSession: body.requireExistingSession === true ? true : undefined,
2079
- enableFork: enableForkFromBody(body, config, Boolean(centerRuntimeCapsResolver)),
2080
- suggestNextPrompts: normalizeSuggestNextPrompts(body.suggestNextPrompts),
2081
- rewindFiles: body.rewindFiles === true ? true : undefined,
2082
- rewindFilesTo: rewindFilesToEntryId,
2083
- additionalDirectories,
2084
- enablePlanMode: config.planModeEnabled ? true : undefined,
2085
- selfOrchestration: selfOrchestrationFromBody({ selfOrchestration: body.selfOrchestration === true || parsedSettings.settings?.ultracode === true }, config, Boolean(centerRuntimeCapsResolver)),
2086
- forwardSubagentEvents: body.forwardSubagentEvents === true ? true : undefined,
2087
- retainSubagentSessions: normalizeRetainSubagentSessions(body.retainSubagentSessions),
2088
- ...taskAgentsSpecFragment(body.agents, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })),
2089
- ...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
2090
- ? { retainBackgroundProcesses: true }
2091
- : {}),
2092
- ...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
2093
- ...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
2094
- ...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
2095
- ...(typeof body.interactiveTools === "boolean"
2096
- ? { interactiveTools: body.interactiveTools }
2097
- : {}),
2098
- resilience: normalizeResilience(body.resilience, explicitOperator(auth?.principal, config.operatorPrincipals)),
2099
- finalVerification: cap.finalVerification === true || body.finalVerification === true ? true : undefined,
2100
- attachments: normalizeAttachments(body.attachments),
2101
- ...(taskHooks ? { hooks: composeHooks(deploymentHooks, taskHooks) } : {}),
2102
- model: picked.model,
2103
- ...((() => {
2104
- if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0)
2105
- return {};
2106
- const cm = matchCatalogModel(body.compactionModel, wireCatalog);
2107
- if (cm === undefined) {
2108
- if (opts?.leg === "fresh") {
2109
- throw new HttpError(400, `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id; a catalog refresh may have removed it mid-request)`);
2110
- }
2111
- metrics.inc("task_model_unknown_fallback_total");
2112
- logger.warn("compaction_model_unknown_fallback", { requested: body.compactionModel.slice(0, 120), sessionId: auth?.sessionId ?? null });
2113
- return {};
2114
- }
2115
- return { compactionModel: cm };
2116
- })()),
2117
- thinking: effectiveThinking(body.reasoningEffort, parsedSettings.settings?.ultracode === true),
2118
- getApiKeyAndHeaders: keyResolver,
2119
- ...(() => {
2120
- const limits = resolveTaskLimits(body.limits, taskLimitCaps, taskTimeoutSec, config.requirePrincipal, body.council === true || body.debate === true || scenarioName === "team");
2121
- const declarations = [];
2122
- if (taskLimitCaps.timeoutSec !== undefined)
2123
- declarations.push({ key: "server.limits.timeoutSecCap", value: String(taskLimitCaps.timeoutSec), reason: "env TASK_TIMEOUT_MAX_SEC (operator ceiling on caller limits.timeoutSec)" });
2124
- if (taskLimitCaps.maxOutputTokens !== undefined)
2125
- declarations.push({ key: "server.limits.maxOutputTokensCap", value: String(taskLimitCaps.maxOutputTokens), reason: "env TASK_MAX_OUTPUT_TOKENS_MAX (operator ceiling)" });
2126
- if (taskLimitCaps.maxTurns !== undefined)
2127
- declarations.push({ key: "server.limits.maxTurnsCap", value: String(taskLimitCaps.maxTurns), reason: "env TASK_MAX_TURNS_MAX (operator ceiling)" });
2128
- if (config.maxTaskCostUsd > 0)
2129
- declarations.push({ key: "server.budget.maxCostUsdCap", value: String(config.maxTaskCostUsd), reason: "env MAX_TASK_COST_USD (operator ceiling; becomes the effective value when the caller sends none)" });
2130
- if (config.maxTaskTokens > 0)
2131
- declarations.push({ key: "server.budget.maxTokensCap", value: String(config.maxTaskTokens), reason: "env MAX_TASK_TOKENS_MAX (operator ceiling; becomes the effective value when the caller sends none)" });
2132
- if (config.requirePrincipal === true)
2133
- declarations.push({ key: "server.limits.wallClockBaseSec", value: "2400/3600", reason: "tenancy wall-clock base (multi-tenant; big tasks 3600) — TASK_TIMEOUT_SEC raises, never shrinks" });
2134
- return { ...(limits !== undefined ? { limits } : {}), ...(declarations.length > 0 ? { configOverrides: declarations } : {}) };
2135
- })(),
2136
- ...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
2137
- maxCostUsd: cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd),
2138
- maxTokens: cappedCeiling(body.maxTokens, config.maxTaskTokens),
2139
- degrade: (() => {
2140
- if (!config.degrade || cappedCeiling(body.maxCostUsd, config.maxTaskCostUsd) === undefined)
2141
- return undefined;
2142
- if (Array.isArray(body.images) && body.images.length > 0 && !config.degrade.toSupportsImages) {
2143
- metrics.inc("degrade_dropped_total", { reason: "vision_target" });
2144
- logger.warn("degrade_dropped", { reason: "vision_target", model: picked.model, images: body.images.length });
2145
- return undefined;
2146
- }
2147
- return { to: config.degrade.to, atCostFraction: config.degrade.atCostFraction };
2148
- })(),
2149
- memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes) : undefined,
2150
- tools: ((base) => {
2151
- const extra = [...(selectEnvTool ? [selectEnvTool] : []), ...(sendUserFileToolSpec ? [sendUserFileToolSpec] : [])];
2152
- const merged = extra.length > 0 ? [...(base ?? []), ...extra] : base;
2153
- return merged && config.toolDeferLongtail ? applyLongtailDefer(merged, true) : merged;
2154
- })(cap.tools),
2155
- skills: mergeUserSkills(cap.skills, body.skills, logger),
2156
- mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
2157
- promptProvider: centerDecls ? centerPromptProvider(centerDecls, scenarioName) : cap.promptProvider,
2158
- toolPolicy: durableEnabled
2159
- ? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
2160
- requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
2161
- ...(approvalExemptionStore && auth?.sessionId
2178
+ }))
2179
+ : approvalEnabled && approvalStore
2180
+ ? createOaApprovalPolicy({
2181
+ store: approvalStore,
2182
+ requireApproval: config.approvalRequire,
2183
+ deny: config.approvalDeny,
2184
+ pollMs: config.approvalPollMs,
2185
+ context: () => ({ sessionId: auth?.sessionId ?? null, owner: auth?.principal ?? null }),
2186
+ neverAuto: config.approvalNeverAuto,
2187
+ ...(approvalExemptionStore
2162
2188
  ? {
2163
- exempt: (toolName) => approvalExemptionStore.has(auth.sessionId, toolName),
2164
- onExempted: (toolName, rawToolName) => logger.info("approval_exempted", { sessionId: auth.sessionId, toolName, rawToolName }),
2189
+ exempt: (sessionId, toolName) => approvalExemptionStore.has(sessionId, toolName),
2190
+ onExempted: (sessionId, toolName) => logger.info("approval_exempted", { sessionId, toolName }),
2165
2191
  }
2166
2192
  : {}),
2167
- }))
2168
- : approvalEnabled && approvalStore
2169
- ? createOaApprovalPolicy({
2170
- store: approvalStore,
2171
- requireApproval: config.approvalRequire,
2172
- deny: config.approvalDeny,
2173
- pollMs: config.approvalPollMs,
2174
- context: () => ({ sessionId: auth?.sessionId ?? null, owner: auth?.principal ?? null }),
2175
- neverAuto: config.approvalNeverAuto,
2176
- ...(approvalExemptionStore
2177
- ? {
2178
- exempt: (sessionId, toolName) => approvalExemptionStore.has(sessionId, toolName),
2179
- onExempted: (sessionId, toolName) => logger.info("approval_exempted", { sessionId, toolName }),
2180
- }
2181
- : {}),
2182
- })
2183
- : singleUserAutoAcceptBaseline
2184
- ? createAllowDenyPolicy({})
2185
- : undefined,
2186
- ...(durableEnabled
2187
- ? {
2188
- checkpointStore,
2189
- durableApproval: {
2190
- scope: auth?.principal ?? "_",
2191
- ...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
2192
- },
2193
- ...((rs) => (rs ? { resourceSuspend: rs } : {}))(resourceSuspendOptIn({
2194
- enabled: config.resourceSuspend,
2195
- ttlSec: config.resourceSuspendTtlSec,
2196
- isVerify: body.verify === true,
2197
- isCascade: body.cascade === true,
2198
- scope: auth?.principal ?? "_",
2199
- })),
2200
- onQuestion: QUESTION_AWAITS_RESUME,
2201
- }
2202
- : {}),
2203
- };
2204
- const governedBase = applyRuntimeGovernance(spec, { autonomy: config.autonomy, commandPolicy: config.commandPolicy });
2205
- let governed = governedBase;
2206
- const bodyMode = coercePermissionMode(body.permissionMode);
2207
- let effectiveSettings = bodyMode ? withPermissionMode(parsedSettings.settings, bodyMode) : parsedSettings.settings;
2208
- if (appendLessPack && effectiveSettings?.outputStyle) {
2209
- logger.warn("output_style_dropped", { detail: "the effective prompt pack cannot mount the append slot (already-assembled systemPrompt or center assembled-identity declaration)", sessionId: auth?.sessionId ?? null });
2210
- const { outputStyle: _dropped, ...rest } = effectiveSettings;
2211
- effectiveSettings = Object.keys(rest).length > 0 ? rest : undefined;
2212
- }
2213
- if (effectiveSettings?.outputStyle && acceptedAppend && acceptedAppend.length + 2 + effectiveSettings.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
2214
- logger.warn("output_style_dropped", { detail: `combined append carriers exceed the ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} cap (rider ${acceptedAppend.length} + style ${effectiveSettings.outputStyle.length}) — style dropped (pre-cap stored body on a resume leg?)`, sessionId: auth?.sessionId ?? null });
2215
- const { outputStyle: _dropped2, ...rest2 } = effectiveSettings;
2216
- effectiveSettings = Object.keys(rest2).length > 0 ? rest2 : undefined;
2217
- }
2218
- const hostSemanticsLane = config.remoteExec === undefined || config.remoteExec.provider === "host";
2219
- const shellScratchpad = await acceptShellScratchpadDir(body.scratchpadDir, {
2220
- requirePrincipal: config.requirePrincipal,
2221
- hostSemanticsLane,
2222
- warn: (msg, meta) => logger.warn(msg, { ...(meta ?? {}), sessionId: auth?.sessionId ?? null }),
2223
- });
2224
- const scratchpadDir = shellScratchpad ??
2225
- (hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
2226
- if (effectiveSettings) {
2227
- const fsWriteGate = hostSemanticsLane
2228
- ? (() => {
2229
- const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
2230
- const gateCwd = sessionCwd ??
2231
- (config.remoteExec === undefined
2232
- ? process.cwd()
2233
- : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
2234
- return {
2235
- env: new NodeExecutionEnv({ cwd: gateCwd }),
2236
- cwd: gateCwd,
2237
- ...(scratchpadDir ? { scratchpadDir } : {}),
2238
- ...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
2239
- ...(config.manualModeShellGate ? { shellGate: config.manualModeShellGate } : {}),
2240
- ...(approvalExemptionStore && auth?.sessionId
2241
- ? {
2242
- isExempt: async (toolName) => {
2243
- const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
2244
- if (hit)
2245
- logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
2246
- return hit;
2247
- },
2248
- }
2249
- : {}),
2250
- };
2251
- })()
2252
- : undefined;
2253
- const workflowGate = approvalExemptionStore && auth?.sessionId
2254
- ? {
2255
- isExempt: async (toolName) => {
2256
- const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
2257
- if (hit)
2258
- logger.info("workflow_gate_exempted", { toolName, sessionId: auth.sessionId });
2259
- return hit;
2260
- },
2261
- }
2262
- : undefined;
2263
- try {
2264
- governed = applyTaskSettings(governedBase, effectiveSettings, fsWriteGate, workflowGate);
2193
+ })
2194
+ : singleUserAutoAcceptBaseline
2195
+ ? createAllowDenyPolicy({})
2196
+ : undefined,
2197
+ ...(durableEnabled
2198
+ ? {
2199
+ checkpointStore,
2200
+ durableApproval: {
2201
+ scope: auth?.principal ?? "_",
2202
+ ...(config.approvalTimeoutSec > 0 ? { ttlMs: config.approvalTimeoutSec * 1000 } : {}),
2203
+ },
2204
+ ...((rs) => (rs ? { resourceSuspend: rs } : {}))(resourceSuspendOptIn({
2205
+ enabled: config.resourceSuspend,
2206
+ ttlSec: config.resourceSuspendTtlSec,
2207
+ isVerify: body.verify === true,
2208
+ isCascade: body.cascade === true,
2209
+ scope: auth?.principal ?? "_",
2210
+ })),
2211
+ onQuestion: QUESTION_AWAITS_RESUME,
2265
2212
  }
2266
- catch (e) {
2267
- throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
2213
+ : {}),
2214
+ };
2215
+ const governedBase = applyRuntimeGovernance(spec, { autonomy: config.autonomy, commandPolicy: config.commandPolicy });
2216
+ let governed = governedBase;
2217
+ const bodyMode = coercePermissionMode(body.permissionMode);
2218
+ let effectiveSettings = bodyMode ? withPermissionMode(parsedSettings.settings, bodyMode) : parsedSettings.settings;
2219
+ if (appendLessPack && effectiveSettings?.outputStyle) {
2220
+ logger.warn("output_style_dropped", { detail: "the effective prompt pack cannot mount the append slot (already-assembled systemPrompt or center assembled-identity declaration)", sessionId: auth?.sessionId ?? null });
2221
+ const { outputStyle: _dropped, ...rest } = effectiveSettings;
2222
+ effectiveSettings = Object.keys(rest).length > 0 ? rest : undefined;
2223
+ }
2224
+ if (effectiveSettings?.outputStyle && acceptedAppend && acceptedAppend.length + 2 + effectiveSettings.outputStyle.length > MAX_SETTINGS_OUTPUT_STYLE_CHARS) {
2225
+ logger.warn("output_style_dropped", { detail: `combined append carriers exceed the ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} cap (rider ${acceptedAppend.length} + style ${effectiveSettings.outputStyle.length}) — style dropped (pre-cap stored body on a resume leg?)`, sessionId: auth?.sessionId ?? null });
2226
+ const { outputStyle: _dropped2, ...rest2 } = effectiveSettings;
2227
+ effectiveSettings = Object.keys(rest2).length > 0 ? rest2 : undefined;
2228
+ }
2229
+ const hostSemanticsLane = config.remoteExec === undefined || config.remoteExec.provider === "host";
2230
+ const shellScratchpad = await acceptShellScratchpadDir(body.scratchpadDir, {
2231
+ requirePrincipal: config.requirePrincipal,
2232
+ hostSemanticsLane,
2233
+ warn: (msg, meta) => logger.warn(msg, { ...(meta ?? {}), sessionId: auth?.sessionId ?? null }),
2234
+ });
2235
+ const scratchpadDir = shellScratchpad ??
2236
+ (hostSemanticsLane && auth?.sessionId ? await ensureScratchpadDir(config.localDataRoot ?? localRoot, auth.sessionId) : undefined);
2237
+ if (effectiveSettings) {
2238
+ const fsWriteGate = hostSemanticsLane
2239
+ ? (() => {
2240
+ const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
2241
+ const gateCwd = sessionCwd ??
2242
+ (config.remoteExec === undefined
2243
+ ? process.cwd()
2244
+ : join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted"));
2245
+ return {
2246
+ env: new NodeExecutionEnv({ cwd: gateCwd }),
2247
+ cwd: gateCwd,
2248
+ ...(scratchpadDir ? { scratchpadDir } : {}),
2249
+ ...(config.sensitiveWritePatterns.length > 0 ? { sensitivePatterns: config.sensitiveWritePatterns } : {}),
2250
+ ...(config.manualModeShellGate ? { shellGate: config.manualModeShellGate } : {}),
2251
+ ...(approvalExemptionStore && auth?.sessionId
2252
+ ? {
2253
+ isExempt: async (toolName) => {
2254
+ const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
2255
+ if (hit)
2256
+ logger.info("fs_write_gate_exempted", { toolName, sessionId: auth.sessionId });
2257
+ return hit;
2258
+ },
2259
+ }
2260
+ : {}),
2261
+ };
2262
+ })()
2263
+ : undefined;
2264
+ const workflowGate = approvalExemptionStore && auth?.sessionId
2265
+ ? {
2266
+ isExempt: async (toolName) => {
2267
+ const hit = await approvalExemptionStore.has(auth.sessionId, toolName);
2268
+ if (hit)
2269
+ logger.info("workflow_gate_exempted", { toolName, sessionId: auth.sessionId });
2270
+ return hit;
2271
+ },
2268
2272
  }
2273
+ : undefined;
2274
+ try {
2275
+ governed = applyTaskSettings(governedBase, effectiveSettings, fsWriteGate, workflowGate);
2269
2276
  }
2270
- if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
2271
- governed = { ...governed, tools: stripDelegationTools(governed.tools) };
2277
+ catch (e) {
2278
+ throw new HttpError(422, `settings are tighten-only and cannot loosen the deployment policy: ${e.message}`);
2272
2279
  }
2273
- const bodyProfile = typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 ? body.sandboxImageProfile : undefined;
2274
- const taskImageProfile = bodyProfile ?? (body.cascade === true || body.verify === true ? undefined : sessionEnvSelection.get(auth?.sessionId));
2275
- let resolvedImageCaps;
2276
- if (taskImageProfile !== undefined) {
2277
- if (config.remoteExec?.provider !== "k8s") {
2278
- throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
2279
- }
2280
- if (!imageIndex) {
2281
- throw new HttpError(400, "sandboxImageProfile requires the image index (sema-registry backend not configured)");
2282
- }
2283
- if (!auth?.sessionId) {
2284
- throw new HttpError(400, "sandboxImageProfile requires a resolved session");
2285
- }
2286
- const capsNeeded = Array.isArray(body.capabilitiesNeeded)
2287
- ? body.capabilitiesNeeded.filter((c) => typeof c === "string")
2288
- : undefined;
2289
- const resolved = await resolveSandboxImageRef({
2290
- profile: taskImageProfile,
2291
- ...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
2292
- viewer: { operator: explicitOperator(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
2293
- index: imageIndex,
2294
- });
2295
- if (!resolved.ok)
2296
- throw new HttpError(resolved.status, resolved.message);
2297
- perTaskImage.set(auth.sessionId, resolved.ref);
2298
- resolvedImageCaps = resolved.capabilities;
2280
+ }
2281
+ if (governed.handsReadOnly === true && Array.isArray(governed.tools)) {
2282
+ governed = { ...governed, tools: stripDelegationTools(governed.tools) };
2283
+ }
2284
+ const bodyProfile = typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 ? body.sandboxImageProfile : undefined;
2285
+ const taskImageProfile = bodyProfile ?? (body.cascade === true || body.verify === true ? undefined : sessionEnvSelection.get(auth?.sessionId));
2286
+ let resolvedImageCaps;
2287
+ if (taskImageProfile !== undefined) {
2288
+ if (config.remoteExec?.provider !== "k8s") {
2289
+ throw new HttpError(400, "sandboxImageProfile is only supported on the k8s sandbox backend");
2299
2290
  }
2300
- if (config.envFactsEnabled) {
2301
- const pkgSourceLane = config.remoteExec?.provider === "e2b" || config.remoteExec?.provider === "k8s";
2302
- const remoteScratchpadDir = !hostSemanticsLane && isRemoteScratchpadLane(config.remoteExec?.provider)
2303
- ? remoteScratchpadDirFor(auth?.sessionId)
2304
- : undefined;
2305
- const factScratchpadDir = scratchpadDir ?? remoteScratchpadDir;
2306
- const facts = buildEnvFacts({
2307
- profile: taskImageProfile,
2308
- capabilities: resolvedImageCaps,
2309
- ...(pkgSourceLane ? { pkgSource: config.sandboxPkgSource } : {}),
2310
- egress: egressForRemoteExec(config.remoteExec),
2311
- ...(factScratchpadDir ? { scratchpadDir: factScratchpadDir } : {}),
2312
- resumeFacts: resumeFactsForLane(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? { k8sSnapshot: Boolean(config.remoteExec.s3Snapshot) } : undefined),
2313
- });
2314
- if (facts)
2315
- governed = { ...governed, envFacts: facts };
2291
+ if (!imageIndex) {
2292
+ throw new HttpError(400, "sandboxImageProfile requires the image index (sema-registry backend not configured)");
2316
2293
  }
2317
- if (scratchpadDir !== undefined) {
2318
- const dirs = governed.additionalDirectories ?? [];
2319
- if (!dirs.includes(scratchpadDir))
2320
- governed = { ...governed, additionalDirectories: [...dirs, scratchpadDir] };
2294
+ if (!auth?.sessionId) {
2295
+ throw new HttpError(400, "sandboxImageProfile requires a resolved session");
2321
2296
  }
2322
- if (config.routerEnabled) {
2323
- const isolatedExecEnv = isIsolatedExecEnv(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? config.remoteExec.runtimeClass : undefined);
2324
- const decision = routeServiceTask({
2325
- isolatedExecEnv,
2326
- hasExecutionEnv: config.remoteExec != null,
2327
- ...(config.autonomy ? { autonomy: config.autonomy } : {}),
2328
- explicitTeam: body.council === true || body.debate === true || scenarioName === "team",
2329
- });
2330
- logger.info("orchestration_routed", { mode: decision.mode, reason: decision.reason, sessionId: auth?.sessionId ?? null });
2331
- if (decision.mode === "supervisor") {
2332
- return tightenTaskSpec(governed, supPostureOverrides());
2333
- }
2297
+ const capsNeeded = Array.isArray(body.capabilitiesNeeded)
2298
+ ? body.capabilitiesNeeded.filter((c) => typeof c === "string")
2299
+ : undefined;
2300
+ const resolved = await resolveSandboxImageRef({
2301
+ profile: taskImageProfile,
2302
+ ...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
2303
+ viewer: { operator: explicitOperator(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
2304
+ index: imageIndex,
2305
+ });
2306
+ if (!resolved.ok)
2307
+ throw new HttpError(resolved.status, resolved.message);
2308
+ perTaskImage.set(auth.sessionId, resolved.ref);
2309
+ resolvedImageCaps = resolved.capabilities;
2310
+ }
2311
+ if (config.envFactsEnabled) {
2312
+ const pkgSourceLane = config.remoteExec?.provider === "e2b" || config.remoteExec?.provider === "k8s";
2313
+ const remoteScratchpadDir = !hostSemanticsLane && isRemoteScratchpadLane(config.remoteExec?.provider)
2314
+ ? remoteScratchpadDirFor(auth?.sessionId)
2315
+ : undefined;
2316
+ const factScratchpadDir = scratchpadDir ?? remoteScratchpadDir;
2317
+ const facts = buildEnvFacts({
2318
+ profile: taskImageProfile,
2319
+ capabilities: resolvedImageCaps,
2320
+ ...(pkgSourceLane ? { pkgSource: config.sandboxPkgSource } : {}),
2321
+ egress: egressForRemoteExec(config.remoteExec),
2322
+ ...(factScratchpadDir ? { scratchpadDir: factScratchpadDir } : {}),
2323
+ resumeFacts: resumeFactsForLane(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? { k8sSnapshot: Boolean(config.remoteExec.s3Snapshot) } : undefined),
2324
+ });
2325
+ if (facts)
2326
+ governed = { ...governed, envFacts: facts };
2327
+ }
2328
+ if (scratchpadDir !== undefined) {
2329
+ const dirs = governed.additionalDirectories ?? [];
2330
+ if (!dirs.includes(scratchpadDir))
2331
+ governed = { ...governed, additionalDirectories: [...dirs, scratchpadDir] };
2332
+ }
2333
+ if (config.routerEnabled) {
2334
+ const isolatedExecEnv = isIsolatedExecEnv(config.remoteExec?.provider, config.remoteExec?.provider === "k8s" ? config.remoteExec.runtimeClass : undefined);
2335
+ const decision = routeServiceTask({
2336
+ isolatedExecEnv,
2337
+ hasExecutionEnv: config.remoteExec != null,
2338
+ ...(config.autonomy ? { autonomy: config.autonomy } : {}),
2339
+ explicitTeam: body.council === true || body.debate === true || scenarioName === "team",
2340
+ });
2341
+ logger.info("orchestration_routed", { mode: decision.mode, reason: decision.reason, sessionId: auth?.sessionId ?? null });
2342
+ if (decision.mode === "supervisor") {
2343
+ return tightenTaskSpec(governed, supPostureOverrides());
2334
2344
  }
2335
- return governed;
2336
- },
2337
- });
2345
+ }
2346
+ return governed;
2347
+ };
2348
+ const server = createHttpServer({ runner, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
2338
2349
  runDenySweep = server.denyExpiredApprovals;
2339
2350
  const bindHost = resolveBindHost(config);
2340
2351
  await new Promise((resolve) => (bindHost ? server.listen(config.port, bindHost, resolve) : server.listen(config.port, resolve)));