agent-relay-server 0.60.0 → 0.61.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 (172) hide show
  1. package/docs/openapi.json +516 -1
  2. package/package.json +3 -2
  3. package/public/assets/MessageBubble-CX63ZV4X.js +2 -0
  4. package/public/assets/MessageBubble-CX63ZV4X.js.map +1 -0
  5. package/public/assets/{PlanGraphPanel-CXix1C-H.js → PlanGraphPanel-Dby7wq9c.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-CXix1C-H.js.map → PlanGraphPanel-Dby7wq9c.js.map} +1 -1
  7. package/public/assets/{activity-CRafQ1Je.js → activity-2xPOScu_.js} +2 -2
  8. package/public/assets/{activity-CRafQ1Je.js.map → activity-2xPOScu_.js.map} +1 -1
  9. package/public/assets/{agent-profiles-DW4o5-ND.js → agent-profiles-BfGTTbeY.js} +2 -2
  10. package/public/assets/{agent-profiles-DW4o5-ND.js.map → agent-profiles-BfGTTbeY.js.map} +1 -1
  11. package/public/assets/agents-Bg3WOBRV.js +2 -0
  12. package/public/assets/{agents-Cbegv0Hm.js.map → agents-Bg3WOBRV.js.map} +1 -1
  13. package/public/assets/{analytics-DX6T7JX3.js → analytics-De0cEZGx.js} +3 -3
  14. package/public/assets/{analytics-DX6T7JX3.js.map → analytics-De0cEZGx.js.map} +1 -1
  15. package/public/assets/{automation-CzSBv4gg.js → automation-CHxhzq2t.js} +2 -2
  16. package/public/assets/{automation-CzSBv4gg.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-CRA50r81.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-B1MQCPCT.js → channels-BG0Nib7y.js} +2 -2
  26. package/public/assets/{channels-B1MQCPCT.js.map → channels-BG0Nib7y.js.map} +1 -1
  27. package/public/assets/chat-CjP_crSl.js +2 -0
  28. package/public/assets/{chat-BqIaFYGJ.js.map → chat-CjP_crSl.js.map} +1 -1
  29. package/public/assets/{connectors-C9aFJIrh.js → connectors-DWh7MwV_.js} +2 -2
  30. package/public/assets/{connectors-C9aFJIrh.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-DOXH7oDF.js → formatted-body-impl-u0W_hWYA.js} +2 -2
  60. package/public/assets/{formatted-body-impl-DOXH7oDF.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-br_Y6Cvt.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-B1edq5lk.js → insights-CAF9Hi5D.js} +2 -2
  67. package/public/assets/{insights-B1edq5lk.js.map → insights-CAF9Hi5D.js.map} +1 -1
  68. package/public/assets/{integrations-Cm4fjp7_.js → integrations-CG9qyHUf.js} +2 -2
  69. package/public/assets/{integrations-Cm4fjp7_.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-D6zvw0fU.js → maintenance-DgHA35ve.js} +2 -2
  75. package/public/assets/{maintenance-D6zvw0fU.js.map → maintenance-DgHA35ve.js.map} +1 -1
  76. package/public/assets/{managed-agents-CzfEznM1.js → managed-agents-Rk6ghKR9.js} +2 -2
  77. package/public/assets/{managed-agents-CzfEznM1.js.map → managed-agents-Rk6ghKR9.js.map} +1 -1
  78. package/public/assets/{markdown-preview-impl-DZlCaTHQ.js → markdown-preview-impl-NWMtJdYz.js} +2 -2
  79. package/public/assets/{markdown-preview-impl-DZlCaTHQ.js.map → markdown-preview-impl-NWMtJdYz.js.map} +1 -1
  80. package/public/assets/memory-DamwWa9A.js +2 -0
  81. package/public/assets/{memory-CUUAzJDc.js.map → memory-DamwWa9A.js.map} +1 -1
  82. package/public/assets/{messages-6QZH46LG.js → messages--NU-YIL6.js} +2 -2
  83. package/public/assets/{messages-6QZH46LG.js.map → messages--NU-YIL6.js.map} +1 -1
  84. package/public/assets/{orchestrators-CrcqkPuc.js → orchestrators-DQnIn6qx.js} +2 -2
  85. package/public/assets/{orchestrators-CrcqkPuc.js.map → orchestrators-DQnIn6qx.js.map} +1 -1
  86. package/public/assets/{overview-418xXZO_.js → overview-DCmNgXDf.js} +2 -2
  87. package/public/assets/{overview-418xXZO_.js.map → overview-DCmNgXDf.js.map} +1 -1
  88. package/public/assets/{pairs-CnedATgT.js → pairs-DNELeC9P.js} +2 -2
  89. package/public/assets/{pairs-CnedATgT.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-DS13pbag.js → security-Dd3Kh0oC.js} +2 -2
  93. package/public/assets/{security-DS13pbag.js.map → security-Dd3Kh0oC.js.map} +1 -1
  94. package/public/assets/{select-BOprGZDS.js → select-DQbeJZX0.js} +2 -2
  95. package/public/assets/{select-BOprGZDS.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-B-4oCPuv.js.map → tasks-BYR4L9GP.js.map} +1 -1
  102. package/public/assets/teams-CoTifrq_.js +2 -0
  103. package/public/assets/{teams-sCQAs_QA.js.map → teams-CoTifrq_.js.map} +1 -1
  104. package/public/assets/{terminal-viewer-impl-BhqS2XpS.js → terminal-viewer-impl-B1YzfpE6.js} +3 -3
  105. package/public/assets/{terminal-viewer-impl-BhqS2XpS.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-BDhjlwUH.js → work-queue-x57vlgyZ.js} +2 -2
  109. package/public/assets/{work-queue-BDhjlwUH.js.map → work-queue-x57vlgyZ.js.map} +1 -1
  110. package/public/assets/{workspaces-6I-C69nf.js → workspaces-CYm5bLg9.js} +3 -3
  111. package/public/assets/{workspaces-6I-C69nf.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/cli/index.ts +11 -0
  116. package/src/cli/merges.ts +89 -0
  117. package/src/config-store.ts +5 -6
  118. package/src/config.ts +4 -0
  119. package/src/connectors.ts +5 -0
  120. package/src/db/merge-lease.ts +95 -1
  121. package/src/db/migrations.ts +21 -0
  122. package/src/db/schema.ts +1 -1
  123. package/src/index.ts +5 -2
  124. package/src/maintenance.ts +8 -8
  125. package/src/mcp-merge-tools.ts +66 -0
  126. package/src/mcp-provider-catalog.ts +9 -0
  127. package/src/mcp.ts +7 -8
  128. package/src/merge-pause-maintenance.ts +13 -0
  129. package/src/provider-catalog-store.ts +33 -4
  130. package/src/provider-config-store.ts +216 -0
  131. package/src/recipe-runner.ts +2 -1
  132. package/src/recipe-validator.ts +4 -3
  133. package/src/routes/agent-sessions.ts +4 -2
  134. package/src/routes/agents-spawn.ts +2 -2
  135. package/src/routes/commands.ts +20 -0
  136. package/src/routes/index.ts +19 -2
  137. package/src/routes/insights.ts +4 -3
  138. package/src/routes/merges.ts +95 -0
  139. package/src/routes/provider-config.ts +89 -41
  140. package/src/routes/settings.ts +127 -0
  141. package/src/routes/steward.ts +5 -5
  142. package/src/routes/tokens.ts +32 -1
  143. package/src/security.ts +36 -37
  144. package/src/services/merge-pause.ts +37 -0
  145. package/src/services/spawn-agent.ts +10 -8
  146. package/src/settings/modules.ts +149 -0
  147. package/src/settings/registry.ts +397 -0
  148. package/src/spawn-command.ts +5 -1
  149. package/src/team-template-config.ts +2 -2
  150. package/src/token-db.ts +20 -0
  151. package/src/workspace-pr-completion.ts +70 -0
  152. package/public/assets/MessageBubble-BrEKymoV.js +0 -2
  153. package/public/assets/MessageBubble-BrEKymoV.js.map +0 -1
  154. package/public/assets/agents-Cbegv0Hm.js +0 -2
  155. package/public/assets/branch-state-badge-CRA50r81.js +0 -2
  156. package/public/assets/card-BU6PvVHv.js +0 -2
  157. package/public/assets/chat-BqIaFYGJ.js +0 -2
  158. package/public/assets/display-CketawEF.js +0 -3
  159. package/public/assets/dist-D41tzJYg.js +0 -2
  160. package/public/assets/dist-DUoZnlB2.js +0 -2
  161. package/public/assets/dist-De2oguBm.js +0 -2
  162. package/public/assets/index-DaNOQJci.css +0 -2
  163. package/public/assets/index-br_Y6Cvt.js +0 -21
  164. package/public/assets/lucide-react-H_-8G79A.js +0 -9
  165. package/public/assets/lucide-react-H_-8G79A.js.map +0 -1
  166. package/public/assets/memory-CUUAzJDc.js +0 -2
  167. package/public/assets/settings-BgInn_f3.js +0 -10
  168. package/public/assets/settings-BgInn_f3.js.map +0 -1
  169. package/public/assets/store-DHO_a8BD.js +0 -9
  170. package/public/assets/store-DHO_a8BD.js.map +0 -1
  171. package/public/assets/tasks-B-4oCPuv.js +0 -2
  172. package/public/assets/teams-sCQAs_QA.js +0 -2
@@ -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,11 +30,13 @@ 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";
36
37
  import { postTeamDissolveByIdRoute } from "./teams";
37
38
  import { deleteTeamTemplateRoute, getTeamTemplateRoute, getTeamTemplatesRoute, postTeamTemplateRoute, putTeamTemplateRoute } from "./team-templates";
39
+ import { getMergePauseById, getMergePauses, postMergePause, postMergePauseRenew, postMergePauseResume } from "./merges";
38
40
  import { postMcp } from "../mcp";
39
41
  import { postOrchestratorBootstrap, postOrchestratorBootstrapExchange } from "./orchestrator-bootstrap";
40
42
 
@@ -139,6 +141,12 @@ const routes: Route[] = [
139
141
  route("POST", "/api/workspaces/:id/actions", postWorkspaceAction),
140
142
  route("DELETE", "/api/workspaces/:id", deleteWorkspaceById),
141
143
 
144
+ route("GET", "/api/merges/pauses", getMergePauses),
145
+ route("POST", "/api/merges/pause", postMergePause),
146
+ route("POST", "/api/merges/resume", postMergePauseResume),
147
+ route("POST", "/api/merges/renew", postMergePauseRenew),
148
+ route("GET", "/api/merges/pauses/:id", getMergePauseById),
149
+
142
150
  route("POST", "/api/system/broadcast", postSystemBroadcast),
143
151
  route("GET", "/api/teams", getTeamsRoute),
144
152
  route("GET", "/api/teams/outcomes", getTeamOutcomesRoute),
@@ -161,6 +169,7 @@ const routes: Route[] = [
161
169
  route("GET", "/api/recipes/:name", getRecipeByName),
162
170
  route("GET", "/api/tokens", getTokens),
163
171
  route("POST", "/api/tokens", postToken),
172
+ route("GET", "/api/tokens/me", getTokenMe),
164
173
  route("POST", "/api/runtime-tokens/renew", postRuntimeTokenRenew),
165
174
  route("POST", "/api/runtime-tokens/interactive-runner", postInteractiveRunnerRuntimeToken),
166
175
  route("POST", "/api/runtime-tokens/mcp-client", postMcpRuntimeToken),
@@ -189,6 +198,11 @@ const routes: Route[] = [
189
198
  route("GET", "/api/insights/observations", getInsightsObservationsRoute),
190
199
  route("POST", "/api/insights/observations", postInsightsObservationRoute),
191
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),
192
206
  route("GET", "/api/config/:namespace", getConfigNamespace),
193
207
  route("GET", "/api/config/:namespace/:key/history", getConfigKeyHistory),
194
208
  route("GET", "/api/config/:namespace/:key", getConfigKey),
@@ -206,6 +220,9 @@ const routes: Route[] = [
206
220
  route("GET", "/api/spawn-policy/:name/health", getSpawnPolicyHealth),
207
221
  route("GET", "/api/providers", getProvidersRoute),
208
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),
209
226
  route("GET", "/api/providers/:provider/config", getProviderConfigRoute),
210
227
  route("PUT", "/api/providers/:provider/config", putProviderConfigRoute),
211
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) {
@@ -0,0 +1,95 @@
1
+ // Auto-merge coordination routes. Generic pause/resume primitive for callers
2
+ // that need Relay's auto-merge sweep to defer a target briefly (#448).
3
+ import { isRecord } from "agent-relay-sdk";
4
+ import { ValidationError } from "../db";
5
+ import { getAutoMergePause, listAutoMergePauses, pauseAutoMerge, renewAutoMergePause, resumeAutoMerge } from "../services/merge-pause";
6
+ import { cleanString } from "../validation";
7
+ import { authAuditMetadata, authorizeRoute, error, json, parseBody, type Handler } from "./_shared";
8
+
9
+ function cleanTtlMs(value: unknown): number | undefined {
10
+ if (value === undefined || value === null || value === "") return undefined;
11
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
12
+ throw new ValidationError("ttlMs must be a positive integer");
13
+ }
14
+ return value;
15
+ }
16
+
17
+ function actorFromRequest(req: Request): string {
18
+ const audit = authAuditMetadata(req);
19
+ const auth = isRecord(audit.auth) ? audit.auth : undefined;
20
+ return typeof auth?.sub === "string" ? auth.sub
21
+ : typeof auth?.name === "string" ? auth.name
22
+ : "root";
23
+ }
24
+
25
+ export const getMergePauses: Handler = (req) => {
26
+ const denied = authorizeRoute(req, { scope: "command:read" });
27
+ if (denied) return denied;
28
+ return json({ pauses: listAutoMergePauses() });
29
+ };
30
+
31
+ export const postMergePause: Handler = async (req) => {
32
+ const denied = authorizeRoute(req, { scope: "command:write" });
33
+ if (denied) return denied;
34
+ const parsed = await parseBody<unknown>(req);
35
+ if (!parsed.ok) return error(parsed.error, parsed.status);
36
+ try {
37
+ if (!isRecord(parsed.body)) return error("body required");
38
+ const repoRoot = cleanString(parsed.body.repoRoot, "repoRoot", { required: true, max: 1000 })!;
39
+ const baseRef = cleanString(parsed.body.baseRef, "baseRef", { max: 240 }) ?? "main";
40
+ const holder = cleanString(parsed.body.holder, "holder", { max: 240 }) ?? actorFromRequest(req);
41
+ const reason = cleanString(parsed.body.reason, "reason", { max: 500 });
42
+ const ttlMs = cleanTtlMs(parsed.body.ttlMs);
43
+ const result = pauseAutoMerge({ repoRoot, baseRef, holder, reason, ttlMs });
44
+ if (!result.ok) {
45
+ return json({ error: "merge target already paused", pause: result.pause }, 409);
46
+ }
47
+ return json({ pause: result.pause }, 201);
48
+ } catch (e) {
49
+ if (e instanceof ValidationError) return error(e.message, 400);
50
+ throw e;
51
+ }
52
+ };
53
+
54
+ export const postMergePauseRenew: Handler = async (req) => {
55
+ const denied = authorizeRoute(req, { scope: "command:write" });
56
+ if (denied) return denied;
57
+ const parsed = await parseBody<unknown>(req);
58
+ if (!parsed.ok) return error(parsed.error, parsed.status);
59
+ try {
60
+ if (!isRecord(parsed.body)) return error("body required");
61
+ const id = cleanString(parsed.body.id, "id", { required: true, max: 120 })!;
62
+ const ttlMs = cleanTtlMs(parsed.body.ttlMs);
63
+ const pause = renewAutoMergePause(id, ttlMs);
64
+ return pause ? json({ pause }) : error("merge pause not found or expired", 404);
65
+ } catch (e) {
66
+ if (e instanceof ValidationError) return error(e.message, 400);
67
+ throw e;
68
+ }
69
+ };
70
+
71
+ export const postMergePauseResume: Handler = async (req) => {
72
+ const denied = authorizeRoute(req, { scope: "command:write" });
73
+ if (denied) return denied;
74
+ const parsed = await parseBody<unknown>(req);
75
+ if (!parsed.ok) return error(parsed.error, parsed.status);
76
+ try {
77
+ if (!isRecord(parsed.body)) return error("body required");
78
+ const id = cleanString(parsed.body.id, "id", { max: 120 });
79
+ const repoRoot = cleanString(parsed.body.repoRoot, "repoRoot", { max: 1000 });
80
+ const baseRef = cleanString(parsed.body.baseRef, "baseRef", { max: 240 });
81
+ if (!id && !repoRoot) return error("id or repoRoot required", 400);
82
+ const released = resumeAutoMerge(id ? { id } : { repoRoot: repoRoot!, baseRef: baseRef ?? "main" });
83
+ return json({ released });
84
+ } catch (e) {
85
+ if (e instanceof ValidationError) return error(e.message, 400);
86
+ throw e;
87
+ }
88
+ };
89
+
90
+ export const getMergePauseById: Handler = (req, params) => {
91
+ const denied = authorizeRoute(req, { scope: "command:read" });
92
+ if (denied) return denied;
93
+ const pause = getAutoMergePause(params.id!);
94
+ return pause ? json(pause) : error("merge pause not found", 404);
95
+ };
@@ -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) {