agent-relay-server 0.60.1 → 0.61.1

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 (158) hide show
  1. package/docs/openapi.json +361 -1
  2. package/package.json +3 -2
  3. package/public/assets/{MessageBubble-vzupFQD8.js → MessageBubble-CX63ZV4X.js} +2 -2
  4. package/public/assets/{MessageBubble-vzupFQD8.js.map → MessageBubble-CX63ZV4X.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-DQvycBMW.js → PlanGraphPanel-Dby7wq9c.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-DQvycBMW.js.map → PlanGraphPanel-Dby7wq9c.js.map} +1 -1
  7. package/public/assets/{activity-gmfkOBhQ.js → activity-2xPOScu_.js} +2 -2
  8. package/public/assets/{activity-gmfkOBhQ.js.map → activity-2xPOScu_.js.map} +1 -1
  9. package/public/assets/{agent-profiles-7X8gFuwj.js → agent-profiles-BfGTTbeY.js} +2 -2
  10. package/public/assets/{agent-profiles-7X8gFuwj.js.map → agent-profiles-BfGTTbeY.js.map} +1 -1
  11. package/public/assets/agents-Bg3WOBRV.js +2 -0
  12. package/public/assets/{agents-D201CuTR.js.map → agents-Bg3WOBRV.js.map} +1 -1
  13. package/public/assets/{analytics-CkOTrr5t.js → analytics-De0cEZGx.js} +3 -3
  14. package/public/assets/{analytics-CkOTrr5t.js.map → analytics-De0cEZGx.js.map} +1 -1
  15. package/public/assets/{automation-_5u-nVs8.js → automation-CHxhzq2t.js} +2 -2
  16. package/public/assets/{automation-_5u-nVs8.js.map → automation-CHxhzq2t.js.map} +1 -1
  17. package/public/assets/{badge-Cy6qO_Jz.js → badge-DhLyCPag.js} +2 -2
  18. package/public/assets/{badge-Cy6qO_Jz.js.map → badge-DhLyCPag.js.map} +1 -1
  19. package/public/assets/branch-state-badge-DSyWKluD.js +2 -0
  20. package/public/assets/{branch-state-badge-BPXXLpQG.js.map → branch-state-badge-DSyWKluD.js.map} +1 -1
  21. package/public/assets/{button-73wpRw65.js → button-K20wAZG1.js} +2 -2
  22. package/public/assets/{button-73wpRw65.js.map → button-K20wAZG1.js.map} +1 -1
  23. package/public/assets/card-CcZ9hLI7.js +2 -0
  24. package/public/assets/{card-BU6PvVHv.js.map → card-CcZ9hLI7.js.map} +1 -1
  25. package/public/assets/{channels-BsJu5TXR.js → channels-BG0Nib7y.js} +2 -2
  26. package/public/assets/{channels-BsJu5TXR.js.map → channels-BG0Nib7y.js.map} +1 -1
  27. package/public/assets/chat-CjP_crSl.js +2 -0
  28. package/public/assets/{chat-CHgPGHQD.js.map → chat-CjP_crSl.js.map} +1 -1
  29. package/public/assets/{connectors-arjfBQvc.js → connectors-DWh7MwV_.js} +2 -2
  30. package/public/assets/{connectors-arjfBQvc.js.map → connectors-DWh7MwV_.js.map} +1 -1
  31. package/public/assets/{copy-button-HRopUwui.js → copy-button-eF3cKp4Q.js} +2 -2
  32. package/public/assets/{copy-button-HRopUwui.js.map → copy-button-eF3cKp4Q.js.map} +1 -1
  33. package/public/assets/display-DzNvOu1j.js +3 -0
  34. package/public/assets/{display-CketawEF.js.map → display-DzNvOu1j.js.map} +1 -1
  35. package/public/assets/{dist-Kkpnjx_M.js → dist-1dhfdhm9.js} +2 -2
  36. package/public/assets/{dist-Kkpnjx_M.js.map → dist-1dhfdhm9.js.map} +1 -1
  37. package/public/assets/dist-BLBF7qqr.js +2 -0
  38. package/public/assets/{dist-De2oguBm.js.map → dist-BLBF7qqr.js.map} +1 -1
  39. package/public/assets/{dist-T7u7IZGJ.js → dist-BrQsqBbc.js} +2 -2
  40. package/public/assets/{dist-T7u7IZGJ.js.map → dist-BrQsqBbc.js.map} +1 -1
  41. package/public/assets/{dist-Cko2KPW8.js → dist-COQuunEv.js} +2 -2
  42. package/public/assets/{dist-Cko2KPW8.js.map → dist-COQuunEv.js.map} +1 -1
  43. package/public/assets/{dist-CLHMeXYi.js → dist-Ci5Y37nj.js} +2 -2
  44. package/public/assets/{dist-CLHMeXYi.js.map → dist-Ci5Y37nj.js.map} +1 -1
  45. package/public/assets/{dist-Ci1MM51P.js → dist-CoROafNg.js} +2 -2
  46. package/public/assets/{dist-Ci1MM51P.js.map → dist-CoROafNg.js.map} +1 -1
  47. package/public/assets/{dist-CC5f7oYw.js → dist-I-MFUv51.js} +2 -2
  48. package/public/assets/{dist-CC5f7oYw.js.map → dist-I-MFUv51.js.map} +1 -1
  49. package/public/assets/{dist-DFLciw0h.js → dist-UQGKwqZH.js} +2 -2
  50. package/public/assets/{dist-DFLciw0h.js.map → dist-UQGKwqZH.js.map} +1 -1
  51. package/public/assets/dist-YpHfnnIA.js +2 -0
  52. package/public/assets/{dist-DUoZnlB2.js.map → dist-YpHfnnIA.js.map} +1 -1
  53. package/public/assets/dist-qd198ib9.js +2 -0
  54. package/public/assets/{dist-D41tzJYg.js.map → dist-qd198ib9.js.map} +1 -1
  55. package/public/assets/{dist-DmN94rc8.js → dist-yGOq6sB3.js} +2 -2
  56. package/public/assets/{dist-DmN94rc8.js.map → dist-yGOq6sB3.js.map} +1 -1
  57. package/public/assets/{es2015-Df08Opjx.js → es2015-CRxz9lOj.js} +2 -2
  58. package/public/assets/{es2015-Df08Opjx.js.map → es2015-CRxz9lOj.js.map} +1 -1
  59. package/public/assets/{formatted-body-impl-DBt5tnS0.js → formatted-body-impl-u0W_hWYA.js} +2 -2
  60. package/public/assets/{formatted-body-impl-DBt5tnS0.js.map → formatted-body-impl-u0W_hWYA.js.map} +1 -1
  61. package/public/assets/index-BwfVOzUG.js +21 -0
  62. package/public/assets/{index-D7js3XlH.js.map → index-BwfVOzUG.js.map} +1 -1
  63. package/public/assets/index-Dn_GDXbi.css +2 -0
  64. package/public/assets/{input-B2EDz-BE.js → input-BHYNpXAG.js} +2 -2
  65. package/public/assets/{input-B2EDz-BE.js.map → input-BHYNpXAG.js.map} +1 -1
  66. package/public/assets/{insights-BvjkOXk9.js → insights-CAF9Hi5D.js} +2 -2
  67. package/public/assets/{insights-BvjkOXk9.js.map → insights-CAF9Hi5D.js.map} +1 -1
  68. package/public/assets/{integrations-7L1f3eIE.js → integrations-CG9qyHUf.js} +2 -2
  69. package/public/assets/{integrations-7L1f3eIE.js.map → integrations-CG9qyHUf.js.map} +1 -1
  70. package/public/assets/{lib-BZrjIp-o.js → lib-Df4aph5u.js} +2 -2
  71. package/public/assets/{lib-BZrjIp-o.js.map → lib-Df4aph5u.js.map} +1 -1
  72. package/public/assets/lucide-react-L2UxFDic.js +9 -0
  73. package/public/assets/lucide-react-L2UxFDic.js.map +1 -0
  74. package/public/assets/{maintenance-BBoL--vs.js → maintenance-DgHA35ve.js} +2 -2
  75. package/public/assets/{maintenance-BBoL--vs.js.map → maintenance-DgHA35ve.js.map} +1 -1
  76. package/public/assets/{managed-agents-BjYBzVZt.js → managed-agents-Rk6ghKR9.js} +2 -2
  77. package/public/assets/{managed-agents-BjYBzVZt.js.map → managed-agents-Rk6ghKR9.js.map} +1 -1
  78. package/public/assets/{markdown-preview-impl-BnwjZsmO.js → markdown-preview-impl-NWMtJdYz.js} +2 -2
  79. package/public/assets/{markdown-preview-impl-BnwjZsmO.js.map → markdown-preview-impl-NWMtJdYz.js.map} +1 -1
  80. package/public/assets/memory-DamwWa9A.js +2 -0
  81. package/public/assets/{memory-SLycz2gC.js.map → memory-DamwWa9A.js.map} +1 -1
  82. package/public/assets/{messages-DfSCTe6N.js → messages--NU-YIL6.js} +2 -2
  83. package/public/assets/{messages-DfSCTe6N.js.map → messages--NU-YIL6.js.map} +1 -1
  84. package/public/assets/{orchestrators-BwxdTQm_.js → orchestrators-DQnIn6qx.js} +2 -2
  85. package/public/assets/{orchestrators-BwxdTQm_.js.map → orchestrators-DQnIn6qx.js.map} +1 -1
  86. package/public/assets/{overview-Djy3qkY5.js → overview-DCmNgXDf.js} +2 -2
  87. package/public/assets/{overview-Djy3qkY5.js.map → overview-DCmNgXDf.js.map} +1 -1
  88. package/public/assets/{pairs-9KNduLnC.js → pairs-DNELeC9P.js} +2 -2
  89. package/public/assets/{pairs-9KNduLnC.js.map → pairs-DNELeC9P.js.map} +1 -1
  90. package/public/assets/{react-dom-WI6TjVe7.js → react-dom-CrP_AtcL.js} +2 -2
  91. package/public/assets/{react-dom-WI6TjVe7.js.map → react-dom-CrP_AtcL.js.map} +1 -1
  92. package/public/assets/{security-tG3q7fFW.js → security-Dd3Kh0oC.js} +2 -2
  93. package/public/assets/{security-tG3q7fFW.js.map → security-Dd3Kh0oC.js.map} +1 -1
  94. package/public/assets/{select-CGUdMNOG.js → select-DQbeJZX0.js} +2 -2
  95. package/public/assets/{select-CGUdMNOG.js.map → select-DQbeJZX0.js.map} +1 -1
  96. package/public/assets/settings-BMAJrBIH.js +4 -0
  97. package/public/assets/settings-BMAJrBIH.js.map +1 -0
  98. package/public/assets/store-B8xP6z6T.js +9 -0
  99. package/public/assets/store-B8xP6z6T.js.map +1 -0
  100. package/public/assets/tasks-BYR4L9GP.js +2 -0
  101. package/public/assets/{tasks-DWlKg218.js.map → tasks-BYR4L9GP.js.map} +1 -1
  102. package/public/assets/teams-CoTifrq_.js +2 -0
  103. package/public/assets/{teams-DRchUP_p.js.map → teams-CoTifrq_.js.map} +1 -1
  104. package/public/assets/{terminal-viewer-impl-D_Sqe-vo.js → terminal-viewer-impl-B1YzfpE6.js} +3 -3
  105. package/public/assets/{terminal-viewer-impl-D_Sqe-vo.js.map → terminal-viewer-impl-B1YzfpE6.js.map} +1 -1
  106. package/public/assets/{use-keyboard-viewport-fOL_3RHB.js → use-keyboard-viewport-DrRH_QzG.js} +2 -2
  107. package/public/assets/{use-keyboard-viewport-fOL_3RHB.js.map → use-keyboard-viewport-DrRH_QzG.js.map} +1 -1
  108. package/public/assets/{work-queue-HNcyjON9.js → work-queue-x57vlgyZ.js} +2 -2
  109. package/public/assets/{work-queue-HNcyjON9.js.map → work-queue-x57vlgyZ.js.map} +1 -1
  110. package/public/assets/{workspaces-CicYE2Ax.js → workspaces-CYm5bLg9.js} +3 -3
  111. package/public/assets/{workspaces-CicYE2Ax.js.map → workspaces-CYm5bLg9.js.map} +1 -1
  112. package/public/index.html +22 -22
  113. package/runner/src/config.ts +66 -2
  114. package/src/automations.ts +3 -4
  115. package/src/config-store.ts +5 -6
  116. package/src/connectors.ts +5 -0
  117. package/src/db/migrations.ts +6 -0
  118. package/src/db/schema.ts +1 -1
  119. package/src/index.ts +5 -2
  120. package/src/mcp-provider-catalog.ts +9 -0
  121. package/src/mcp.ts +4 -4
  122. package/src/provider-catalog-store.ts +33 -4
  123. package/src/provider-config-store.ts +216 -0
  124. package/src/recipe-runner.ts +2 -1
  125. package/src/recipe-validator.ts +4 -3
  126. package/src/routes/agents-spawn.ts +2 -2
  127. package/src/routes/index.ts +12 -2
  128. package/src/routes/insights.ts +4 -3
  129. package/src/routes/orchestrator.ts +21 -1
  130. package/src/routes/provider-config.ts +89 -41
  131. package/src/routes/settings.ts +127 -0
  132. package/src/routes/steward.ts +5 -5
  133. package/src/routes/tokens.ts +32 -1
  134. package/src/security.ts +34 -37
  135. package/src/settings/modules.ts +149 -0
  136. package/src/settings/registry.ts +397 -0
  137. package/src/spawn-command.ts +5 -1
  138. package/src/team-template-config.ts +2 -2
  139. package/src/token-db.ts +20 -0
  140. package/public/assets/agents-D201CuTR.js +0 -2
  141. package/public/assets/branch-state-badge-BPXXLpQG.js +0 -2
  142. package/public/assets/card-BU6PvVHv.js +0 -2
  143. package/public/assets/chat-CHgPGHQD.js +0 -2
  144. package/public/assets/display-CketawEF.js +0 -3
  145. package/public/assets/dist-D41tzJYg.js +0 -2
  146. package/public/assets/dist-DUoZnlB2.js +0 -2
  147. package/public/assets/dist-De2oguBm.js +0 -2
  148. package/public/assets/index-D7js3XlH.js +0 -21
  149. package/public/assets/index-DaNOQJci.css +0 -2
  150. package/public/assets/lucide-react-H_-8G79A.js +0 -9
  151. package/public/assets/lucide-react-H_-8G79A.js.map +0 -1
  152. package/public/assets/memory-SLycz2gC.js +0 -2
  153. package/public/assets/settings-CGcqyJ5L.js +0 -10
  154. package/public/assets/settings-CGcqyJ5L.js.map +0 -1
  155. package/public/assets/store-DXdfhiVf.js +0 -9
  156. package/public/assets/store-DXdfhiVf.js.map +0 -1
  157. package/public/assets/tasks-DWlKg218.js +0 -2
  158. package/public/assets/teams-DRchUP_p.js +0 -2
@@ -1,6 +1,7 @@
1
1
  import type { MemoryType, Recipe, RecipeAgent, RecipeMemoryPolicy } from "./types";
2
2
  import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
3
3
  import { errMessage, isRecord } from "agent-relay-sdk";
4
+ import { effectiveProviderCatalogList } from "./provider-catalog-store";
4
5
 
5
6
  class RecipeValidationError extends Error {}
6
7
 
@@ -50,7 +51,7 @@ function validateAgent(value: unknown): RecipeAgent {
50
51
  const model = optionalString(value.model, "agent.model");
51
52
  const effort = optionalEffort(value.effort, "agent.effort");
52
53
  try {
53
- resolveProviderSelection({ provider, model, effort });
54
+ resolveProviderSelection({ provider, model, effort }, effectiveProviderCatalogList());
54
55
  } catch (error) {
55
56
  throw new RecipeValidationError(errMessage(error));
56
57
  }
@@ -103,8 +104,8 @@ function optionalString(value: unknown, field: string): string | undefined {
103
104
 
104
105
  function optionalEffort(value: unknown, field: string): ProviderEffort | undefined {
105
106
  if (value === undefined || value === null) return undefined;
106
- if (value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max") return value;
107
- throw new RecipeValidationError(`${field} must be low, medium, high, xhigh, or max`);
107
+ if (typeof value !== "string") throw new RecipeValidationError(`${field} must be a string`);
108
+ return value.trim() as ProviderEffort || undefined;
108
109
  }
109
110
 
110
111
  function optionalBoolean(value: unknown, field: string): boolean | undefined {
@@ -1,5 +1,5 @@
1
1
  // Auto-split from routes.ts (#299). Domain: agents-spawn.
2
- import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_EFFORTS, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
2
+ import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
3
3
  import { McpAuthError, McpNotFoundError } from "../mcp-errors";
4
4
  import { VALID_AGENT_STATUSES, error, json, parseBody, type Handler } from "./_shared";
5
5
  import { ValidationError, listAgents } from "../db";
@@ -109,7 +109,7 @@ export const postAgentSpawn: Handler = async (req) => {
109
109
  orchestratorId: cleanString(parsed.body.orchestratorId, "orchestratorId", { max: 200 }),
110
110
  cwd: cleanString(parsed.body.cwd, "cwd", { max: 500 }),
111
111
  model: cleanString(parsed.body.model, "model", { max: 120 }),
112
- effort: optionalEnum(parsed.body.effort, "effort", VALID_EFFORTS) as ProviderEffort | undefined,
112
+ effort: cleanString(parsed.body.effort, "effort", { max: 120 }) as ProviderEffort | undefined,
113
113
  approvalMode: optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined,
114
114
  workspaceMode: optionalEnum(parsed.body.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
115
115
  label: cleanString(parsed.body.label, "label", { max: 120 }),
@@ -4,14 +4,14 @@ import { deleteAgentProfileRoute, getAgentProfileRoute, getAgentProfilesRoute, p
4
4
  import { deleteAgentTerminalSession, postAgentAction, postAgentPermissionDecision, postAgentPrompt, postAgentTerminalSession } from "./agent-sessions";
5
5
  import { deleteArtifactById, getArtifactById, getArtifactContent, getArtifacts, headArtifactContent, postArtifact } from "./artifacts";
6
6
  import { deleteAutomationById, getAutomationById, getAutomationRuns, getAutomations, patchAutomation, postAutomation, postAutomationRun } from "./automations";
7
- import { deleteCommandById, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, postProviderConfigTestRoute, putProviderConfigRoute } from "./provider-config";
7
+ import { deleteCommandById, deleteProviderModelRoute, getProviderConfigRoute, getProviderConfigsRoute, getProvidersRoute, patchProviderModelRoute, postProviderConfigTestRoute, putProviderConfigRoute, putProviderModelRoute } from "./provider-config";
8
8
  import { deleteConfigKey, getConfigKey, getConfigKeyHistory, getConfigNamespace, putConfigKey } from "./config";
9
9
  import { deleteInboxDraftRoute, getChatHistoryImportsRoute, getInboxStateRoute, patchInboxThreadState, postChatHistoryImportRoute, putInboxDraft } from "./inbox";
10
10
  import { deleteMemoryRoute, getMemories, getMemoryBrokerInfo, getMemoryById, getMemoryStats, patchMemory, postMemory, postMemoryInject, postMemorySearch } from "./memory";
11
11
  import { deleteMessageById, getCursorRoute, getMessageById, getMessageDeliveryRoute, getMessageStatusRoute, getMessageThread, getMessages, getQueuedMessagesRoute, patchMessage, postClaimMessage, postMessage, postMessageDeliveryAction, postMessageDeliveryAttempt, postMessageReaction, postRenewMessageClaim, postSystemBroadcast } from "./messages";
12
12
  import { deleteOrchestratorById, getOrchestratorById, getOrchestrators, patchOrchestratorAgents, postOrchestrator, postOrchestratorAction, postOrchestratorHeartbeat } from "./orchestrator";
13
13
  import { deleteSpawnPolicyRoute, getSpawnPoliciesHealth, getSpawnPoliciesRoute, getSpawnPolicyHealth, getSpawnPolicyRoute, getSpawnPolicyStatus, postSpawnPolicyRestart, postSpawnPolicyStart, postSpawnPolicyStop, putSpawnPolicyRoute } from "./spawn-policy";
14
- import { deleteTokenProfileRoute, getTokenById, getTokenProfiles, getTokens, patchTokenProfile, postIntegrationRuntimeToken, postInteractiveRunnerRuntimeToken, postMcpRuntimeToken, postOrchestratorRunnerToken, postRuntimeTokenRenew, postToken, postTokenProfile, postTokenRevoke } from "./tokens";
14
+ import { deleteTokenProfileRoute, getTokenById, getTokenMe, getTokenProfiles, getTokens, patchTokenProfile, postIntegrationRuntimeToken, postInteractiveRunnerRuntimeToken, postMcpRuntimeToken, postOrchestratorRunnerToken, postRuntimeTokenRenew, postToken, postTokenProfile, postTokenRevoke } from "./tokens";
15
15
  import { deleteWorkspaceById, getWorkspaceById, getWorkspaceDiagnostics, getWorkspaceDiff, getWorkspaceGitState, getWorkspaceMergePreview, getWorkspaceOrphans, getWorkspaceStewards, getWorkspaces, postWorkspaceAction, postWorkspaceCleanupStale, postWorkspaceOrphanReclaim } from "./workspaces";
16
16
  import { error, type Handler } from "./_shared";
17
17
  import { getActivityEvents, postActivityEvent } from "./activity";
@@ -30,6 +30,7 @@ import { getPlanByIdRoute, getPlansRoute } from "./plans";
30
30
  import { getBlackboardEntryRoute, getBlackboardRoute } from "./blackboard";
31
31
  import { getProjectEntriesRoute, getProjectEntryRoute, getProjectRoute, getProjectsRoute } from "./projects";
32
32
  import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, getRecipeInstances, getRecipes, postRecipeInstanceArtifacts, postRecipeStart, postRecipeStop } from "./recipes";
33
+ import { getSettingRoute, getSettingsNamespaceRoute, getSettingsRoute, putSettingRoute, putSettingsDefaultRoute } from "./settings";
33
34
  import { getStewardConfigRoute, getWorkspaceConfigRoute, putStewardConfigRoute, putWorkspaceConfigRoute } from "./steward";
34
35
  import { getTaskById, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim } from "./tasks";
35
36
  import { getMyTeamRoute, getTeamOutcomesRoute, getTeamsRoute, postMyTeamDissolveRoute, putMyTeamBriefRoute } from "./teams";
@@ -168,6 +169,7 @@ const routes: Route[] = [
168
169
  route("GET", "/api/recipes/:name", getRecipeByName),
169
170
  route("GET", "/api/tokens", getTokens),
170
171
  route("POST", "/api/tokens", postToken),
172
+ route("GET", "/api/tokens/me", getTokenMe),
171
173
  route("POST", "/api/runtime-tokens/renew", postRuntimeTokenRenew),
172
174
  route("POST", "/api/runtime-tokens/interactive-runner", postInteractiveRunnerRuntimeToken),
173
175
  route("POST", "/api/runtime-tokens/mcp-client", postMcpRuntimeToken),
@@ -196,6 +198,11 @@ const routes: Route[] = [
196
198
  route("GET", "/api/insights/observations", getInsightsObservationsRoute),
197
199
  route("POST", "/api/insights/observations", postInsightsObservationRoute),
198
200
  route("POST", "/api/continuation-archives", postContinuationArchiveRoute),
201
+ route("GET", "/api/settings", getSettingsRoute),
202
+ route("GET", "/api/settings/:namespace", getSettingsNamespaceRoute),
203
+ route("GET", "/api/settings/:namespace/:key", getSettingRoute),
204
+ route("PUT", "/api/settings/:namespace", putSettingsDefaultRoute),
205
+ route("PUT", "/api/settings/:namespace/:key", putSettingRoute),
199
206
  route("GET", "/api/config/:namespace", getConfigNamespace),
200
207
  route("GET", "/api/config/:namespace/:key/history", getConfigKeyHistory),
201
208
  route("GET", "/api/config/:namespace/:key", getConfigKey),
@@ -213,6 +220,9 @@ const routes: Route[] = [
213
220
  route("GET", "/api/spawn-policy/:name/health", getSpawnPolicyHealth),
214
221
  route("GET", "/api/providers", getProvidersRoute),
215
222
  route("GET", "/api/providers/config", getProviderConfigsRoute),
223
+ route("PUT", "/api/providers/:provider/models/:alias", putProviderModelRoute),
224
+ route("PATCH", "/api/providers/:provider/models/:alias", patchProviderModelRoute),
225
+ route("DELETE", "/api/providers/:provider/models/:alias", deleteProviderModelRoute),
216
226
  route("GET", "/api/providers/:provider/config", getProviderConfigRoute),
217
227
  route("PUT", "/api/providers/:provider/config", putProviderConfigRoute),
218
228
  route("POST", "/api/providers/:provider/config/test", postProviderConfigTestRoute),
@@ -3,11 +3,12 @@ import { ValidationError } from "../db";
3
3
  import { cleanString } from "../validation";
4
4
  import { emitConfigChanged } from "../sse";
5
5
  import { error, json, parseBody, type Handler } from "./_shared";
6
- import { getInsightsConfigEntry, setInsightsConfig } from "../config-store";
6
+ import { getInsightsConfigEntry } from "../config-store";
7
7
  import { getObservationStats, listObservationProjects, listObservations, recordObservation } from "../insights-db";
8
+ import { getRegisteredSettingEntry, setRegisteredSettingValue } from "./settings";
8
9
  import { isRecord } from "agent-relay-sdk";
9
10
 
10
- export const getInsightsConfigRoute: Handler = () => json(getInsightsConfigEntry());
11
+ export const getInsightsConfigRoute: Handler = () => json(getRegisteredSettingEntry("insights", "default"));
11
12
 
12
13
  export const putInsightsConfigRoute: Handler = async (req) => {
13
14
  const parsed = await parseBody<unknown>(req);
@@ -17,7 +18,7 @@ export const putInsightsConfigRoute: Handler = async (req) => {
17
18
  ? parsed.body.value
18
19
  : parsed.body;
19
20
  const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
20
- const entry = setInsightsConfig(value, updatedBy);
21
+ const entry = setRegisteredSettingValue("insights", "default", value, updatedBy);
21
22
  emitConfigChanged(entry.namespace, entry.key, entry.version);
22
23
  return json(entry, entry.version === 1 ? 201 : 200);
23
24
  } catch (e) {
@@ -1,6 +1,7 @@
1
1
  // Auto-split from routes.ts (#299). Domain: orchestrator.
2
2
  import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
3
3
  import { CONTRACT_VERSIONS, parseRuntimeCapabilities, parseRuntimeContracts, parseRuntimePackage, type RuntimeCapabilities, type RuntimeContracts, type RuntimePackageMetadata } from "../contracts";
4
+ import { notifyAgentOffline } from "../agent-lifecycle-events";
4
5
  import { VERSION } from "../config";
5
6
  import { ValidationError, deleteOrchestrator, getOrchestrator, listOrchestrators, orchestratorHeartbeat, recordAgentExitDiagnostics, setOrchestratorUpgradeState, updateManagedAgents, upsertOrchestrator } from "../db";
6
7
  import { auditEvent, authAuditMetadata, authorizeRoute, cleanJsonArray, cleanSafeNumber, dashboardAttribution, emitCommand, error, isRootCredentialRequest, json, parseBody, spawnRequestId, type Handler } from "./_shared";
@@ -201,7 +202,15 @@ export const patchOrchestratorAgents: Handler = async (req, params) => {
201
202
  const updated = updateManagedAgents(params.id!, cleaned);
202
203
  for (const exited of exitedAgents) {
203
204
  const agent = recordAgentExitDiagnostics(exited.agentId, exited);
204
- if (agent) emitAgentStatus(agent.id);
205
+ if (agent) {
206
+ emitAgentStatus(agent.id);
207
+ // #466 — fast exit path must wake the spawn-parent. recordAgentExitDiagnostics already
208
+ // flipped the agent offline (above), so the stale-agent-reaper will structurally skip it
209
+ // (its WHERE excludes status='offline'), guaranteeing a single notifyAgentOffline emission.
210
+ // notifyAgentOffline no-ops for non-spawned agents. Only fire when we actually recorded the
211
+ // exit (agent !== null) so an already-offline row can't double-notify.
212
+ notifyAgentOffline(exited.agentId, exitReason(exited));
213
+ }
205
214
  auditEvent({
206
215
  clientId: [
207
216
  "managed-session-exit",
@@ -248,6 +257,17 @@ export const patchOrchestratorAgents: Handler = async (req, params) => {
248
257
  }
249
258
  };
250
259
 
260
+ // #466 — derive a human-meaningful offline cause for the spawn-parent notification from the exit
261
+ // diagnostics, in preference to the reaper's generic "heartbeat lost". Prefer the captured
262
+ // lastError, then the systemd result (e.g. "success", "signal"), else a plain fallback.
263
+ function exitReason(exited: ManagedSessionExitDiagnostics): string {
264
+ const lastError = exited.lastError?.trim();
265
+ if (lastError) return lastError;
266
+ const result = exited.systemd?.result?.trim();
267
+ if (result) return `exited (${result})`;
268
+ return "exited";
269
+ }
270
+
251
271
  function cleanManagedSessionExitDiagnostics(value: unknown, index: number): ManagedSessionExitDiagnostics {
252
272
  if (!isRecord(value)) throw new ValidationError(`exitedAgents[${index}] must be an object`);
253
273
  const provider = optionalEnum(value.provider, `exitedAgents[${index}].provider`, SPAWN_PROVIDERS);
@@ -2,14 +2,13 @@
2
2
  import { ValidationError, listOrchestrators } from "../db";
3
3
  import { applyCommandToRecipe } from "../recipe-runner";
4
4
  import { cleanString, cleanStringArray, optionalEnum } from "../validation";
5
- import { defaultProviderConfig, loadProviderConfig, providerConfigPublic, writeProviderConfig } from "../../runner/src/config";
6
5
  import { deleteCommand, getCommand } from "../commands-db";
7
- import { effectiveProviderCatalogList } from "../provider-catalog-store";
6
+ import { effectiveProviderCatalogList, removeProviderModelOverride, setProviderModelDisabled, upsertProviderModelOverride } from "../provider-catalog-store";
8
7
  import { emitCommand, error, json, parseBody, type Handler } from "./_shared";
9
- import { isRecord } from "agent-relay-sdk";
10
- import { type ProviderConfig } from "../../runner/src/adapter";
11
-
12
- const VALID_PROVIDER_CONFIGS = ["claude", "codex"] as const;
8
+ import { getProviderConfigEntry, listProviderConfigEntries, migrateLocalProviderConfigs, providerConfigPublic, setProviderConfig, VALID_PROVIDER_CONFIGS } from "../provider-config-store";
9
+ import { emitConfigChanged } from "../sse";
10
+ import { isRecord, type SpawnProvider } from "agent-relay-sdk";
11
+ import type { ProviderModelCatalogEntry } from "agent-relay-sdk/provider-catalog";
13
12
 
14
13
  export const getProvidersRoute: Handler = () => {
15
14
  return json({
@@ -25,17 +24,72 @@ export const getProvidersRoute: Handler = () => {
25
24
  });
26
25
  };
27
26
 
28
- export const getProviderConfigsRoute: Handler = () => {
27
+ export const getProviderConfigsRoute: Handler = (req) => {
28
+ const host = new URL(req.url).searchParams.get("host") ?? undefined;
29
+ migrateLocalProviderConfigs();
30
+ const entries = listProviderConfigEntries(host);
29
31
  return json(Object.fromEntries(VALID_PROVIDER_CONFIGS.map((provider) => {
30
- const config = loadProviderConfig(provider);
31
- return [provider, providerConfigPublic(config)];
32
+ return [provider, providerConfigPublic(entries[provider])];
32
33
  })));
33
34
  };
34
35
 
36
+ export const putProviderModelRoute: Handler = async (req, params) => {
37
+ const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS) as SpawnProvider | undefined;
38
+ if (!provider) return error("provider required");
39
+ const alias = cleanString(params.alias, "alias", { required: true, max: 120 })!;
40
+ const parsed = await parseBody<unknown>(req);
41
+ if (!parsed.ok) return error(parsed.error, parsed.status);
42
+ try {
43
+ if (!isRecord(parsed.body)) return error("model body must be an object");
44
+ const entry = cleanProviderModelEntry(alias, parsed.body);
45
+ return json(upsertProviderModelOverride({
46
+ provider,
47
+ alias,
48
+ entry,
49
+ disabled: optionalBoolean(parsed.body.disabled, "disabled"),
50
+ updatedBy: cleanString(parsed.body.updatedBy, "updatedBy", { max: 120 }),
51
+ }));
52
+ } catch (e) {
53
+ if (e instanceof ValidationError) return error(e.message, 400);
54
+ throw e;
55
+ }
56
+ };
57
+
58
+ export const patchProviderModelRoute: Handler = async (req, params) => {
59
+ const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS) as SpawnProvider | undefined;
60
+ if (!provider) return error("provider required");
61
+ const alias = cleanString(params.alias, "alias", { required: true, max: 120 })!;
62
+ const parsed = await parseBody<unknown>(req);
63
+ if (!parsed.ok) return error(parsed.error, parsed.status);
64
+ try {
65
+ if (!isRecord(parsed.body)) return error("model patch body must be an object");
66
+ if (parsed.body.disabled === undefined) throw new ValidationError("disabled required");
67
+ return json(setProviderModelDisabled(provider, alias, optionalBoolean(parsed.body.disabled, "disabled") ?? false, cleanString(parsed.body.updatedBy, "updatedBy", { max: 120 })));
68
+ } catch (e) {
69
+ if (e instanceof ValidationError) return error(e.message, 400);
70
+ throw e;
71
+ }
72
+ };
73
+
74
+ export const deleteProviderModelRoute: Handler = (_req, params) => {
75
+ const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS) as SpawnProvider | undefined;
76
+ if (!provider) return error("provider required");
77
+ try {
78
+ const alias = cleanString(params.alias, "alias", { required: true, max: 120 })!;
79
+ return json(removeProviderModelOverride(provider, alias));
80
+ } catch (e) {
81
+ if (e instanceof ValidationError) return error(e.message, 400);
82
+ throw e;
83
+ }
84
+ };
85
+
35
86
  export const getProviderConfigRoute: Handler = (_req, params) => {
36
87
  const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
37
88
  if (!provider) return error("provider required");
38
- return json(providerConfigPublic(loadProviderConfig(provider)));
89
+ const host = new URL(_req.url).searchParams.get("host") ?? undefined;
90
+ const includeSecrets = new URL(_req.url).searchParams.get("includeSecrets") === "1";
91
+ migrateLocalProviderConfigs();
92
+ return json(providerConfigPublic(getProviderConfigEntry(provider, host), { maskEnv: !includeSecrets }));
39
93
  };
40
94
 
41
95
  export const putProviderConfigRoute: Handler = async (req, params) => {
@@ -45,23 +99,11 @@ export const putProviderConfigRoute: Handler = async (req, params) => {
45
99
  if (!parsed.ok) return error(parsed.error, parsed.status);
46
100
  try {
47
101
  if (!isRecord(parsed.body)) return error("provider config body must be an object");
48
- const defaults = defaultProviderConfig(provider);
49
- const headless = isRecord(parsed.body.headless) ? parsed.body.headless : {};
50
- const config: ProviderConfig = {
51
- command: cleanString(parsed.body.command, "command", { required: true, max: 500 })!,
52
- defaultArgs: cleanStringArray(parsed.body.defaultArgs, "defaultArgs", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultArgs,
53
- env: cleanEnvRecord(parsed.body.env),
54
- pluginDirs: cleanStringArray(parsed.body.pluginDirs, "pluginDirs", { itemMax: 80, maxItems: 50 }) ?? defaults.pluginDirs,
55
- defaultCapabilities: cleanStringArray(parsed.body.defaultCapabilities, "defaultCapabilities", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultCapabilities,
56
- defaultApprovalMode: cleanString(parsed.body.defaultApprovalMode, "defaultApprovalMode", { max: 80 }) ?? defaults.defaultApprovalMode,
57
- defaultTags: cleanStringArray(parsed.body.defaultTags, "defaultTags", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultTags,
58
- chatCaptureMode: (optionalEnum(parsed.body.chatCaptureMode, "chatCaptureMode", ["final", "full"]) ?? defaults.chatCaptureMode) as "final" | "full",
59
- headless: {
60
- tmuxPrefix: cleanString(headless.tmuxPrefix, "headless.tmuxPrefix", { max: 120 }) ?? defaults.headless.tmuxPrefix,
61
- shutdownTimeoutMs: cleanPositiveInt(headless.shutdownTimeoutMs, "headless.shutdownTimeoutMs") ?? defaults.headless.shutdownTimeoutMs,
62
- },
63
- };
64
- return json(providerConfigPublic(writeProviderConfig(provider, config)));
102
+ const host = new URL(req.url).searchParams.get("host") ?? undefined;
103
+ const updatedBy = cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 });
104
+ const entry = setProviderConfig(provider, parsed.body, { host, updatedBy });
105
+ emitConfigChanged(entry.namespace, entry.key, entry.version);
106
+ return json(providerConfigPublic(entry), entry.version === 1 ? 201 : 200);
65
107
  } catch (e) {
66
108
  if (e instanceof ValidationError) return error(e.message, 400);
67
109
  throw e;
@@ -71,7 +113,8 @@ export const putProviderConfigRoute: Handler = async (req, params) => {
71
113
  export const postProviderConfigTestRoute: Handler = async (_req, params) => {
72
114
  const provider = optionalEnum(params.provider, "provider", VALID_PROVIDER_CONFIGS);
73
115
  if (!provider) return error("provider required");
74
- const config = loadProviderConfig(provider);
116
+ const host = new URL(_req.url).searchParams.get("host") ?? undefined;
117
+ const config = getProviderConfigEntry(provider, host).value;
75
118
  const proc = Bun.spawn(["bash", "-lc", `command -v "$1"`, "bash", config.command], {
76
119
  stdout: "pipe",
77
120
  stderr: "pipe",
@@ -82,22 +125,27 @@ export const postProviderConfigTestRoute: Handler = async (_req, params) => {
82
125
  new Response(proc.stderr).text(),
83
126
  ]);
84
127
  return json({ ok: exitCode === 0, command: config.command, path: stdout.trim(), error: stderr.trim() }, exitCode === 0 ? 200 : 422);
85
- };
128
+ }
86
129
 
87
- function cleanEnvRecord(value: unknown): Record<string, string> {
88
- if (value === undefined) return {};
89
- if (!isRecord(value)) throw new ValidationError("env must be an object");
90
- const env: Record<string, string> = {};
91
- for (const [key, item] of Object.entries(value)) {
92
- if (typeof item !== "string") throw new ValidationError(`env.${key} must be a string`);
93
- env[key] = item;
94
- }
95
- return env;
130
+ function cleanProviderModelEntry(alias: string, body: Record<string, unknown>): ProviderModelCatalogEntry {
131
+ const source = isRecord(body.entry) ? body.entry : body;
132
+ const efforts = cleanStringArray(source.efforts, "efforts", { itemMax: 120, maxItems: 50 }) ?? [];
133
+ const defaultEffort = cleanString(source.defaultEffort, "defaultEffort", { max: 120 });
134
+ if (defaultEffort && !efforts.includes(defaultEffort)) throw new ValidationError("defaultEffort must be included in efforts");
135
+ return {
136
+ alias,
137
+ label: cleanString(source.label, "label", { required: true, max: 120 })!,
138
+ providerModel: cleanString(source.providerModel, "providerModel", { required: true, max: 160 })!,
139
+ efforts,
140
+ ...(defaultEffort ? { defaultEffort } : {}),
141
+ ...(isRecord(source.limits) ? { limits: source.limits as ProviderModelCatalogEntry["limits"] } : {}),
142
+ ...(isRecord(source.capabilities) ? { capabilities: source.capabilities as unknown as ProviderModelCatalogEntry["capabilities"] } : {}),
143
+ };
96
144
  }
97
145
 
98
- function cleanPositiveInt(value: unknown, field: string): number | undefined {
99
- if (value === undefined) return undefined;
100
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new ValidationError(`${field} must be a positive integer`);
146
+ function optionalBoolean(value: unknown, field: string): boolean | undefined {
147
+ if (value === undefined || value === null) return undefined;
148
+ if (typeof value !== "boolean") throw new ValidationError(`${field} must be a boolean`);
101
149
  return value;
102
150
  }
103
151
 
@@ -0,0 +1,127 @@
1
+ // Auto-split from routes.ts (#299). Domain: schema-driven settings.
2
+ import { ValidationError } from "../db";
3
+ import { getConfig, setConfig } from "../config-store";
4
+ import { ensureProviderConfigDescriptors, migrateLocalProviderConfigs } from "../provider-config-store";
5
+ import { getSettingDescriptor, listSettingDescriptors, validateSettingValue, PROVIDER_CONFIG_NAMESPACE } from "../settings/registry";
6
+ import { isRequestAuthorizedFor } from "../security";
7
+ import { cleanString } from "../validation";
8
+ import { emitConfigChanged } from "../sse";
9
+ import { error, json, normalizeConfigPathParam, parseBody, type Handler } from "./_shared";
10
+ import { isRecord, type ConfigEntry, type SettingDescriptor, type SettingEntry } from "agent-relay-sdk";
11
+
12
+ type SettingEnvelope = SettingEntry;
13
+
14
+ export const getSettingsRoute: Handler = (req) => {
15
+ migrateLocalProviderConfigs();
16
+ ensureProviderConfigDescriptors();
17
+ return json({ settings: listVisibleSettings(req) });
18
+ };
19
+
20
+ export const getSettingsNamespaceRoute: Handler = (req, params) => {
21
+ const namespace = normalizeConfigPathParam(params.namespace, "namespace");
22
+ migrateLocalProviderConfigs();
23
+ ensureProviderConfigDescriptors();
24
+ return json({ settings: listVisibleSettings(req).filter((setting) => setting.namespace === namespace) });
25
+ };
26
+
27
+ export const getSettingRoute: Handler = (req, params) => {
28
+ const namespace = normalizeConfigPathParam(params.namespace, "namespace");
29
+ const key = normalizeConfigPathParam(params.key, "key");
30
+ migrateLocalProviderConfigs();
31
+ ensureProviderConfigDescriptors(namespace === PROVIDER_CONFIG_NAMESPACE ? [key] : []);
32
+ const descriptor = getSettingDescriptor(namespace, key);
33
+ if (!descriptor) return error("setting not found", 404);
34
+ const readable = canAccessSetting(req, descriptor.scope.read);
35
+ if (!readable) return error("forbidden", 403);
36
+ return json(settingEnvelope(req, descriptor));
37
+ };
38
+
39
+ export const putSettingsDefaultRoute: Handler = (req, params) => putSetting(req, params.namespace, "default");
40
+ export const putSettingRoute: Handler = (req, params) => putSetting(req, params.namespace, params.key);
41
+
42
+ export function getRegisteredSettingEntry(namespace: string, key: string): ConfigEntry | null {
43
+ ensureProviderConfigDescriptors(namespace === PROVIDER_CONFIG_NAMESPACE ? [key] : []);
44
+ const descriptor = getSettingDescriptor(namespace, key);
45
+ return descriptor ? currentSettingEntry(descriptor) : null;
46
+ }
47
+
48
+ export function setRegisteredSettingValue(namespace: string, key: string, value: unknown, updatedBy?: string): ConfigEntry {
49
+ ensureProviderConfigDescriptors(namespace === PROVIDER_CONFIG_NAMESPACE ? [key] : []);
50
+ const descriptor = getSettingDescriptor(namespace, key);
51
+ if (!descriptor) throw new ValidationError("setting not found");
52
+ const merged = mergeSettingDefaults(descriptor.defaults, value);
53
+ const validation = validateSettingValue(namespace, key, merged);
54
+ if (!validation.valid) throw new ValidationError(validation.errors.join("; "));
55
+ return setConfig(namespace, key, merged, updatedBy);
56
+ }
57
+
58
+ async function putSetting(req: Request, namespaceRaw: string | undefined, keyRaw: string | undefined): Promise<Response> {
59
+ const namespace = normalizeConfigPathParam(namespaceRaw, "namespace");
60
+ const key = normalizeConfigPathParam(keyRaw, "key");
61
+ const parsed = await parseBody<unknown>(req);
62
+ if (!parsed.ok) return error(parsed.error, parsed.status);
63
+ if (!isRecord(parsed.body)) return error("JSON object body required", 400);
64
+ if (!Object.prototype.hasOwnProperty.call(parsed.body, "value")) return error("value required", 400);
65
+
66
+ try {
67
+ migrateLocalProviderConfigs();
68
+ ensureProviderConfigDescriptors(namespace === PROVIDER_CONFIG_NAMESPACE ? [key] : []);
69
+ const descriptor = getSettingDescriptor(namespace, key);
70
+ if (!descriptor) return error("setting not found", 404);
71
+ if (descriptor.tier !== "server-runtime") return error("setting is read-only", 403);
72
+ if (!canAccessSetting(req, descriptor.scope.write)) return error("forbidden", 403);
73
+ const updatedBy = cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 });
74
+ const validation = validateSettingValue(namespace, key, parsed.body.value);
75
+ if (!validation.valid) return json({ error: "invalid setting value", details: validation.errors }, 400);
76
+ const entry = setConfig(namespace, key, parsed.body.value, updatedBy);
77
+ emitConfigChanged(entry.namespace, entry.key, entry.version);
78
+ return json(settingEnvelope(req, descriptor, entry), entry.version === 1 ? 201 : 200);
79
+ } catch (e) {
80
+ if (e instanceof ValidationError) return error(e.message, 400);
81
+ throw e;
82
+ }
83
+ }
84
+
85
+ function listVisibleSettings(req: Request): SettingEnvelope[] {
86
+ return listSettingDescriptors()
87
+ .map((descriptor) => settingEnvelope(req, descriptor))
88
+ .filter((setting) => setting.readable);
89
+ }
90
+
91
+ function settingEnvelope(req: Request, descriptor: SettingDescriptor, entry = currentSettingEntry(descriptor)): SettingEnvelope {
92
+ return {
93
+ ...descriptor,
94
+ value: entry.value,
95
+ version: entry.version,
96
+ updatedAt: entry.updatedAt,
97
+ updatedBy: entry.updatedBy,
98
+ readable: canAccessSetting(req, descriptor.scope.read),
99
+ writable: descriptor.tier === "server-runtime" && canAccessSetting(req, descriptor.scope.write),
100
+ };
101
+ }
102
+
103
+ function currentSettingEntry(descriptor: SettingDescriptor): ConfigEntry {
104
+ const entry = getConfig(descriptor.namespace, descriptor.key);
105
+ if (entry) return entry;
106
+ return {
107
+ namespace: descriptor.namespace,
108
+ key: descriptor.key,
109
+ value: descriptor.defaults,
110
+ version: 0,
111
+ updatedAt: "default",
112
+ updatedBy: "system",
113
+ };
114
+ }
115
+
116
+ function canAccessSetting(req: Request, scope: string): boolean {
117
+ return isRequestAuthorizedFor(req, { scope });
118
+ }
119
+
120
+ function mergeSettingDefaults(defaults: unknown, value: unknown): unknown {
121
+ if (!isRecord(defaults) || !isRecord(value)) return value;
122
+ const merged: Record<string, unknown> = { ...defaults };
123
+ for (const [key, item] of Object.entries(value)) {
124
+ merged[key] = mergeSettingDefaults(defaults[key], item);
125
+ }
126
+ return merged;
127
+ }
@@ -3,10 +3,10 @@ import { ValidationError } from "../db";
3
3
  import { cleanString } from "../validation";
4
4
  import { emitConfigChanged } from "../sse";
5
5
  import { error, json, parseBody, type Handler } from "./_shared";
6
- import { getStewardConfigEntry, getWorkspaceConfigEntry, setStewardConfig, setWorkspaceConfig } from "../config-store";
6
+ import { getRegisteredSettingEntry, setRegisteredSettingValue } from "./settings";
7
7
  import { isRecord } from "agent-relay-sdk";
8
8
 
9
- export const getStewardConfigRoute: Handler = () => json(getStewardConfigEntry());
9
+ export const getStewardConfigRoute: Handler = () => json(getRegisteredSettingEntry("steward", "default"));
10
10
 
11
11
  export const putStewardConfigRoute: Handler = async (req) => {
12
12
  const parsed = await parseBody<unknown>(req);
@@ -16,7 +16,7 @@ export const putStewardConfigRoute: Handler = async (req) => {
16
16
  ? parsed.body.value
17
17
  : parsed.body;
18
18
  const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
19
- const entry = setStewardConfig(value, updatedBy);
19
+ const entry = setRegisteredSettingValue("steward", "default", value, updatedBy);
20
20
  emitConfigChanged(entry.namespace, entry.key, entry.version);
21
21
  return json(entry, entry.version === 1 ? 201 : 200);
22
22
  } catch (e) {
@@ -25,7 +25,7 @@ export const putStewardConfigRoute: Handler = async (req) => {
25
25
  }
26
26
  };
27
27
 
28
- export const getWorkspaceConfigRoute: Handler = () => json(getWorkspaceConfigEntry());
28
+ export const getWorkspaceConfigRoute: Handler = () => json(getRegisteredSettingEntry("workspace", "default"));
29
29
 
30
30
  export const putWorkspaceConfigRoute: Handler = async (req) => {
31
31
  const parsed = await parseBody<unknown>(req);
@@ -35,7 +35,7 @@ export const putWorkspaceConfigRoute: Handler = async (req) => {
35
35
  ? parsed.body.value
36
36
  : parsed.body;
37
37
  const updatedBy = isRecord(parsed.body) ? cleanString(parsed.body.updatedBy, "updatedBy", { max: 200 }) : undefined;
38
- const entry = setWorkspaceConfig(value, updatedBy);
38
+ const entry = setRegisteredSettingValue("workspace", "default", value, updatedBy);
39
39
  emitConfigChanged(entry.namespace, entry.key, entry.version);
40
40
  return json(entry, entry.version === 1 ? 201 : 200);
41
41
  } catch (e) {
@@ -4,12 +4,43 @@ import { ValidationError, getOrchestrator, upsertIntegrationRegistry } from "../
4
4
  import { auditEvent, authAuditMetadata, authorizeRoute, error, isRootCredentialRequest, json, parseBody, type Handler } from "./_shared";
5
5
  import { cleanString, cleanStringArray, cleanTokenConstraints, optionalEnum } from "../validation";
6
6
  import { createToken, deleteTokenProfile, getToken, getTokenProfile, listTokenProfiles, listTokens, renewToken, revokeToken, updateTokenProfile, upsertTokenProfile } from "../token-db";
7
- import { getComponentAuth } from "../security";
7
+ import { getComponentAuth, getIntegrationAuth, isAuthorized } from "../security";
8
8
  import { issueIntegrationRuntimeToken, issueInteractiveRunnerRuntimeToken, issueMcpRuntimeToken, reissueRunnerRuntimeToken } from "../runtime-tokens";
9
9
  import { type SpawnProvider } from "../types";
10
10
 
11
11
  export const getTokens: Handler = () => json(listTokens());
12
12
 
13
+ export const getTokenMe: Handler = (req) => {
14
+ const component = getComponentAuth(req);
15
+ if (component) {
16
+ return json({
17
+ actor: component.sub,
18
+ kind: "component",
19
+ role: component.role,
20
+ scopes: component.scope,
21
+ constraints: component.constraints,
22
+ jti: component.jti,
23
+ exp: component.exp,
24
+ });
25
+ }
26
+
27
+ const integration = getIntegrationAuth(req);
28
+ if (integration) {
29
+ return json({
30
+ actor: integration.name,
31
+ kind: "integration",
32
+ scopes: integration.scopes,
33
+ targets: integration.targets,
34
+ channels: integration.channels,
35
+ });
36
+ }
37
+
38
+ if (isAuthorized(req)) {
39
+ return json({ actor: "server", kind: "server", scopes: ["*"] });
40
+ }
41
+ return error("unauthorized", 401);
42
+ };
43
+
13
44
  function cleanTtlSeconds(value: unknown): number | undefined {
14
45
  if (value === undefined || value === null || value === "") return undefined;
15
46
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {