agent-relay-server 0.60.1 → 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.
- package/docs/openapi.json +361 -1
- package/package.json +3 -2
- package/public/assets/{MessageBubble-vzupFQD8.js → MessageBubble-CX63ZV4X.js} +2 -2
- package/public/assets/{MessageBubble-vzupFQD8.js.map → MessageBubble-CX63ZV4X.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-DQvycBMW.js → PlanGraphPanel-Dby7wq9c.js} +2 -2
- package/public/assets/{PlanGraphPanel-DQvycBMW.js.map → PlanGraphPanel-Dby7wq9c.js.map} +1 -1
- package/public/assets/{activity-gmfkOBhQ.js → activity-2xPOScu_.js} +2 -2
- package/public/assets/{activity-gmfkOBhQ.js.map → activity-2xPOScu_.js.map} +1 -1
- package/public/assets/{agent-profiles-7X8gFuwj.js → agent-profiles-BfGTTbeY.js} +2 -2
- package/public/assets/{agent-profiles-7X8gFuwj.js.map → agent-profiles-BfGTTbeY.js.map} +1 -1
- package/public/assets/agents-Bg3WOBRV.js +2 -0
- package/public/assets/{agents-D201CuTR.js.map → agents-Bg3WOBRV.js.map} +1 -1
- package/public/assets/{analytics-CkOTrr5t.js → analytics-De0cEZGx.js} +3 -3
- package/public/assets/{analytics-CkOTrr5t.js.map → analytics-De0cEZGx.js.map} +1 -1
- package/public/assets/{automation-_5u-nVs8.js → automation-CHxhzq2t.js} +2 -2
- package/public/assets/{automation-_5u-nVs8.js.map → automation-CHxhzq2t.js.map} +1 -1
- package/public/assets/{badge-Cy6qO_Jz.js → badge-DhLyCPag.js} +2 -2
- package/public/assets/{badge-Cy6qO_Jz.js.map → badge-DhLyCPag.js.map} +1 -1
- package/public/assets/branch-state-badge-DSyWKluD.js +2 -0
- package/public/assets/{branch-state-badge-BPXXLpQG.js.map → branch-state-badge-DSyWKluD.js.map} +1 -1
- package/public/assets/{button-73wpRw65.js → button-K20wAZG1.js} +2 -2
- package/public/assets/{button-73wpRw65.js.map → button-K20wAZG1.js.map} +1 -1
- package/public/assets/card-CcZ9hLI7.js +2 -0
- package/public/assets/{card-BU6PvVHv.js.map → card-CcZ9hLI7.js.map} +1 -1
- package/public/assets/{channels-BsJu5TXR.js → channels-BG0Nib7y.js} +2 -2
- package/public/assets/{channels-BsJu5TXR.js.map → channels-BG0Nib7y.js.map} +1 -1
- package/public/assets/chat-CjP_crSl.js +2 -0
- package/public/assets/{chat-CHgPGHQD.js.map → chat-CjP_crSl.js.map} +1 -1
- package/public/assets/{connectors-arjfBQvc.js → connectors-DWh7MwV_.js} +2 -2
- package/public/assets/{connectors-arjfBQvc.js.map → connectors-DWh7MwV_.js.map} +1 -1
- package/public/assets/{copy-button-HRopUwui.js → copy-button-eF3cKp4Q.js} +2 -2
- package/public/assets/{copy-button-HRopUwui.js.map → copy-button-eF3cKp4Q.js.map} +1 -1
- package/public/assets/display-DzNvOu1j.js +3 -0
- package/public/assets/{display-CketawEF.js.map → display-DzNvOu1j.js.map} +1 -1
- package/public/assets/{dist-Kkpnjx_M.js → dist-1dhfdhm9.js} +2 -2
- package/public/assets/{dist-Kkpnjx_M.js.map → dist-1dhfdhm9.js.map} +1 -1
- package/public/assets/dist-BLBF7qqr.js +2 -0
- package/public/assets/{dist-De2oguBm.js.map → dist-BLBF7qqr.js.map} +1 -1
- package/public/assets/{dist-T7u7IZGJ.js → dist-BrQsqBbc.js} +2 -2
- package/public/assets/{dist-T7u7IZGJ.js.map → dist-BrQsqBbc.js.map} +1 -1
- package/public/assets/{dist-Cko2KPW8.js → dist-COQuunEv.js} +2 -2
- package/public/assets/{dist-Cko2KPW8.js.map → dist-COQuunEv.js.map} +1 -1
- package/public/assets/{dist-CLHMeXYi.js → dist-Ci5Y37nj.js} +2 -2
- package/public/assets/{dist-CLHMeXYi.js.map → dist-Ci5Y37nj.js.map} +1 -1
- package/public/assets/{dist-Ci1MM51P.js → dist-CoROafNg.js} +2 -2
- package/public/assets/{dist-Ci1MM51P.js.map → dist-CoROafNg.js.map} +1 -1
- package/public/assets/{dist-CC5f7oYw.js → dist-I-MFUv51.js} +2 -2
- package/public/assets/{dist-CC5f7oYw.js.map → dist-I-MFUv51.js.map} +1 -1
- package/public/assets/{dist-DFLciw0h.js → dist-UQGKwqZH.js} +2 -2
- package/public/assets/{dist-DFLciw0h.js.map → dist-UQGKwqZH.js.map} +1 -1
- package/public/assets/dist-YpHfnnIA.js +2 -0
- package/public/assets/{dist-DUoZnlB2.js.map → dist-YpHfnnIA.js.map} +1 -1
- package/public/assets/dist-qd198ib9.js +2 -0
- package/public/assets/{dist-D41tzJYg.js.map → dist-qd198ib9.js.map} +1 -1
- package/public/assets/{dist-DmN94rc8.js → dist-yGOq6sB3.js} +2 -2
- package/public/assets/{dist-DmN94rc8.js.map → dist-yGOq6sB3.js.map} +1 -1
- package/public/assets/{es2015-Df08Opjx.js → es2015-CRxz9lOj.js} +2 -2
- package/public/assets/{es2015-Df08Opjx.js.map → es2015-CRxz9lOj.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DBt5tnS0.js → formatted-body-impl-u0W_hWYA.js} +2 -2
- package/public/assets/{formatted-body-impl-DBt5tnS0.js.map → formatted-body-impl-u0W_hWYA.js.map} +1 -1
- package/public/assets/index-BwfVOzUG.js +21 -0
- package/public/assets/{index-D7js3XlH.js.map → index-BwfVOzUG.js.map} +1 -1
- package/public/assets/index-Dn_GDXbi.css +2 -0
- package/public/assets/{input-B2EDz-BE.js → input-BHYNpXAG.js} +2 -2
- package/public/assets/{input-B2EDz-BE.js.map → input-BHYNpXAG.js.map} +1 -1
- package/public/assets/{insights-BvjkOXk9.js → insights-CAF9Hi5D.js} +2 -2
- package/public/assets/{insights-BvjkOXk9.js.map → insights-CAF9Hi5D.js.map} +1 -1
- package/public/assets/{integrations-7L1f3eIE.js → integrations-CG9qyHUf.js} +2 -2
- package/public/assets/{integrations-7L1f3eIE.js.map → integrations-CG9qyHUf.js.map} +1 -1
- package/public/assets/{lib-BZrjIp-o.js → lib-Df4aph5u.js} +2 -2
- package/public/assets/{lib-BZrjIp-o.js.map → lib-Df4aph5u.js.map} +1 -1
- package/public/assets/lucide-react-L2UxFDic.js +9 -0
- package/public/assets/lucide-react-L2UxFDic.js.map +1 -0
- package/public/assets/{maintenance-BBoL--vs.js → maintenance-DgHA35ve.js} +2 -2
- package/public/assets/{maintenance-BBoL--vs.js.map → maintenance-DgHA35ve.js.map} +1 -1
- package/public/assets/{managed-agents-BjYBzVZt.js → managed-agents-Rk6ghKR9.js} +2 -2
- package/public/assets/{managed-agents-BjYBzVZt.js.map → managed-agents-Rk6ghKR9.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BnwjZsmO.js → markdown-preview-impl-NWMtJdYz.js} +2 -2
- package/public/assets/{markdown-preview-impl-BnwjZsmO.js.map → markdown-preview-impl-NWMtJdYz.js.map} +1 -1
- package/public/assets/memory-DamwWa9A.js +2 -0
- package/public/assets/{memory-SLycz2gC.js.map → memory-DamwWa9A.js.map} +1 -1
- package/public/assets/{messages-DfSCTe6N.js → messages--NU-YIL6.js} +2 -2
- package/public/assets/{messages-DfSCTe6N.js.map → messages--NU-YIL6.js.map} +1 -1
- package/public/assets/{orchestrators-BwxdTQm_.js → orchestrators-DQnIn6qx.js} +2 -2
- package/public/assets/{orchestrators-BwxdTQm_.js.map → orchestrators-DQnIn6qx.js.map} +1 -1
- package/public/assets/{overview-Djy3qkY5.js → overview-DCmNgXDf.js} +2 -2
- package/public/assets/{overview-Djy3qkY5.js.map → overview-DCmNgXDf.js.map} +1 -1
- package/public/assets/{pairs-9KNduLnC.js → pairs-DNELeC9P.js} +2 -2
- package/public/assets/{pairs-9KNduLnC.js.map → pairs-DNELeC9P.js.map} +1 -1
- package/public/assets/{react-dom-WI6TjVe7.js → react-dom-CrP_AtcL.js} +2 -2
- package/public/assets/{react-dom-WI6TjVe7.js.map → react-dom-CrP_AtcL.js.map} +1 -1
- package/public/assets/{security-tG3q7fFW.js → security-Dd3Kh0oC.js} +2 -2
- package/public/assets/{security-tG3q7fFW.js.map → security-Dd3Kh0oC.js.map} +1 -1
- package/public/assets/{select-CGUdMNOG.js → select-DQbeJZX0.js} +2 -2
- package/public/assets/{select-CGUdMNOG.js.map → select-DQbeJZX0.js.map} +1 -1
- package/public/assets/settings-BMAJrBIH.js +4 -0
- package/public/assets/settings-BMAJrBIH.js.map +1 -0
- package/public/assets/store-B8xP6z6T.js +9 -0
- package/public/assets/store-B8xP6z6T.js.map +1 -0
- package/public/assets/tasks-BYR4L9GP.js +2 -0
- package/public/assets/{tasks-DWlKg218.js.map → tasks-BYR4L9GP.js.map} +1 -1
- package/public/assets/teams-CoTifrq_.js +2 -0
- package/public/assets/{teams-DRchUP_p.js.map → teams-CoTifrq_.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-D_Sqe-vo.js → terminal-viewer-impl-B1YzfpE6.js} +3 -3
- package/public/assets/{terminal-viewer-impl-D_Sqe-vo.js.map → terminal-viewer-impl-B1YzfpE6.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-fOL_3RHB.js → use-keyboard-viewport-DrRH_QzG.js} +2 -2
- package/public/assets/{use-keyboard-viewport-fOL_3RHB.js.map → use-keyboard-viewport-DrRH_QzG.js.map} +1 -1
- package/public/assets/{work-queue-HNcyjON9.js → work-queue-x57vlgyZ.js} +2 -2
- package/public/assets/{work-queue-HNcyjON9.js.map → work-queue-x57vlgyZ.js.map} +1 -1
- package/public/assets/{workspaces-CicYE2Ax.js → workspaces-CYm5bLg9.js} +3 -3
- package/public/assets/{workspaces-CicYE2Ax.js.map → workspaces-CYm5bLg9.js.map} +1 -1
- package/public/index.html +22 -22
- package/runner/src/config.ts +66 -2
- package/src/automations.ts +3 -4
- package/src/config-store.ts +5 -6
- package/src/connectors.ts +5 -0
- package/src/db/migrations.ts +6 -0
- package/src/db/schema.ts +1 -1
- package/src/index.ts +5 -2
- package/src/mcp-provider-catalog.ts +9 -0
- package/src/mcp.ts +4 -4
- package/src/provider-catalog-store.ts +33 -4
- package/src/provider-config-store.ts +216 -0
- package/src/recipe-runner.ts +2 -1
- package/src/recipe-validator.ts +4 -3
- package/src/routes/agents-spawn.ts +2 -2
- package/src/routes/index.ts +12 -2
- package/src/routes/insights.ts +4 -3
- package/src/routes/provider-config.ts +89 -41
- package/src/routes/settings.ts +127 -0
- package/src/routes/steward.ts +5 -5
- package/src/routes/tokens.ts +32 -1
- package/src/security.ts +34 -37
- package/src/settings/modules.ts +149 -0
- package/src/settings/registry.ts +397 -0
- package/src/spawn-command.ts +5 -1
- package/src/team-template-config.ts +2 -2
- package/src/token-db.ts +20 -0
- package/public/assets/agents-D201CuTR.js +0 -2
- package/public/assets/branch-state-badge-BPXXLpQG.js +0 -2
- package/public/assets/card-BU6PvVHv.js +0 -2
- package/public/assets/chat-CHgPGHQD.js +0 -2
- package/public/assets/display-CketawEF.js +0 -3
- package/public/assets/dist-D41tzJYg.js +0 -2
- package/public/assets/dist-DUoZnlB2.js +0 -2
- package/public/assets/dist-De2oguBm.js +0 -2
- package/public/assets/index-D7js3XlH.js +0 -21
- package/public/assets/index-DaNOQJci.css +0 -2
- package/public/assets/lucide-react-H_-8G79A.js +0 -9
- package/public/assets/lucide-react-H_-8G79A.js.map +0 -1
- package/public/assets/memory-SLycz2gC.js +0 -2
- package/public/assets/settings-CGcqyJ5L.js +0 -10
- package/public/assets/settings-CGcqyJ5L.js.map +0 -1
- package/public/assets/store-DXdfhiVf.js +0 -9
- package/public/assets/store-DXdfhiVf.js.map +0 -1
- package/public/assets/tasks-DWlKg218.js +0 -2
- package/public/assets/teams-DRchUP_p.js +0 -2
package/src/recipe-validator.ts
CHANGED
|
@@ -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 (
|
|
107
|
-
|
|
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,
|
|
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:
|
|
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 }),
|
package/src/routes/index.ts
CHANGED
|
@@ -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),
|
package/src/routes/insights.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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 =
|
|
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) {
|
|
@@ -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 {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
99
|
-
if (value === undefined) return undefined;
|
|
100
|
-
if (typeof value !== "
|
|
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
|
+
}
|
package/src/routes/steward.ts
CHANGED
|
@@ -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 {
|
|
6
|
+
import { getRegisteredSettingEntry, setRegisteredSettingValue } from "./settings";
|
|
7
7
|
import { isRecord } from "agent-relay-sdk";
|
|
8
8
|
|
|
9
|
-
export const getStewardConfigRoute: Handler = () => json(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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) {
|
package/src/routes/tokens.ts
CHANGED
|
@@ -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) {
|
package/src/security.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { getDb } from "./db";
|
|
6
6
|
import { isPathWithinBase } from "./utils";
|
|
7
7
|
import type { ComponentToken, TokenConstraints } from "./types";
|
|
8
|
+
import { hasScope, normalizeScope } from "agent-relay-sdk/types";
|
|
8
9
|
|
|
9
10
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
10
11
|
|
|
@@ -99,30 +100,7 @@ type AuthorizationCheck = {
|
|
|
99
100
|
resource?: AuthorizationResource;
|
|
100
101
|
};
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
"admin:*": "system:admin",
|
|
104
|
-
"system:write": "system:admin",
|
|
105
|
-
"agents:read": "agent:read",
|
|
106
|
-
"agents:write": "agent:write",
|
|
107
|
-
"messages:read": "message:read",
|
|
108
|
-
"messages:write": "message:send",
|
|
109
|
-
"tasks:read": "task:read",
|
|
110
|
-
"tasks:write": "task:write",
|
|
111
|
-
"commands:read": "command:read",
|
|
112
|
-
"commands:write": "command:write",
|
|
113
|
-
"channels:read": "channel:read",
|
|
114
|
-
"channels:write": "channel:write",
|
|
115
|
-
"integrations:read": "integration:read",
|
|
116
|
-
"integrations:write": "integration:write",
|
|
117
|
-
"connectors:read": "integration:read",
|
|
118
|
-
"connectors:write": "integration:write",
|
|
119
|
-
"events:create": "integration:write",
|
|
120
|
-
"tasks:create": "task:write",
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
export function normalizeScope(scope: string): string {
|
|
124
|
-
return SCOPE_ALIASES[scope] ?? scope;
|
|
125
|
-
}
|
|
103
|
+
export { normalizeScope };
|
|
126
104
|
|
|
127
105
|
export function getIntegrationAuth(req: Request): IntegrationAuth | null {
|
|
128
106
|
const token = extractToken(req);
|
|
@@ -170,6 +148,8 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
170
148
|
if (pathname === "/api/orchestrators/bootstrap") return "token:write";
|
|
171
149
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/logs\//)) return "logs:read";
|
|
172
150
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/terminal\//)) return "terminal:attach";
|
|
151
|
+
const settingScope = requiredSettingsScopeFor(method, pathname);
|
|
152
|
+
if (settingScope !== undefined) return settingScope;
|
|
173
153
|
if (pathname.startsWith("/api/artifacts")) {
|
|
174
154
|
if (method === "GET" || method === "HEAD") return "artifact:read";
|
|
175
155
|
if (method === "DELETE") return "artifact:admin";
|
|
@@ -201,6 +181,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
201
181
|
if (pathname.startsWith("/api/recipes")) return method === "GET" ? "recipes:read" : "recipes:write";
|
|
202
182
|
if (pathname === "/api/runtime-tokens/renew") return "agent:write";
|
|
203
183
|
if (pathname.startsWith("/api/runtime-tokens")) return "token:write";
|
|
184
|
+
if (pathname === "/api/tokens/me") return null;
|
|
204
185
|
if (pathname.startsWith("/api/tokens") || pathname.startsWith("/api/token-profiles")) return method === "GET" ? "token:read" : "token:write";
|
|
205
186
|
if (pathname.startsWith("/api/maintenance")) return "system:admin";
|
|
206
187
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
@@ -251,12 +232,15 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
251
232
|
if (pathname === "/api/mcp") return "mcp:use";
|
|
252
233
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/logs\//)) return "logs:read";
|
|
253
234
|
if (pathname.match(/^\/api\/orchestrators\/[^/]+\/terminal\//)) return "terminal:attach";
|
|
235
|
+
const settingScope = requiredSettingsScopeFor(method, pathname);
|
|
236
|
+
if (settingScope !== undefined) return settingScope ?? "system:admin";
|
|
254
237
|
if (pathname.startsWith("/api/commands")) {
|
|
255
238
|
if (method === "GET") return "command:read";
|
|
256
239
|
return "command:write";
|
|
257
240
|
}
|
|
258
241
|
if (pathname.startsWith("/api/merges")) return method === "GET" ? "command:read" : "command:write";
|
|
259
242
|
if (pathname.startsWith("/api/workspaces")) return method === "GET" ? "agent:read" : "agent:write";
|
|
243
|
+
if (pathname === "/api/tokens/me") return null;
|
|
260
244
|
if (pathname.startsWith("/api/tokens") || pathname.startsWith("/api/token-profiles")) return method === "GET" ? "token:read" : "token:write";
|
|
261
245
|
if (pathname.startsWith("/api/maintenance")) return "system:admin";
|
|
262
246
|
if (pathname === "/api/recipes/start") return "recipe:start";
|
|
@@ -298,6 +282,32 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
298
282
|
return "system:admin";
|
|
299
283
|
}
|
|
300
284
|
|
|
285
|
+
const CONFIG_SETTINGS_NAMESPACES = new Set(["steward", "insights", "notifications", "workspace", "landing", "provider"]);
|
|
286
|
+
|
|
287
|
+
function requiredSettingsScopeFor(method: string, pathname: string): string | null | undefined {
|
|
288
|
+
if (pathname === "/api/settings") return method === "GET" ? "settings:read" : null;
|
|
289
|
+
const settingsMatch = pathname.match(/^\/api\/settings\/([^/]+)(?:\/[^/]+)?$/);
|
|
290
|
+
if (settingsMatch) return settingsScope(method, decodeURIComponent(settingsMatch[1]!));
|
|
291
|
+
if (pathname === "/api/steward-config") return settingsScope(method, "steward");
|
|
292
|
+
if (pathname === "/api/workspace-config") return settingsScope(method, "workspace");
|
|
293
|
+
if (pathname === "/api/insights/config") return settingsScope(method, "insights");
|
|
294
|
+
const configMatch = pathname.match(/^\/api\/config\/([^/]+)(?:\/[^/]+)?(?:\/history)?$/);
|
|
295
|
+
if (configMatch) {
|
|
296
|
+
const namespace = decodeURIComponent(configMatch[1]!);
|
|
297
|
+
if (CONFIG_SETTINGS_NAMESPACES.has(namespace)) return settingsScope(method, namespace);
|
|
298
|
+
}
|
|
299
|
+
if (pathname.match(/^\/api\/providers\/[^/]+\/models\/[^/]+$/) && ["PUT", "PATCH", "DELETE"].includes(method)) {
|
|
300
|
+
return "settings:write:provider";
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function settingsScope(method: string, namespace: string): string | null {
|
|
306
|
+
if (method === "GET" || method === "HEAD") return `settings:read:${namespace}`;
|
|
307
|
+
if (["PUT", "PATCH", "POST", "DELETE"].includes(method)) return `settings:write:${namespace}`;
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
301
311
|
export function signComponentToken(payload: Omit<ComponentToken, "iat"> & { iat?: number }): string {
|
|
302
312
|
const header = { alg: "HS256", typ: "JWT" };
|
|
303
313
|
const body: ComponentToken = {
|
|
@@ -404,19 +414,6 @@ function isComponentToken(value: unknown): value is ComponentToken {
|
|
|
404
414
|
(record.jti === undefined || typeof record.jti === "string");
|
|
405
415
|
}
|
|
406
416
|
|
|
407
|
-
function hasScope(scopes: string[], requiredScope: string): boolean {
|
|
408
|
-
const required = normalizeScope(requiredScope);
|
|
409
|
-
if (scopes.includes("*")) return true;
|
|
410
|
-
for (const rawScope of scopes) {
|
|
411
|
-
const scope = normalizeScope(rawScope);
|
|
412
|
-
if (scope === "system:admin") return true;
|
|
413
|
-
if (scope === required) return true;
|
|
414
|
-
const [category] = required.split(":");
|
|
415
|
-
if (category && scope === `${category}:*`) return true;
|
|
416
|
-
}
|
|
417
|
-
return false;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
417
|
/** Authoritative parent lineage from a registering token's signed constraints — agent-initiated
|
|
421
418
|
* spawn (`spawnedBy`) or the delegating-component path (`parentAgents`). One home, used by both the
|
|
422
419
|
* HTTP `postAgent` route and the bus register path so `spawned_by` is set however a child registers
|