agent-relay-server 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +516 -1
- package/package.json +3 -2
- package/public/assets/MessageBubble-CX63ZV4X.js +2 -0
- package/public/assets/MessageBubble-CX63ZV4X.js.map +1 -0
- package/public/assets/{PlanGraphPanel-CXix1C-H.js → PlanGraphPanel-Dby7wq9c.js} +2 -2
- package/public/assets/{PlanGraphPanel-CXix1C-H.js.map → PlanGraphPanel-Dby7wq9c.js.map} +1 -1
- package/public/assets/{activity-CRafQ1Je.js → activity-2xPOScu_.js} +2 -2
- package/public/assets/{activity-CRafQ1Je.js.map → activity-2xPOScu_.js.map} +1 -1
- package/public/assets/{agent-profiles-DW4o5-ND.js → agent-profiles-BfGTTbeY.js} +2 -2
- package/public/assets/{agent-profiles-DW4o5-ND.js.map → agent-profiles-BfGTTbeY.js.map} +1 -1
- package/public/assets/agents-Bg3WOBRV.js +2 -0
- package/public/assets/{agents-Cbegv0Hm.js.map → agents-Bg3WOBRV.js.map} +1 -1
- package/public/assets/{analytics-DX6T7JX3.js → analytics-De0cEZGx.js} +3 -3
- package/public/assets/{analytics-DX6T7JX3.js.map → analytics-De0cEZGx.js.map} +1 -1
- package/public/assets/{automation-CzSBv4gg.js → automation-CHxhzq2t.js} +2 -2
- package/public/assets/{automation-CzSBv4gg.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-CRA50r81.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-B1MQCPCT.js → channels-BG0Nib7y.js} +2 -2
- package/public/assets/{channels-B1MQCPCT.js.map → channels-BG0Nib7y.js.map} +1 -1
- package/public/assets/chat-CjP_crSl.js +2 -0
- package/public/assets/{chat-BqIaFYGJ.js.map → chat-CjP_crSl.js.map} +1 -1
- package/public/assets/{connectors-C9aFJIrh.js → connectors-DWh7MwV_.js} +2 -2
- package/public/assets/{connectors-C9aFJIrh.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-DOXH7oDF.js → formatted-body-impl-u0W_hWYA.js} +2 -2
- package/public/assets/{formatted-body-impl-DOXH7oDF.js.map → formatted-body-impl-u0W_hWYA.js.map} +1 -1
- package/public/assets/index-BwfVOzUG.js +21 -0
- package/public/assets/{index-br_Y6Cvt.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-B1edq5lk.js → insights-CAF9Hi5D.js} +2 -2
- package/public/assets/{insights-B1edq5lk.js.map → insights-CAF9Hi5D.js.map} +1 -1
- package/public/assets/{integrations-Cm4fjp7_.js → integrations-CG9qyHUf.js} +2 -2
- package/public/assets/{integrations-Cm4fjp7_.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-D6zvw0fU.js → maintenance-DgHA35ve.js} +2 -2
- package/public/assets/{maintenance-D6zvw0fU.js.map → maintenance-DgHA35ve.js.map} +1 -1
- package/public/assets/{managed-agents-CzfEznM1.js → managed-agents-Rk6ghKR9.js} +2 -2
- package/public/assets/{managed-agents-CzfEznM1.js.map → managed-agents-Rk6ghKR9.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DZlCaTHQ.js → markdown-preview-impl-NWMtJdYz.js} +2 -2
- package/public/assets/{markdown-preview-impl-DZlCaTHQ.js.map → markdown-preview-impl-NWMtJdYz.js.map} +1 -1
- package/public/assets/memory-DamwWa9A.js +2 -0
- package/public/assets/{memory-CUUAzJDc.js.map → memory-DamwWa9A.js.map} +1 -1
- package/public/assets/{messages-6QZH46LG.js → messages--NU-YIL6.js} +2 -2
- package/public/assets/{messages-6QZH46LG.js.map → messages--NU-YIL6.js.map} +1 -1
- package/public/assets/{orchestrators-CrcqkPuc.js → orchestrators-DQnIn6qx.js} +2 -2
- package/public/assets/{orchestrators-CrcqkPuc.js.map → orchestrators-DQnIn6qx.js.map} +1 -1
- package/public/assets/{overview-418xXZO_.js → overview-DCmNgXDf.js} +2 -2
- package/public/assets/{overview-418xXZO_.js.map → overview-DCmNgXDf.js.map} +1 -1
- package/public/assets/{pairs-CnedATgT.js → pairs-DNELeC9P.js} +2 -2
- package/public/assets/{pairs-CnedATgT.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-DS13pbag.js → security-Dd3Kh0oC.js} +2 -2
- package/public/assets/{security-DS13pbag.js.map → security-Dd3Kh0oC.js.map} +1 -1
- package/public/assets/{select-BOprGZDS.js → select-DQbeJZX0.js} +2 -2
- package/public/assets/{select-BOprGZDS.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-B-4oCPuv.js.map → tasks-BYR4L9GP.js.map} +1 -1
- package/public/assets/teams-CoTifrq_.js +2 -0
- package/public/assets/{teams-sCQAs_QA.js.map → teams-CoTifrq_.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BhqS2XpS.js → terminal-viewer-impl-B1YzfpE6.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BhqS2XpS.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-BDhjlwUH.js → work-queue-x57vlgyZ.js} +2 -2
- package/public/assets/{work-queue-BDhjlwUH.js.map → work-queue-x57vlgyZ.js.map} +1 -1
- package/public/assets/{workspaces-6I-C69nf.js → workspaces-CYm5bLg9.js} +3 -3
- package/public/assets/{workspaces-6I-C69nf.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/cli/index.ts +11 -0
- package/src/cli/merges.ts +89 -0
- package/src/config-store.ts +5 -6
- package/src/config.ts +4 -0
- package/src/connectors.ts +5 -0
- package/src/db/merge-lease.ts +95 -1
- package/src/db/migrations.ts +21 -0
- package/src/db/schema.ts +1 -1
- package/src/index.ts +5 -2
- package/src/maintenance.ts +8 -8
- package/src/mcp-merge-tools.ts +66 -0
- package/src/mcp-provider-catalog.ts +9 -0
- package/src/mcp.ts +7 -8
- package/src/merge-pause-maintenance.ts +13 -0
- 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/agent-sessions.ts +4 -2
- package/src/routes/agents-spawn.ts +2 -2
- package/src/routes/commands.ts +20 -0
- package/src/routes/index.ts +19 -2
- package/src/routes/insights.ts +4 -3
- package/src/routes/merges.ts +95 -0
- 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 +36 -37
- package/src/services/merge-pause.ts +37 -0
- package/src/services/spawn-agent.ts +10 -8
- 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/src/workspace-pr-completion.ts +70 -0
- package/public/assets/MessageBubble-BrEKymoV.js +0 -2
- package/public/assets/MessageBubble-BrEKymoV.js.map +0 -1
- package/public/assets/agents-Cbegv0Hm.js +0 -2
- package/public/assets/branch-state-badge-CRA50r81.js +0 -2
- package/public/assets/card-BU6PvVHv.js +0 -2
- package/public/assets/chat-BqIaFYGJ.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-DaNOQJci.css +0 -2
- package/public/assets/index-br_Y6Cvt.js +0 -21
- 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-CUUAzJDc.js +0 -2
- package/public/assets/settings-BgInn_f3.js +0 -10
- package/public/assets/settings-BgInn_f3.js.map +0 -1
- package/public/assets/store-DHO_a8BD.js +0 -9
- package/public/assets/store-DHO_a8BD.js.map +0 -1
- package/public/assets/tasks-B-4oCPuv.js +0 -2
- package/public/assets/teams-sCQAs_QA.js +0 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ValidationError } from "./db";
|
|
2
|
+
import { McpNotFoundError } from "./mcp-errors";
|
|
3
|
+
import { authContextFromMcp } from "./services/auth-context";
|
|
4
|
+
import { listAutoMergePauses, pauseAutoMerge, renewAutoMergePause, resumeAutoMerge } from "./services/merge-pause";
|
|
5
|
+
import { cleanString } from "./validation";
|
|
6
|
+
|
|
7
|
+
export const MERGE_TOOLS = [
|
|
8
|
+
{ name: "relay_merge_pauses", description: "List active auto-merge pauses. These are repo/base-level holds that make the workspace auto-merge job yield during release or build critical sections.", requiredScopes: ["command:read"], inputSchema: { type: "object", properties: {}, additionalProperties: false } },
|
|
9
|
+
{
|
|
10
|
+
name: "relay_merge_pause",
|
|
11
|
+
description: "Pause workspace auto-merge for one repo/base pair with a TTL. Existing active pause for the same target is returned as a conflict instead of replacing its holder.",
|
|
12
|
+
requiredScopes: ["command:write"],
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
repoRoot: { type: "string", description: "Repository root whose auto-merge should pause." },
|
|
17
|
+
baseRef: { type: "string", description: "Base branch/ref to pause (default main)." },
|
|
18
|
+
ttlMs: { type: "integer", minimum: 1, description: "Pause TTL in milliseconds. Defaults to the server's merge-pause TTL." },
|
|
19
|
+
reason: { type: "string" },
|
|
20
|
+
holder: { type: "string", description: "Optional human-readable holder. Defaults to the authenticated caller." },
|
|
21
|
+
},
|
|
22
|
+
required: ["repoRoot"],
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{ name: "relay_merge_renew", description: "Renew an active auto-merge pause by id, extending its TTL from now.", requiredScopes: ["command:write"], inputSchema: { type: "object", properties: { id: { type: "string" }, ttlMs: { type: "integer", minimum: 1 } }, required: ["id"], additionalProperties: false } },
|
|
27
|
+
{ name: "relay_merge_resume", description: "Release an auto-merge pause by id, or by repoRoot/baseRef.", requiredScopes: ["command:write"], inputSchema: { type: "object", properties: { id: { type: "string" }, repoRoot: { type: "string" }, baseRef: { type: "string", description: "Base branch/ref (default main when repoRoot is used)." } }, additionalProperties: false } },
|
|
28
|
+
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown> }>;
|
|
29
|
+
|
|
30
|
+
type McpAuthLike = Parameters<typeof authContextFromMcp>[0];
|
|
31
|
+
|
|
32
|
+
export function relayMergeTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
33
|
+
const ctx = authContextFromMcp(auth);
|
|
34
|
+
if (name === "relay_merge_pauses") {
|
|
35
|
+
const pauses = listAutoMergePauses();
|
|
36
|
+
return { pauses, count: pauses.length };
|
|
37
|
+
}
|
|
38
|
+
if (name === "relay_merge_pause") {
|
|
39
|
+
const result = pauseAutoMerge({
|
|
40
|
+
repoRoot: cleanString(args.repoRoot, "repoRoot", { required: true, max: 1024 })!,
|
|
41
|
+
baseRef: cleanString(args.baseRef, "baseRef", { max: 240 }),
|
|
42
|
+
holder: cleanString(args.holder, "holder", { max: 240 }) ?? ctx.callerAgentId ?? ctx.actor.id,
|
|
43
|
+
reason: cleanString(args.reason, "reason", { max: 1000 }),
|
|
44
|
+
ttlMs: optionalPositiveInt(args.ttlMs, "ttlMs"),
|
|
45
|
+
});
|
|
46
|
+
return result.ok ? { ok: true, pause: result.pause } : { ok: false, error: "merge target already paused", pause: result.pause };
|
|
47
|
+
}
|
|
48
|
+
if (name === "relay_merge_renew") {
|
|
49
|
+
const pause = renewAutoMergePause(cleanString(args.id, "id", { required: true, max: 120 })!, optionalPositiveInt(args.ttlMs, "ttlMs"));
|
|
50
|
+
if (!pause) throw new McpNotFoundError("merge pause not found or expired");
|
|
51
|
+
return { pause };
|
|
52
|
+
}
|
|
53
|
+
if (name === "relay_merge_resume") {
|
|
54
|
+
const id = cleanString(args.id, "id", { max: 120 });
|
|
55
|
+
const repoRoot = cleanString(args.repoRoot, "repoRoot", { max: 1024 });
|
|
56
|
+
if (!id && !repoRoot) throw new ValidationError("id or repoRoot required");
|
|
57
|
+
return { released: resumeAutoMerge({ id, repoRoot, baseRef: cleanString(args.baseRef, "baseRef", { max: 240 }) }) };
|
|
58
|
+
}
|
|
59
|
+
throw new ValidationError(`unknown tool: ${name}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function optionalPositiveInt(value: unknown, field: string): number | undefined {
|
|
63
|
+
if (value === undefined || value === null) return undefined;
|
|
64
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new ValidationError(`${field} must be a positive integer`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { VALID_EFFORTS, isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { effectiveProviderEffortLevels } from "./provider-catalog-store";
|
|
3
|
+
|
|
4
|
+
export function providerCatalogToolInputSchema(name: string, inputSchema: Record<string, unknown>): Record<string, unknown> {
|
|
5
|
+
if (name !== "relay_spawn_agent") return inputSchema;
|
|
6
|
+
const efforts = effectiveProviderEffortLevels();
|
|
7
|
+
const properties = isRecord(inputSchema.properties) ? inputSchema.properties : {};
|
|
8
|
+
return { ...inputSchema, properties: { ...properties, effort: { type: "string", enum: efforts.length ? efforts : VALID_EFFORTS } } };
|
|
9
|
+
}
|
package/src/mcp.ts
CHANGED
|
@@ -50,9 +50,9 @@ import { PLAN_TOOLS, relayPlanTool } from "./mcp-plan-tools";
|
|
|
50
50
|
import { TEAM_TOOLS, relayTeamTool } from "./mcp-team-tools";
|
|
51
51
|
import { BLACKBOARD_TOOLS, relayBlackboardTool } from "./mcp-blackboard-tools";
|
|
52
52
|
import { PROJECT_TOOLS, relayProjectTool } from "./mcp-project-tools";
|
|
53
|
-
import { NOTIFICATION_SUBSCRIPTION_TOOLS, relayNotificationSubscriptionTool } from "./mcp-notification-subscription-tools";
|
|
53
|
+
import { NOTIFICATION_SUBSCRIPTION_TOOLS, relayNotificationSubscriptionTool } from "./mcp-notification-subscription-tools"; import { MERGE_TOOLS, relayMergeTool } from "./mcp-merge-tools";
|
|
54
54
|
import { PROJECT_INSTRUCTION_POLICY_NAMES, parseProjectInstructions } from "./project-instructions";
|
|
55
|
-
import { AGENT_ACTION_TOOL, relayAgentAction } from "./mcp-agent-action";
|
|
55
|
+
import { AGENT_ACTION_TOOL, relayAgentAction } from "./mcp-agent-action"; import { providerCatalogToolInputSchema } from "./mcp-provider-catalog";
|
|
56
56
|
import type { ActivityKind, AgentLifecycle, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider, WorkspaceAutoMergePolicy, WorkspaceMergeStrategy, WorkspaceMode, WorkspaceRecord } from "./types";
|
|
57
57
|
import { LAND_STRATEGIES, applyWorkspaceAction, waitForWorkspaceStatus, type WorkspaceAction } from "./workspace-actions";
|
|
58
58
|
import { AUTO_MERGE_POLICIES } from "./workspace-merge";
|
|
@@ -460,7 +460,7 @@ const TOOLS: ToolDefinition[] = [
|
|
|
460
460
|
additionalProperties: false,
|
|
461
461
|
},
|
|
462
462
|
},
|
|
463
|
-
...TEAM_TOOLS,
|
|
463
|
+
...MERGE_TOOLS, ...TEAM_TOOLS,
|
|
464
464
|
...PLAN_TOOLS,
|
|
465
465
|
...BLACKBOARD_TOOLS,
|
|
466
466
|
...PROJECT_TOOLS,
|
|
@@ -577,7 +577,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
577
577
|
else if (name === "relay_workspace_claim") result = relayWorkspaceMutation(auth, "claim", args);
|
|
578
578
|
else if (name === "relay_workspace_release") result = relayWorkspaceMutation(auth, "release-claim", args);
|
|
579
579
|
else if (name === "relay_workspace_land") result = relayWorkspaceMutation(auth, "merge", args);
|
|
580
|
-
else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
|
|
580
|
+
else if (name.startsWith("relay_merge_")) result = relayMergeTool(auth, name, args); else if (name.startsWith("relay_team_")) result = await relayTeamTool(auth, name, args);
|
|
581
581
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
582
582
|
else if (name.startsWith("relay_blackboard_")) result = relayBlackboardTool(auth, name, args);
|
|
583
583
|
else if (name.startsWith("relay_project_")) result = relayProjectTool(auth, name, args);
|
|
@@ -869,7 +869,7 @@ async function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknow
|
|
|
869
869
|
orchestratorId: optionalString(args.orchestratorId, "orchestratorId", 200),
|
|
870
870
|
cwd: optionalString(args.cwd, "cwd", 500),
|
|
871
871
|
model: optionalString(args.model, "model", 120),
|
|
872
|
-
effort:
|
|
872
|
+
effort: optionalString(args.effort, "effort", 120) as ProviderEffort | undefined,
|
|
873
873
|
approvalMode: optionalEnum(args.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined,
|
|
874
874
|
workspaceMode: optionalEnum(args.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
|
|
875
875
|
lifecycle: (optionalEnum(args.lifecycle, "lifecycle", VALID_AGENT_LIFECYCLES) as AgentLifecycle | undefined) ?? "persistent",
|
|
@@ -1043,7 +1043,6 @@ function relayWorkspaceMutation(auth: McpAuthContext, action: WorkspaceAction, a
|
|
|
1043
1043
|
return payload;
|
|
1044
1044
|
}
|
|
1045
1045
|
|
|
1046
|
-
|
|
1047
1046
|
function payloadWithAttachments(
|
|
1048
1047
|
payload: Record<string, unknown> | undefined,
|
|
1049
1048
|
attachments: AttachmentRef[] | undefined,
|
|
@@ -1141,10 +1140,10 @@ function sniffMediaType(bytes: Uint8Array, hinted?: string, filename?: string):
|
|
|
1141
1140
|
function visibleTools(auth: McpAuthContext): Array<Record<string, unknown>> {
|
|
1142
1141
|
return TOOLS
|
|
1143
1142
|
.filter((tool) => hasAnyScope(auth, tool.requiredScopes))
|
|
1144
|
-
.map(({ name, description,
|
|
1143
|
+
.map(({ name, description, alwaysLoad, ...tool }) => ({
|
|
1145
1144
|
name,
|
|
1146
1145
|
description,
|
|
1147
|
-
inputSchema,
|
|
1146
|
+
inputSchema: providerCatalogToolInputSchema(name, tool.inputSchema),
|
|
1148
1147
|
...(alwaysLoad ? { _meta: { "anthropic/alwaysLoad": true } } : {}),
|
|
1149
1148
|
}));
|
|
1150
1149
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { REAP_INTERVAL_MS } from "./config";
|
|
2
|
+
import { releaseExpiredMergePauses } from "./db";
|
|
3
|
+
|
|
4
|
+
export const MERGE_PAUSE_MAINTENANCE_JOB = {
|
|
5
|
+
id: "merge-pause-expiration",
|
|
6
|
+
title: "Merge pause expiration",
|
|
7
|
+
description: "Release expired repo/base auto-merge pauses.",
|
|
8
|
+
intervalMs: REAP_INTERVAL_MS,
|
|
9
|
+
runOnStart: true,
|
|
10
|
+
handler() {
|
|
11
|
+
return { releasedMergePauseIds: releaseExpiredMergePauses() };
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -13,6 +13,7 @@ interface ProviderModelOverrideRow {
|
|
|
13
13
|
alias: string;
|
|
14
14
|
entry: string;
|
|
15
15
|
deleted: number;
|
|
16
|
+
disabled: number;
|
|
16
17
|
updated_at: number;
|
|
17
18
|
updated_by?: string | null;
|
|
18
19
|
}
|
|
@@ -22,6 +23,7 @@ interface ProviderModelOverride {
|
|
|
22
23
|
alias: string;
|
|
23
24
|
entry: ProviderModelCatalogEntry;
|
|
24
25
|
deleted: boolean;
|
|
26
|
+
disabled: boolean;
|
|
25
27
|
updatedAt: number;
|
|
26
28
|
updatedBy?: string;
|
|
27
29
|
}
|
|
@@ -31,6 +33,7 @@ interface ProviderModelOverrideInput {
|
|
|
31
33
|
alias: string;
|
|
32
34
|
entry: ProviderModelCatalogEntry;
|
|
33
35
|
deleted?: boolean;
|
|
36
|
+
disabled?: boolean;
|
|
34
37
|
updatedBy?: string;
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -45,11 +48,12 @@ export function upsertProviderModelOverride(input: ProviderModelOverrideInput):
|
|
|
45
48
|
const normalized = normalizeOverrideInput(input);
|
|
46
49
|
const now = Date.now();
|
|
47
50
|
getDb().query(`
|
|
48
|
-
INSERT INTO provider_model_overrides (provider, alias, entry, deleted, updated_at, updated_by)
|
|
49
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
51
|
+
INSERT INTO provider_model_overrides (provider, alias, entry, deleted, disabled, updated_at, updated_by)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
50
53
|
ON CONFLICT(provider, alias) DO UPDATE SET
|
|
51
54
|
entry = excluded.entry,
|
|
52
55
|
deleted = excluded.deleted,
|
|
56
|
+
disabled = excluded.disabled,
|
|
53
57
|
updated_at = excluded.updated_at,
|
|
54
58
|
updated_by = excluded.updated_by
|
|
55
59
|
`).run(
|
|
@@ -57,6 +61,7 @@ export function upsertProviderModelOverride(input: ProviderModelOverrideInput):
|
|
|
57
61
|
normalized.alias,
|
|
58
62
|
JSON.stringify(normalized.entry),
|
|
59
63
|
normalized.deleted ? 1 : 0,
|
|
64
|
+
normalized.disabled ? 1 : 0,
|
|
60
65
|
now,
|
|
61
66
|
normalized.updatedBy ?? null,
|
|
62
67
|
);
|
|
@@ -66,6 +71,7 @@ export function upsertProviderModelOverride(input: ProviderModelOverrideInput):
|
|
|
66
71
|
alias: normalized.alias,
|
|
67
72
|
entry: normalized.entry,
|
|
68
73
|
deleted: Boolean(normalized.deleted),
|
|
74
|
+
disabled: Boolean(normalized.disabled),
|
|
69
75
|
updatedAt: now,
|
|
70
76
|
updatedBy: normalized.updatedBy,
|
|
71
77
|
};
|
|
@@ -83,10 +89,26 @@ export function removeProviderModelOverride(provider: SpawnProvider, alias: stri
|
|
|
83
89
|
});
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
export function setProviderModelDisabled(provider: SpawnProvider, alias: string, disabled: boolean, updatedBy?: string): ProviderModelOverride {
|
|
93
|
+
const existing = findModelEntry(provider, alias);
|
|
94
|
+
if (!existing) throw new ValidationError(`unknown ${provider} model: ${alias}`);
|
|
95
|
+
return upsertProviderModelOverride({
|
|
96
|
+
provider,
|
|
97
|
+
alias,
|
|
98
|
+
entry: existing,
|
|
99
|
+
disabled,
|
|
100
|
+
updatedBy,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
export function effectiveProviderCatalogList(): ProviderCatalogEntry[] {
|
|
87
105
|
return mergeProviderCatalog(providerCatalogList(), listProviderModelOverrides());
|
|
88
106
|
}
|
|
89
107
|
|
|
108
|
+
export function effectiveProviderEffortLevels(): string[] {
|
|
109
|
+
return [...new Set(effectiveProviderCatalogList().flatMap((provider) => provider.models.flatMap((model) => model.efforts)))].sort();
|
|
110
|
+
}
|
|
111
|
+
|
|
90
112
|
export function mergeProviderCatalog(
|
|
91
113
|
baseCatalog: ProviderCatalogEntry[],
|
|
92
114
|
overrides: ProviderModelOverride[],
|
|
@@ -102,7 +124,11 @@ export function mergeProviderCatalog(
|
|
|
102
124
|
if (existingIndex >= 0) providerEntry.models.splice(existingIndex, 1);
|
|
103
125
|
continue;
|
|
104
126
|
}
|
|
105
|
-
const nextEntry = cloneModelEntry({
|
|
127
|
+
const nextEntry = cloneModelEntry({
|
|
128
|
+
...override.entry,
|
|
129
|
+
alias: override.alias,
|
|
130
|
+
...(override.disabled ? { disabled: true } : { disabled: undefined }),
|
|
131
|
+
});
|
|
106
132
|
if (existingIndex >= 0) providerEntry.models[existingIndex] = nextEntry;
|
|
107
133
|
else providerEntry.models.push(nextEntry);
|
|
108
134
|
}
|
|
@@ -118,10 +144,12 @@ function normalizeOverrideInput(input: ProviderModelOverrideInput): ProviderMode
|
|
|
118
144
|
if (!input.entry.label?.trim()) throw new ValidationError("model label is required");
|
|
119
145
|
if (!input.entry.providerModel?.trim()) throw new ValidationError("provider model is required");
|
|
120
146
|
if (!Array.isArray(input.entry.efforts)) throw new ValidationError("model efforts must be an array");
|
|
147
|
+
const entry = cloneModelEntry({ ...input.entry, alias });
|
|
148
|
+
delete entry.disabled;
|
|
121
149
|
return {
|
|
122
150
|
...input,
|
|
123
151
|
alias,
|
|
124
|
-
entry
|
|
152
|
+
entry,
|
|
125
153
|
updatedBy: input.updatedBy?.trim() || undefined,
|
|
126
154
|
};
|
|
127
155
|
}
|
|
@@ -134,6 +162,7 @@ function rowToOverride(row: ProviderModelOverrideRow): ProviderModelOverride | u
|
|
|
134
162
|
alias: row.alias,
|
|
135
163
|
entry: cloneModelEntry({ ...parsed, alias: row.alias }),
|
|
136
164
|
deleted: row.deleted === 1,
|
|
165
|
+
disabled: row.disabled === 1,
|
|
137
166
|
updatedAt: row.updated_at,
|
|
138
167
|
updatedBy: row.updated_by ?? undefined,
|
|
139
168
|
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ValidationError, listOrchestrators } from "./db";
|
|
5
|
+
import { getConfig, listConfig, setConfig } from "./config-store";
|
|
6
|
+
import { cleanString, cleanStringArray, optionalEnum } from "./validation";
|
|
7
|
+
import { emitConfigChanged } from "./sse";
|
|
8
|
+
import {
|
|
9
|
+
PROVIDER_CONFIG_NAMESPACE,
|
|
10
|
+
validateSettingValue,
|
|
11
|
+
} from "./settings/registry";
|
|
12
|
+
import {
|
|
13
|
+
getProviderSettingsDeclaration,
|
|
14
|
+
listProviderSettingsDeclarations,
|
|
15
|
+
providerConfigDefaults,
|
|
16
|
+
registerDeclaredProviderConfigSetting,
|
|
17
|
+
} from "./settings/modules";
|
|
18
|
+
import { defaultProviderConfig, loadProviderConfig as loadLocalProviderConfig } from "../runner/src/config";
|
|
19
|
+
import { SPAWN_PROVIDERS, isRecord, type ConfigEntry, type SpawnProvider } from "agent-relay-sdk";
|
|
20
|
+
import type { ProviderConfig } from "../runner/src/adapter";
|
|
21
|
+
|
|
22
|
+
export const VALID_PROVIDER_CONFIGS = SPAWN_PROVIDERS;
|
|
23
|
+
|
|
24
|
+
type ProviderConfigProvider = string;
|
|
25
|
+
type ProviderConfigEntry = ConfigEntry<ProviderConfig>;
|
|
26
|
+
|
|
27
|
+
export function defaultProviderConfigHost(): string {
|
|
28
|
+
return hostname();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function providerConfigKey(host: string, provider: string): string {
|
|
32
|
+
return `${normalizeProviderConfigHost(host)}/${normalizeProviderConfigProvider(provider)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseProviderConfigKey(key: string): { host: string; provider: ProviderConfigProvider } | null {
|
|
36
|
+
const slash = key.lastIndexOf("/");
|
|
37
|
+
if (slash <= 0 || slash === key.length - 1) return null;
|
|
38
|
+
const host = cleanString(key.slice(0, slash), "host", { required: true, max: 120 })!;
|
|
39
|
+
const provider = cleanString(key.slice(slash + 1), "provider", { required: true, max: 80 })!;
|
|
40
|
+
return getProviderSettingsDeclaration(provider) ? { host, provider } : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ensureProviderConfigDescriptors(extraKeys: string[] = []): void {
|
|
44
|
+
const keys = new Set<string>();
|
|
45
|
+
const providerDeclarations = listProviderSettingsDeclarations();
|
|
46
|
+
for (const entry of listConfig(PROVIDER_CONFIG_NAMESPACE)) keys.add(entry.key);
|
|
47
|
+
for (const orchestrator of listOrchestrators()) {
|
|
48
|
+
if (!orchestrator.hostname) continue;
|
|
49
|
+
for (const declaration of providerDeclarations) keys.add(providerConfigKey(orchestrator.hostname, declaration.provider));
|
|
50
|
+
}
|
|
51
|
+
for (const declaration of providerDeclarations) keys.add(providerConfigKey(defaultProviderConfigHost(), declaration.provider));
|
|
52
|
+
for (const key of extraKeys) keys.add(key);
|
|
53
|
+
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
const parsed = parseProviderConfigKey(key);
|
|
56
|
+
if (!parsed) continue;
|
|
57
|
+
registerDeclaredProviderConfigSetting({
|
|
58
|
+
host: parsed.host,
|
|
59
|
+
provider: parsed.provider,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getProviderConfigEntry(provider: string, host = defaultProviderConfigHost()): ProviderConfigEntry {
|
|
65
|
+
const cleanProvider = normalizeProviderConfigProvider(provider);
|
|
66
|
+
const cleanHost = normalizeProviderConfigHost(host);
|
|
67
|
+
const key = providerConfigKey(cleanHost, cleanProvider);
|
|
68
|
+
ensureProviderConfigDescriptors([key]);
|
|
69
|
+
const entry = getConfig<ProviderConfig>(PROVIDER_CONFIG_NAMESPACE, key);
|
|
70
|
+
if (entry) return { ...entry, value: mergeProviderConfig(cleanProvider, entry.value) };
|
|
71
|
+
return {
|
|
72
|
+
namespace: PROVIDER_CONFIG_NAMESPACE,
|
|
73
|
+
key,
|
|
74
|
+
value: providerConfigDefaults(cleanProvider),
|
|
75
|
+
version: 0,
|
|
76
|
+
updatedAt: "default",
|
|
77
|
+
updatedBy: "system",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function listProviderConfigEntries(host = defaultProviderConfigHost()): Record<SpawnProvider, ProviderConfigEntry> {
|
|
82
|
+
return Object.fromEntries(
|
|
83
|
+
VALID_PROVIDER_CONFIGS.map((provider) => [provider, getProviderConfigEntry(provider, host)]),
|
|
84
|
+
) as Record<SpawnProvider, ProviderConfigEntry>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function setProviderConfig(
|
|
88
|
+
provider: string,
|
|
89
|
+
value: unknown,
|
|
90
|
+
opts: { host?: string; updatedBy?: string } = {},
|
|
91
|
+
): ProviderConfigEntry {
|
|
92
|
+
const cleanProvider = normalizeProviderConfigProvider(provider);
|
|
93
|
+
const host = normalizeProviderConfigHost(opts.host ?? defaultProviderConfigHost());
|
|
94
|
+
const normalized = cleanProviderConfig(cleanProvider, value);
|
|
95
|
+
const key = providerConfigKey(host, cleanProvider);
|
|
96
|
+
ensureProviderConfigDescriptors([key]);
|
|
97
|
+
const validation = validateSettingValue(PROVIDER_CONFIG_NAMESPACE, key, normalized);
|
|
98
|
+
if (!validation.valid) throw new ValidationError(validation.errors.join("; "));
|
|
99
|
+
return setConfig(PROVIDER_CONFIG_NAMESPACE, key, normalized, opts.updatedBy) as ProviderConfigEntry;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function providerConfigPublic(entry: ProviderConfigEntry, opts: { maskEnv?: boolean } = {}): Record<string, unknown> {
|
|
103
|
+
const parsed = parseProviderConfigKey(entry.key);
|
|
104
|
+
return {
|
|
105
|
+
namespace: entry.namespace,
|
|
106
|
+
key: entry.key,
|
|
107
|
+
path: `config:${entry.namespace}/${entry.key}`,
|
|
108
|
+
host: parsed?.host,
|
|
109
|
+
provider: parsed?.provider,
|
|
110
|
+
version: entry.version,
|
|
111
|
+
updatedAt: entry.updatedAt,
|
|
112
|
+
updatedBy: entry.updatedBy,
|
|
113
|
+
command: entry.value.command,
|
|
114
|
+
defaultArgs: entry.value.defaultArgs,
|
|
115
|
+
env: opts.maskEnv === false ? entry.value.env : maskEnv(entry.value.env),
|
|
116
|
+
pluginDirs: entry.value.pluginDirs,
|
|
117
|
+
defaultCapabilities: entry.value.defaultCapabilities,
|
|
118
|
+
defaultApprovalMode: entry.value.defaultApprovalMode,
|
|
119
|
+
defaultTags: entry.value.defaultTags,
|
|
120
|
+
chatCaptureMode: entry.value.chatCaptureMode,
|
|
121
|
+
reasoningCapture: entry.value.reasoningCapture,
|
|
122
|
+
headless: entry.value.headless,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function migrateLocalProviderConfigs(home = agentRelayHome(), host = defaultProviderConfigHost()): ProviderConfigEntry[] {
|
|
127
|
+
const dir = join(home, "providers");
|
|
128
|
+
if (!existsSync(dir)) return [];
|
|
129
|
+
const migrated: ProviderConfigEntry[] = [];
|
|
130
|
+
for (const file of readdirSync(dir)) {
|
|
131
|
+
const provider = file.endsWith(".json") ? file.slice(0, -".json".length) : "";
|
|
132
|
+
if (!VALID_PROVIDER_CONFIGS.includes(provider as SpawnProvider)) continue;
|
|
133
|
+
const key = providerConfigKey(host, provider);
|
|
134
|
+
if (getConfig(PROVIDER_CONFIG_NAMESPACE, key)) continue;
|
|
135
|
+
const local = loadLocalProviderConfig(provider, home);
|
|
136
|
+
const entry = setProviderConfig(provider, local, { host, updatedBy: `migration:${host}` });
|
|
137
|
+
emitConfigChanged(entry.namespace, entry.key, entry.version);
|
|
138
|
+
migrated.push(entry);
|
|
139
|
+
}
|
|
140
|
+
return migrated;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function normalizeProviderConfigHost(host: string): string {
|
|
144
|
+
return cleanString(host, "host", { required: true, max: 120 })!;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeProviderConfigProvider(provider: string): ProviderConfigProvider {
|
|
148
|
+
const clean = cleanString(provider, "provider", { required: true, max: 80 })!;
|
|
149
|
+
if (!getProviderSettingsDeclaration(clean)) {
|
|
150
|
+
throw new ValidationError(`provider must be one of: ${listProviderSettingsDeclarations().map((d) => d.provider).join(", ")}`);
|
|
151
|
+
}
|
|
152
|
+
return clean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cleanProviderConfig(provider: string, value: unknown): ProviderConfig {
|
|
156
|
+
if (!isRecord(value)) throw new ValidationError("provider config body must be an object");
|
|
157
|
+
const defaults = providerConfigDefaults(provider);
|
|
158
|
+
const headless = isRecord(value.headless) ? value.headless : {};
|
|
159
|
+
return {
|
|
160
|
+
command: cleanString(value.command, "command", { required: true, max: 500 })!,
|
|
161
|
+
defaultArgs: cleanStringArray(value.defaultArgs, "defaultArgs", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultArgs,
|
|
162
|
+
env: cleanEnvRecord(value.env),
|
|
163
|
+
pluginDirs: cleanStringArray(value.pluginDirs, "pluginDirs", { itemMax: 80, maxItems: 50 }) ?? defaults.pluginDirs,
|
|
164
|
+
defaultCapabilities: cleanStringArray(value.defaultCapabilities, "defaultCapabilities", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultCapabilities,
|
|
165
|
+
defaultApprovalMode: cleanString(value.defaultApprovalMode, "defaultApprovalMode", { max: 80 }) ?? defaults.defaultApprovalMode,
|
|
166
|
+
defaultTags: cleanStringArray(value.defaultTags, "defaultTags", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultTags,
|
|
167
|
+
chatCaptureMode: (optionalEnum(value.chatCaptureMode, "chatCaptureMode", ["final", "full"]) ?? defaults.chatCaptureMode) as "final" | "full",
|
|
168
|
+
reasoningCapture: value.reasoningCapture === undefined ? defaults.reasoningCapture ?? true : cleanBoolean(value.reasoningCapture, "reasoningCapture"),
|
|
169
|
+
headless: {
|
|
170
|
+
tmuxPrefix: cleanString(headless.tmuxPrefix, "headless.tmuxPrefix", { max: 120 }) ?? defaults.headless.tmuxPrefix,
|
|
171
|
+
shutdownTimeoutMs: cleanPositiveInt(headless.shutdownTimeoutMs, "headless.shutdownTimeoutMs") ?? defaults.headless.shutdownTimeoutMs,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function mergeProviderConfig(provider: string, value: Partial<ProviderConfig>): ProviderConfig {
|
|
177
|
+
const defaults = providerConfigDefaults(provider);
|
|
178
|
+
return cleanProviderConfig(provider, { ...defaults, ...value, headless: { ...defaults.headless, ...value.headless } });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cleanEnvRecord(value: unknown): Record<string, string> {
|
|
182
|
+
if (value === undefined) return {};
|
|
183
|
+
if (!isRecord(value)) throw new ValidationError("env must be an object");
|
|
184
|
+
const env: Record<string, string> = {};
|
|
185
|
+
for (const [key, item] of Object.entries(value)) {
|
|
186
|
+
const cleanKey = cleanString(key, "env key", { required: true, max: 120 })!;
|
|
187
|
+
if (typeof item !== "string") throw new ValidationError(`env.${cleanKey} must be a string`);
|
|
188
|
+
env[cleanKey] = item;
|
|
189
|
+
}
|
|
190
|
+
return env;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cleanBoolean(value: unknown, field: string): boolean {
|
|
194
|
+
if (typeof value !== "boolean") throw new ValidationError(`${field} must be a boolean`);
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cleanPositiveInt(value: unknown, field: string): number | undefined {
|
|
199
|
+
if (value === undefined) return undefined;
|
|
200
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
201
|
+
throw new ValidationError(`${field} must be a positive integer`);
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function maskEnv(env: Record<string, string>): Record<string, string> {
|
|
207
|
+
const result: Record<string, string> = {};
|
|
208
|
+
for (const [key, value] of Object.entries(env)) {
|
|
209
|
+
result[key] = /token|secret|key|password/i.test(key) && !value.startsWith("$env:") ? "********" : value;
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function agentRelayHome(): string {
|
|
215
|
+
return process.env.AGENT_RELAY_HOME || join(process.env.HOME || ".", ".agent-relay");
|
|
216
|
+
}
|
package/src/recipe-runner.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createCommand } from "./commands-db";
|
|
|
2
2
|
import { listOrchestrators } from "./db";
|
|
3
3
|
import { resolveProviderSelection } from "agent-relay-sdk/provider-catalog";
|
|
4
4
|
import { getRecipe } from "./recipe-loader";
|
|
5
|
+
import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
5
6
|
import {
|
|
6
7
|
createRecipeInstance,
|
|
7
8
|
getRecipeInstance,
|
|
@@ -56,7 +57,7 @@ export function startRecipe(input: StartRecipeInput): StartRecipeResult {
|
|
|
56
57
|
for (const agent of recipe.agents) {
|
|
57
58
|
const count = agent.count ?? 1;
|
|
58
59
|
for (let index = 0; index < count; index++) {
|
|
59
|
-
const selection = resolveProviderSelection({ provider: agent.provider, model: agent.model, effort: agent.effort });
|
|
60
|
+
const selection = resolveProviderSelection({ provider: agent.provider, model: agent.model, effort: agent.effort }, effectiveProviderCatalogList());
|
|
60
61
|
const label = count > 1 ? `${recipe.name}:${agent.role}:${index + 1}` : `${recipe.name}:${agent.role}`;
|
|
61
62
|
const memoryTags = [...new Set([...(recipe.lifecycle?.memory?.memoryTags ?? []), ...(agent.memoryTags ?? [])])];
|
|
62
63
|
const alwaysReload = recipe.lifecycle?.memory?.alwaysReload ?? [];
|
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,6 +1,6 @@
|
|
|
1
1
|
// Auto-split from routes.ts (#299). Domain: agent-sessions.
|
|
2
2
|
import { RELAY_TOKEN_HEADER, isRecord } from "agent-relay-sdk";
|
|
3
|
-
import { VALID_AGENT_ACTIONS, agentControlActionIcon, auditEvent, authAuditMetadata, authorizeRoute, dashboardAttribution, emitCommand, error, json, metaString, parseBody, spawnRequestId, type AgentControlAction, type Handler } from "./_shared";
|
|
3
|
+
import { VALID_AGENT_ACTIONS, agentControlActionIcon, auditEvent, authAuditMetadata, authorizeRoute, cleanAttachmentRefs, dashboardAttribution, emitCommand, error, json, metaString, parseBody, spawnRequestId, type AgentControlAction, type Handler } from "./_shared";
|
|
4
4
|
import { ValidationError, getAgent, getAgentTimeline, getOrchestrator, markReady, mergeAgentMeta, sendMessage } from "../db";
|
|
5
5
|
import { buildManagedSpawnParams } from "../managed-policy";
|
|
6
6
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams } from "../spawn-command";
|
|
@@ -306,6 +306,7 @@ export const postAgentPrompt: Handler = async (req, params) => {
|
|
|
306
306
|
try {
|
|
307
307
|
if (!isRecord(parsed.body)) return error("body required");
|
|
308
308
|
const body = cleanString(parsed.body.body, "body", { required: true, max: 100_000 })!;
|
|
309
|
+
const attachments = cleanAttachmentRefs(parsed.body.attachments ?? parsed.body.artifacts, "attachments");
|
|
309
310
|
const agent = getAgent(params.id!);
|
|
310
311
|
if (!agent) return error("agent not found", 404);
|
|
311
312
|
if (agent.status === "offline") return error("agent is offline", 422);
|
|
@@ -319,12 +320,13 @@ export const postAgentPrompt: Handler = async (req, params) => {
|
|
|
319
320
|
to: agent.id,
|
|
320
321
|
kind: "session",
|
|
321
322
|
body,
|
|
323
|
+
...(attachments ? { payload: { attachments } } : {}),
|
|
322
324
|
});
|
|
323
325
|
const command = createCommand({
|
|
324
326
|
type: "prompt.inject",
|
|
325
327
|
source: "dashboard",
|
|
326
328
|
target: agent.id,
|
|
327
|
-
params: { body, messageId: message.id },
|
|
329
|
+
params: { body, messageId: message.id, ...(attachments ? { attachments } : {}) },
|
|
328
330
|
});
|
|
329
331
|
emitCommand(command);
|
|
330
332
|
emitNewMessage(message);
|
|
@@ -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/commands.ts
CHANGED
|
@@ -315,6 +315,26 @@ export const patchCommand: Handler = async (req, params) => {
|
|
|
315
315
|
});
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
|
+
if (command.type === "workspace.pr-refresh") {
|
|
319
|
+
const resultWorkspaceId = isRecord(command.result) ? cleanString(command.result.workspaceId, "result.workspaceId", { max: 160 }) : undefined;
|
|
320
|
+
const workspaceId = resultWorkspaceId ?? command.correlationId;
|
|
321
|
+
if (workspaceId && command.status === "succeeded" && isRecord(command.result) && command.result.prRefreshed === true) {
|
|
322
|
+
patchWorkspaceMetadata(workspaceId, {
|
|
323
|
+
prRefreshCommandId: command.id,
|
|
324
|
+
prRefreshedAt: Date.now(),
|
|
325
|
+
prRefreshResult: command.result,
|
|
326
|
+
prRefreshError: undefined,
|
|
327
|
+
prRefreshFailedAt: undefined,
|
|
328
|
+
});
|
|
329
|
+
} else if (workspaceId && (command.status === "failed" || command.status === "timed_out" || command.status === "rejected" || command.status === "canceled")) {
|
|
330
|
+
patchWorkspaceMetadata(workspaceId, {
|
|
331
|
+
prRefreshCommandId: command.id,
|
|
332
|
+
prRefreshError: command.error ?? "PR branch refresh failed",
|
|
333
|
+
prRefreshFailedAt: Date.now(),
|
|
334
|
+
prRefreshResult: command.result,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
318
338
|
if (command.type === "workspace.deps-refresh" && command.status === "succeeded" && isRecord(command.result)) {
|
|
319
339
|
// Record the outcome on the row without touching status (#51) — observability
|
|
320
340
|
// for the dashboard; the CLI reads the result straight off the command.
|