agent-relay-server 0.60.1 → 0.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/orchestrator.ts +21 -1
- 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) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Auto-split from routes.ts (#299). Domain: orchestrator.
|
|
2
2
|
import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
|
|
3
3
|
import { CONTRACT_VERSIONS, parseRuntimeCapabilities, parseRuntimeContracts, parseRuntimePackage, type RuntimeCapabilities, type RuntimeContracts, type RuntimePackageMetadata } from "../contracts";
|
|
4
|
+
import { notifyAgentOffline } from "../agent-lifecycle-events";
|
|
4
5
|
import { VERSION } from "../config";
|
|
5
6
|
import { ValidationError, deleteOrchestrator, getOrchestrator, listOrchestrators, orchestratorHeartbeat, recordAgentExitDiagnostics, setOrchestratorUpgradeState, updateManagedAgents, upsertOrchestrator } from "../db";
|
|
6
7
|
import { auditEvent, authAuditMetadata, authorizeRoute, cleanJsonArray, cleanSafeNumber, dashboardAttribution, emitCommand, error, isRootCredentialRequest, json, parseBody, spawnRequestId, type Handler } from "./_shared";
|
|
@@ -201,7 +202,15 @@ export const patchOrchestratorAgents: Handler = async (req, params) => {
|
|
|
201
202
|
const updated = updateManagedAgents(params.id!, cleaned);
|
|
202
203
|
for (const exited of exitedAgents) {
|
|
203
204
|
const agent = recordAgentExitDiagnostics(exited.agentId, exited);
|
|
204
|
-
if (agent)
|
|
205
|
+
if (agent) {
|
|
206
|
+
emitAgentStatus(agent.id);
|
|
207
|
+
// #466 — fast exit path must wake the spawn-parent. recordAgentExitDiagnostics already
|
|
208
|
+
// flipped the agent offline (above), so the stale-agent-reaper will structurally skip it
|
|
209
|
+
// (its WHERE excludes status='offline'), guaranteeing a single notifyAgentOffline emission.
|
|
210
|
+
// notifyAgentOffline no-ops for non-spawned agents. Only fire when we actually recorded the
|
|
211
|
+
// exit (agent !== null) so an already-offline row can't double-notify.
|
|
212
|
+
notifyAgentOffline(exited.agentId, exitReason(exited));
|
|
213
|
+
}
|
|
205
214
|
auditEvent({
|
|
206
215
|
clientId: [
|
|
207
216
|
"managed-session-exit",
|
|
@@ -248,6 +257,17 @@ export const patchOrchestratorAgents: Handler = async (req, params) => {
|
|
|
248
257
|
}
|
|
249
258
|
};
|
|
250
259
|
|
|
260
|
+
// #466 — derive a human-meaningful offline cause for the spawn-parent notification from the exit
|
|
261
|
+
// diagnostics, in preference to the reaper's generic "heartbeat lost". Prefer the captured
|
|
262
|
+
// lastError, then the systemd result (e.g. "success", "signal"), else a plain fallback.
|
|
263
|
+
function exitReason(exited: ManagedSessionExitDiagnostics): string {
|
|
264
|
+
const lastError = exited.lastError?.trim();
|
|
265
|
+
if (lastError) return lastError;
|
|
266
|
+
const result = exited.systemd?.result?.trim();
|
|
267
|
+
if (result) return `exited (${result})`;
|
|
268
|
+
return "exited";
|
|
269
|
+
}
|
|
270
|
+
|
|
251
271
|
function cleanManagedSessionExitDiagnostics(value: unknown, index: number): ManagedSessionExitDiagnostics {
|
|
252
272
|
if (!isRecord(value)) throw new ValidationError(`exitedAgents[${index}] must be an object`);
|
|
253
273
|
const provider = optionalEnum(value.provider, `exitedAgents[${index}].provider`, SPAWN_PROVIDERS);
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
import { ValidationError, listOrchestrators } from "../db";
|
|
3
3
|
import { applyCommandToRecipe } from "../recipe-runner";
|
|
4
4
|
import { cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
5
|
-
import { defaultProviderConfig, loadProviderConfig, providerConfigPublic, writeProviderConfig } from "../../runner/src/config";
|
|
6
5
|
import { deleteCommand, getCommand } from "../commands-db";
|
|
7
|
-
import { effectiveProviderCatalogList } from "../provider-catalog-store";
|
|
6
|
+
import { effectiveProviderCatalogList, removeProviderModelOverride, setProviderModelDisabled, upsertProviderModelOverride } from "../provider-catalog-store";
|
|
8
7
|
import { emitCommand, error, json, parseBody, type Handler } from "./_shared";
|
|
9
|
-
import {
|
|
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) {
|