agent-relay-server 0.60.1 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +361 -1
- package/package.json +3 -2
- package/public/assets/{MessageBubble-vzupFQD8.js → MessageBubble-CX63ZV4X.js} +2 -2
- package/public/assets/{MessageBubble-vzupFQD8.js.map → MessageBubble-CX63ZV4X.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-DQvycBMW.js → PlanGraphPanel-Dby7wq9c.js} +2 -2
- package/public/assets/{PlanGraphPanel-DQvycBMW.js.map → PlanGraphPanel-Dby7wq9c.js.map} +1 -1
- package/public/assets/{activity-gmfkOBhQ.js → activity-2xPOScu_.js} +2 -2
- package/public/assets/{activity-gmfkOBhQ.js.map → activity-2xPOScu_.js.map} +1 -1
- package/public/assets/{agent-profiles-7X8gFuwj.js → agent-profiles-BfGTTbeY.js} +2 -2
- package/public/assets/{agent-profiles-7X8gFuwj.js.map → agent-profiles-BfGTTbeY.js.map} +1 -1
- package/public/assets/agents-Bg3WOBRV.js +2 -0
- package/public/assets/{agents-D201CuTR.js.map → agents-Bg3WOBRV.js.map} +1 -1
- package/public/assets/{analytics-CkOTrr5t.js → analytics-De0cEZGx.js} +3 -3
- package/public/assets/{analytics-CkOTrr5t.js.map → analytics-De0cEZGx.js.map} +1 -1
- package/public/assets/{automation-_5u-nVs8.js → automation-CHxhzq2t.js} +2 -2
- package/public/assets/{automation-_5u-nVs8.js.map → automation-CHxhzq2t.js.map} +1 -1
- package/public/assets/{badge-Cy6qO_Jz.js → badge-DhLyCPag.js} +2 -2
- package/public/assets/{badge-Cy6qO_Jz.js.map → badge-DhLyCPag.js.map} +1 -1
- package/public/assets/branch-state-badge-DSyWKluD.js +2 -0
- package/public/assets/{branch-state-badge-BPXXLpQG.js.map → branch-state-badge-DSyWKluD.js.map} +1 -1
- package/public/assets/{button-73wpRw65.js → button-K20wAZG1.js} +2 -2
- package/public/assets/{button-73wpRw65.js.map → button-K20wAZG1.js.map} +1 -1
- package/public/assets/card-CcZ9hLI7.js +2 -0
- package/public/assets/{card-BU6PvVHv.js.map → card-CcZ9hLI7.js.map} +1 -1
- package/public/assets/{channels-BsJu5TXR.js → channels-BG0Nib7y.js} +2 -2
- package/public/assets/{channels-BsJu5TXR.js.map → channels-BG0Nib7y.js.map} +1 -1
- package/public/assets/chat-CjP_crSl.js +2 -0
- package/public/assets/{chat-CHgPGHQD.js.map → chat-CjP_crSl.js.map} +1 -1
- package/public/assets/{connectors-arjfBQvc.js → connectors-DWh7MwV_.js} +2 -2
- package/public/assets/{connectors-arjfBQvc.js.map → connectors-DWh7MwV_.js.map} +1 -1
- package/public/assets/{copy-button-HRopUwui.js → copy-button-eF3cKp4Q.js} +2 -2
- package/public/assets/{copy-button-HRopUwui.js.map → copy-button-eF3cKp4Q.js.map} +1 -1
- package/public/assets/display-DzNvOu1j.js +3 -0
- package/public/assets/{display-CketawEF.js.map → display-DzNvOu1j.js.map} +1 -1
- package/public/assets/{dist-Kkpnjx_M.js → dist-1dhfdhm9.js} +2 -2
- package/public/assets/{dist-Kkpnjx_M.js.map → dist-1dhfdhm9.js.map} +1 -1
- package/public/assets/dist-BLBF7qqr.js +2 -0
- package/public/assets/{dist-De2oguBm.js.map → dist-BLBF7qqr.js.map} +1 -1
- package/public/assets/{dist-T7u7IZGJ.js → dist-BrQsqBbc.js} +2 -2
- package/public/assets/{dist-T7u7IZGJ.js.map → dist-BrQsqBbc.js.map} +1 -1
- package/public/assets/{dist-Cko2KPW8.js → dist-COQuunEv.js} +2 -2
- package/public/assets/{dist-Cko2KPW8.js.map → dist-COQuunEv.js.map} +1 -1
- package/public/assets/{dist-CLHMeXYi.js → dist-Ci5Y37nj.js} +2 -2
- package/public/assets/{dist-CLHMeXYi.js.map → dist-Ci5Y37nj.js.map} +1 -1
- package/public/assets/{dist-Ci1MM51P.js → dist-CoROafNg.js} +2 -2
- package/public/assets/{dist-Ci1MM51P.js.map → dist-CoROafNg.js.map} +1 -1
- package/public/assets/{dist-CC5f7oYw.js → dist-I-MFUv51.js} +2 -2
- package/public/assets/{dist-CC5f7oYw.js.map → dist-I-MFUv51.js.map} +1 -1
- package/public/assets/{dist-DFLciw0h.js → dist-UQGKwqZH.js} +2 -2
- package/public/assets/{dist-DFLciw0h.js.map → dist-UQGKwqZH.js.map} +1 -1
- package/public/assets/dist-YpHfnnIA.js +2 -0
- package/public/assets/{dist-DUoZnlB2.js.map → dist-YpHfnnIA.js.map} +1 -1
- package/public/assets/dist-qd198ib9.js +2 -0
- package/public/assets/{dist-D41tzJYg.js.map → dist-qd198ib9.js.map} +1 -1
- package/public/assets/{dist-DmN94rc8.js → dist-yGOq6sB3.js} +2 -2
- package/public/assets/{dist-DmN94rc8.js.map → dist-yGOq6sB3.js.map} +1 -1
- package/public/assets/{es2015-Df08Opjx.js → es2015-CRxz9lOj.js} +2 -2
- package/public/assets/{es2015-Df08Opjx.js.map → es2015-CRxz9lOj.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DBt5tnS0.js → formatted-body-impl-u0W_hWYA.js} +2 -2
- package/public/assets/{formatted-body-impl-DBt5tnS0.js.map → formatted-body-impl-u0W_hWYA.js.map} +1 -1
- package/public/assets/index-BwfVOzUG.js +21 -0
- package/public/assets/{index-D7js3XlH.js.map → index-BwfVOzUG.js.map} +1 -1
- package/public/assets/index-Dn_GDXbi.css +2 -0
- package/public/assets/{input-B2EDz-BE.js → input-BHYNpXAG.js} +2 -2
- package/public/assets/{input-B2EDz-BE.js.map → input-BHYNpXAG.js.map} +1 -1
- package/public/assets/{insights-BvjkOXk9.js → insights-CAF9Hi5D.js} +2 -2
- package/public/assets/{insights-BvjkOXk9.js.map → insights-CAF9Hi5D.js.map} +1 -1
- package/public/assets/{integrations-7L1f3eIE.js → integrations-CG9qyHUf.js} +2 -2
- package/public/assets/{integrations-7L1f3eIE.js.map → integrations-CG9qyHUf.js.map} +1 -1
- package/public/assets/{lib-BZrjIp-o.js → lib-Df4aph5u.js} +2 -2
- package/public/assets/{lib-BZrjIp-o.js.map → lib-Df4aph5u.js.map} +1 -1
- package/public/assets/lucide-react-L2UxFDic.js +9 -0
- package/public/assets/lucide-react-L2UxFDic.js.map +1 -0
- package/public/assets/{maintenance-BBoL--vs.js → maintenance-DgHA35ve.js} +2 -2
- package/public/assets/{maintenance-BBoL--vs.js.map → maintenance-DgHA35ve.js.map} +1 -1
- package/public/assets/{managed-agents-BjYBzVZt.js → managed-agents-Rk6ghKR9.js} +2 -2
- package/public/assets/{managed-agents-BjYBzVZt.js.map → managed-agents-Rk6ghKR9.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BnwjZsmO.js → markdown-preview-impl-NWMtJdYz.js} +2 -2
- package/public/assets/{markdown-preview-impl-BnwjZsmO.js.map → markdown-preview-impl-NWMtJdYz.js.map} +1 -1
- package/public/assets/memory-DamwWa9A.js +2 -0
- package/public/assets/{memory-SLycz2gC.js.map → memory-DamwWa9A.js.map} +1 -1
- package/public/assets/{messages-DfSCTe6N.js → messages--NU-YIL6.js} +2 -2
- package/public/assets/{messages-DfSCTe6N.js.map → messages--NU-YIL6.js.map} +1 -1
- package/public/assets/{orchestrators-BwxdTQm_.js → orchestrators-DQnIn6qx.js} +2 -2
- package/public/assets/{orchestrators-BwxdTQm_.js.map → orchestrators-DQnIn6qx.js.map} +1 -1
- package/public/assets/{overview-Djy3qkY5.js → overview-DCmNgXDf.js} +2 -2
- package/public/assets/{overview-Djy3qkY5.js.map → overview-DCmNgXDf.js.map} +1 -1
- package/public/assets/{pairs-9KNduLnC.js → pairs-DNELeC9P.js} +2 -2
- package/public/assets/{pairs-9KNduLnC.js.map → pairs-DNELeC9P.js.map} +1 -1
- package/public/assets/{react-dom-WI6TjVe7.js → react-dom-CrP_AtcL.js} +2 -2
- package/public/assets/{react-dom-WI6TjVe7.js.map → react-dom-CrP_AtcL.js.map} +1 -1
- package/public/assets/{security-tG3q7fFW.js → security-Dd3Kh0oC.js} +2 -2
- package/public/assets/{security-tG3q7fFW.js.map → security-Dd3Kh0oC.js.map} +1 -1
- package/public/assets/{select-CGUdMNOG.js → select-DQbeJZX0.js} +2 -2
- package/public/assets/{select-CGUdMNOG.js.map → select-DQbeJZX0.js.map} +1 -1
- package/public/assets/settings-BMAJrBIH.js +4 -0
- package/public/assets/settings-BMAJrBIH.js.map +1 -0
- package/public/assets/store-B8xP6z6T.js +9 -0
- package/public/assets/store-B8xP6z6T.js.map +1 -0
- package/public/assets/tasks-BYR4L9GP.js +2 -0
- package/public/assets/{tasks-DWlKg218.js.map → tasks-BYR4L9GP.js.map} +1 -1
- package/public/assets/teams-CoTifrq_.js +2 -0
- package/public/assets/{teams-DRchUP_p.js.map → teams-CoTifrq_.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-D_Sqe-vo.js → terminal-viewer-impl-B1YzfpE6.js} +3 -3
- package/public/assets/{terminal-viewer-impl-D_Sqe-vo.js.map → terminal-viewer-impl-B1YzfpE6.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-fOL_3RHB.js → use-keyboard-viewport-DrRH_QzG.js} +2 -2
- package/public/assets/{use-keyboard-viewport-fOL_3RHB.js.map → use-keyboard-viewport-DrRH_QzG.js.map} +1 -1
- package/public/assets/{work-queue-HNcyjON9.js → work-queue-x57vlgyZ.js} +2 -2
- package/public/assets/{work-queue-HNcyjON9.js.map → work-queue-x57vlgyZ.js.map} +1 -1
- package/public/assets/{workspaces-CicYE2Ax.js → workspaces-CYm5bLg9.js} +3 -3
- package/public/assets/{workspaces-CicYE2Ax.js.map → workspaces-CYm5bLg9.js.map} +1 -1
- package/public/index.html +22 -22
- package/runner/src/config.ts +66 -2
- package/src/automations.ts +3 -4
- package/src/config-store.ts +5 -6
- package/src/connectors.ts +5 -0
- package/src/db/migrations.ts +6 -0
- package/src/db/schema.ts +1 -1
- package/src/index.ts +5 -2
- package/src/mcp-provider-catalog.ts +9 -0
- package/src/mcp.ts +4 -4
- package/src/provider-catalog-store.ts +33 -4
- package/src/provider-config-store.ts +216 -0
- package/src/recipe-runner.ts +2 -1
- package/src/recipe-validator.ts +4 -3
- package/src/routes/agents-spawn.ts +2 -2
- package/src/routes/index.ts +12 -2
- package/src/routes/insights.ts +4 -3
- package/src/routes/provider-config.ts +89 -41
- package/src/routes/settings.ts +127 -0
- package/src/routes/steward.ts +5 -5
- package/src/routes/tokens.ts +32 -1
- package/src/security.ts +34 -37
- package/src/settings/modules.ts +149 -0
- package/src/settings/registry.ts +397 -0
- package/src/spawn-command.ts +5 -1
- package/src/team-template-config.ts +2 -2
- package/src/token-db.ts +20 -0
- package/public/assets/agents-D201CuTR.js +0 -2
- package/public/assets/branch-state-badge-BPXXLpQG.js +0 -2
- package/public/assets/card-BU6PvVHv.js +0 -2
- package/public/assets/chat-CHgPGHQD.js +0 -2
- package/public/assets/display-CketawEF.js +0 -3
- package/public/assets/dist-D41tzJYg.js +0 -2
- package/public/assets/dist-DUoZnlB2.js +0 -2
- package/public/assets/dist-De2oguBm.js +0 -2
- package/public/assets/index-D7js3XlH.js +0 -21
- package/public/assets/index-DaNOQJci.css +0 -2
- package/public/assets/lucide-react-H_-8G79A.js +0 -9
- package/public/assets/lucide-react-H_-8G79A.js.map +0 -1
- package/public/assets/memory-SLycz2gC.js +0 -2
- package/public/assets/settings-CGcqyJ5L.js +0 -10
- package/public/assets/settings-CGcqyJ5L.js.map +0 -1
- package/public/assets/store-DXdfhiVf.js +0 -9
- package/public/assets/store-DXdfhiVf.js.map +0 -1
- package/public/assets/tasks-DWlKg218.js +0 -2
- package/public/assets/teams-DRchUP_p.js +0 -2
package/runner/src/config.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import { DEFAULT_RELAY_URL, stringValue } from "agent-relay-sdk";
|
|
6
|
-
import type { WorkspaceMetadata } from "agent-relay-sdk";
|
|
5
|
+
import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER, errMessage, stringValue } from "agent-relay-sdk";
|
|
6
|
+
import type { SettingEntry, WorkspaceMetadata } from "agent-relay-sdk";
|
|
7
7
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
8
8
|
import type { ProviderConfig } from "./adapter";
|
|
9
9
|
|
|
@@ -36,6 +36,7 @@ export function defaultProviderConfig(provider: string): ProviderConfig {
|
|
|
36
36
|
defaultApprovalMode: "guarded",
|
|
37
37
|
defaultTags: [],
|
|
38
38
|
chatCaptureMode: "final",
|
|
39
|
+
reasoningCapture: true,
|
|
39
40
|
headless: {
|
|
40
41
|
tmuxPrefix: `${provider}-relay`,
|
|
41
42
|
shutdownTimeoutMs: 10_000,
|
|
@@ -67,6 +68,7 @@ export function loadProviderConfig(provider: string, home = agentRelayHome()): L
|
|
|
67
68
|
defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
|
|
68
69
|
defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
|
|
69
70
|
chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
|
|
71
|
+
reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
|
|
70
72
|
headless: {
|
|
71
73
|
tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
|
|
72
74
|
shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
|
|
@@ -74,6 +76,24 @@ export function loadProviderConfig(provider: string, home = agentRelayHome()): L
|
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
export async function loadProviderConfigFromRelay(
|
|
80
|
+
provider: string,
|
|
81
|
+
opts: { relayUrl: string; token?: string; home?: string; host?: string },
|
|
82
|
+
): Promise<LoadedProviderConfig> {
|
|
83
|
+
const host = opts.host ?? hostname();
|
|
84
|
+
const home = opts.home ?? agentRelayHome();
|
|
85
|
+
const local = loadProviderConfig(provider, home);
|
|
86
|
+
const key = `${host}/${provider}`;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const fetched = await fetchProviderConfig(provider, opts.relayUrl, opts.token, key);
|
|
90
|
+
if (fetched) return fetched;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn(`[agent-relay] provider config relay read failed (${errMessage(error)}); using local fallback ${local.path}.`);
|
|
93
|
+
}
|
|
94
|
+
return local;
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
export function writeProviderConfig(provider: string, config: ProviderConfig, home = agentRelayHome()): LoadedProviderConfig {
|
|
78
98
|
const dir = providersDir(home);
|
|
79
99
|
mkdirSync(dir, { recursive: true });
|
|
@@ -93,6 +113,7 @@ export function providerConfigPublic(config: LoadedProviderConfig): Record<strin
|
|
|
93
113
|
defaultApprovalMode: config.defaultApprovalMode,
|
|
94
114
|
defaultTags: config.defaultTags,
|
|
95
115
|
chatCaptureMode: config.chatCaptureMode,
|
|
116
|
+
reasoningCapture: config.reasoningCapture,
|
|
96
117
|
headless: config.headless,
|
|
97
118
|
};
|
|
98
119
|
}
|
|
@@ -156,6 +177,10 @@ function enumValue<T extends string>(value: unknown, allowed: T[]): T | undefine
|
|
|
156
177
|
return typeof value === "string" && allowed.includes(value as T) ? (value as T) : undefined;
|
|
157
178
|
}
|
|
158
179
|
|
|
180
|
+
function booleanValue(value: unknown): boolean | undefined {
|
|
181
|
+
return typeof value === "boolean" ? value : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
159
184
|
function stringArray(value: unknown): string[] | undefined {
|
|
160
185
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
|
|
161
186
|
}
|
|
@@ -178,3 +203,42 @@ function maskEnv(env: Record<string, string>): Record<string, string> {
|
|
|
178
203
|
}
|
|
179
204
|
return result;
|
|
180
205
|
}
|
|
206
|
+
|
|
207
|
+
async function fetchProviderConfig(
|
|
208
|
+
provider: string,
|
|
209
|
+
relayUrl: string,
|
|
210
|
+
token: string | undefined,
|
|
211
|
+
key: string,
|
|
212
|
+
): Promise<LoadedProviderConfig | null> {
|
|
213
|
+
const url = new URL(`/api/settings/provider-config/${encodeURIComponent(key)}`, relayUrl);
|
|
214
|
+
const res = await fetch(url, {
|
|
215
|
+
headers: token ? { [RELAY_TOKEN_HEADER]: token } : undefined,
|
|
216
|
+
signal: AbortSignal.timeout(5_000),
|
|
217
|
+
});
|
|
218
|
+
if (res.status === 404) return null;
|
|
219
|
+
const body = await res.json().catch(() => null) as (Partial<SettingEntry> & { error?: string }) | null;
|
|
220
|
+
if (!res.ok) throw new Error(body?.error ? `${res.status}: ${body.error}` : `HTTP ${res.status}`);
|
|
221
|
+
if (body?.version === 0) return null;
|
|
222
|
+
return providerConfigFromValue(provider, body?.value, `config:provider-config/${key}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function providerConfigFromValue(provider: string, value: unknown, path: string): LoadedProviderConfig {
|
|
226
|
+
const raw = recordValue(value);
|
|
227
|
+
const defaults = defaultProviderConfig(provider);
|
|
228
|
+
return {
|
|
229
|
+
path,
|
|
230
|
+
command: stringValue(raw.command) ?? defaults.command,
|
|
231
|
+
defaultArgs: stringArray(raw.defaultArgs) ?? defaults.defaultArgs,
|
|
232
|
+
env: stringRecord(raw.env) ?? defaults.env,
|
|
233
|
+
pluginDirs: stringArray(raw.pluginDirs) ?? defaults.pluginDirs,
|
|
234
|
+
defaultCapabilities: stringArray(raw.defaultCapabilities) ?? defaults.defaultCapabilities,
|
|
235
|
+
defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
|
|
236
|
+
defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
|
|
237
|
+
chatCaptureMode: enumValue(raw.chatCaptureMode, ["final", "full"]) ?? defaults.chatCaptureMode,
|
|
238
|
+
reasoningCapture: booleanValue(raw.reasoningCapture) ?? defaults.reasoningCapture,
|
|
239
|
+
headless: {
|
|
240
|
+
tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
|
|
241
|
+
shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
package/src/automations.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { createCommand } from "./commands-db";
|
|
|
16
16
|
import { cleanEnum, cleanString, cleanStringArray, optionalEnum } from "./validation";
|
|
17
17
|
import { getAgentProfile, getSpawnPolicy } from "./config-store";
|
|
18
18
|
import { buildSpawnCommand, resolveSpawnModelParams } from "./spawn-command";
|
|
19
|
-
import { resolveProviderSelection, type ProviderEffort
|
|
19
|
+
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog"; import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
20
20
|
import { errMessage, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
21
21
|
import { runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
22
22
|
import type {
|
|
@@ -38,7 +38,6 @@ import type {
|
|
|
38
38
|
TaskSeverity,
|
|
39
39
|
UpdateAutomationInput,
|
|
40
40
|
} from "./types";
|
|
41
|
-
|
|
42
41
|
const AUTOMATION_KIND = "scheduled_task";
|
|
43
42
|
const DEFAULT_TIMEZONE = "UTC";
|
|
44
43
|
const DEFAULT_CATCH_UP: AutomationCatchUpPolicy = "skip";
|
|
@@ -200,11 +199,11 @@ function normalizeTargetPolicy(value: unknown): AutomationTargetPolicy {
|
|
|
200
199
|
}
|
|
201
200
|
const provider = optionalEnum(input.provider, "targetPolicy.provider", ["claude", "codex"] as const, "codex");
|
|
202
201
|
const model = cleanString(input.model, "targetPolicy.model", { max: 120 });
|
|
203
|
-
const effort =
|
|
202
|
+
const effort = cleanString(input.effort, "targetPolicy.effort", { max: 120 }) as ProviderEffort | undefined;
|
|
204
203
|
const profile = cleanString(input.profile, "targetPolicy.profile", { max: 120 });
|
|
205
204
|
if (profile && !getAgentProfile(profile)) throw new ValidationError("agent profile not found");
|
|
206
205
|
try {
|
|
207
|
-
resolveProviderSelection({ provider, model, effort });
|
|
206
|
+
resolveProviderSelection({ provider, model, effort }, effectiveProviderCatalogList());
|
|
208
207
|
} catch (error) {
|
|
209
208
|
throw new ValidationError(errMessage(error));
|
|
210
209
|
}
|
package/src/config-store.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { DEFAULT_CONTEXT_THRESHOLD_CONFIG, validateContextThresholdConfig } from
|
|
|
4
4
|
import { DEFAULT_SELF_RESUME_CONFIG, validateSelfResumeConfig } from "./self-resume-config";
|
|
5
5
|
import { TEAM_TEMPLATE_NAMESPACE, validateTeamTemplate } from "./team-template-config";
|
|
6
6
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
7
|
-
import { errMessage, isRecord, SPAWN_PROVIDERS,
|
|
7
|
+
import { errMessage, isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES } from "agent-relay-sdk"; import { effectiveProviderCatalogList } from "./provider-catalog-store";
|
|
8
8
|
import type {
|
|
9
9
|
AgentProfile,
|
|
10
10
|
AgentProfileBase,
|
|
@@ -23,7 +23,6 @@ import type {
|
|
|
23
23
|
WorkspaceConfig,
|
|
24
24
|
WorkspaceLandingPolicy,
|
|
25
25
|
} from "./types";
|
|
26
|
-
|
|
27
26
|
const CONFIG_HISTORY_LIMIT = 50;
|
|
28
27
|
const SPAWN_POLICY_NAMESPACE = "spawn-policy";
|
|
29
28
|
const AGENT_PROFILE_NAMESPACE = "agent-profile";
|
|
@@ -342,7 +341,7 @@ function validateSpawnPolicy(key: string, value: unknown): SpawnPolicy {
|
|
|
342
341
|
workspaceMode: value.workspaceMode === undefined || value.workspaceMode === null ? "inherit" : cleanEnum(value.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES),
|
|
343
342
|
rig: cleanString(value.rig, "rig", { max: 120 }),
|
|
344
343
|
model: cleanString(value.model, "model", { max: 120 }),
|
|
345
|
-
effort:
|
|
344
|
+
effort: cleanString(value.effort, "effort", { max: 120 }) as ProviderEffort | undefined,
|
|
346
345
|
profile: cleanString(value.profile, "profile", { max: 120 }),
|
|
347
346
|
providerArgs: cleanStringArray(value.providerArgs, "providerArgs", { itemMax: 120, maxItems: 100 }) ?? [],
|
|
348
347
|
prompt: cleanString(value.prompt, "prompt", { max: 16_000 }),
|
|
@@ -356,7 +355,7 @@ function validateSpawnPolicy(key: string, value: unknown): SpawnPolicy {
|
|
|
356
355
|
backoff: cleanBackoff(value.backoff),
|
|
357
356
|
};
|
|
358
357
|
try {
|
|
359
|
-
resolveProviderSelection({ provider: policy.provider, model: policy.model, effort: policy.effort });
|
|
358
|
+
resolveProviderSelection({ provider: policy.provider, model: policy.model, effort: policy.effort }, effectiveProviderCatalogList());
|
|
360
359
|
} catch (error) {
|
|
361
360
|
throw new ValidationError(errMessage(error));
|
|
362
361
|
}
|
|
@@ -408,7 +407,7 @@ function validateStewardConfig(value: unknown): StewardConfig {
|
|
|
408
407
|
enabled: value.enabled === undefined ? false : cleanBoolean(value.enabled, "enabled"),
|
|
409
408
|
provider: cleanEnum(value.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider,
|
|
410
409
|
model: cleanString(value.model, "model", { max: 120 }),
|
|
411
|
-
effort:
|
|
410
|
+
effort: cleanString(value.effort, "effort", { max: 120 }) as ProviderEffort | undefined,
|
|
412
411
|
permissionMode: (value.permissionMode === undefined || value.permissionMode === null
|
|
413
412
|
? "open"
|
|
414
413
|
: cleanEnum(value.permissionMode, "permissionMode", VALID_PERMISSION_MODES)) as SpawnApprovalMode,
|
|
@@ -419,7 +418,7 @@ function validateStewardConfig(value: unknown): StewardConfig {
|
|
|
419
418
|
// Reject a provider/model/effort combo the catalog can't resolve before it ever
|
|
420
419
|
// reaches a spawn (same guard as spawn policies).
|
|
421
420
|
try {
|
|
422
|
-
resolveProviderSelection({ provider: config.provider, model: config.model, effort: config.effort });
|
|
421
|
+
resolveProviderSelection({ provider: config.provider, model: config.model, effort: config.effort }, effectiveProviderCatalogList());
|
|
423
422
|
} catch (error) {
|
|
424
423
|
throw new ValidationError(errMessage(error));
|
|
425
424
|
}
|
package/src/connectors.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import type { ConnectorAction, ConnectorActionResult, ConnectorManifest, ConnectorSummary } from "./types";
|
|
5
5
|
import { ValidationError } from "./db";
|
|
6
6
|
import { isRecord } from "agent-relay-sdk";
|
|
7
|
+
import { registerConnectorConfigSetting, validateConnectorConfig } from "./settings/modules";
|
|
7
8
|
|
|
8
9
|
const CONNECTOR_SCHEMA = "agent-relay.connector.v1";
|
|
9
10
|
const VALID_KINDS = new Set(["channel", "event", "provider", "orchestrator"]);
|
|
@@ -99,6 +100,7 @@ function stringArray(value: unknown, field: string): string[] {
|
|
|
99
100
|
|
|
100
101
|
export function registerConnectorManifest(manifest: ConnectorManifest, opts: { config?: Record<string, unknown>; state?: Record<string, unknown> } = {}): ConnectorSummary {
|
|
101
102
|
const normalized = normalizeConnectorManifest(manifest);
|
|
103
|
+
registerConnectorConfigSetting(normalized, { defaults: opts.config });
|
|
102
104
|
const dir = connectorDir(normalized.id);
|
|
103
105
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
104
106
|
writeFileSync(join(dir, "manifest.json"), JSON.stringify(normalized, null, 2) + "\n", { mode: 0o600 });
|
|
@@ -165,6 +167,9 @@ export function readConnectorConfig(id: string): Record<string, unknown> {
|
|
|
165
167
|
|
|
166
168
|
export function writeConnectorConfig(id: string, config: Record<string, unknown>): Record<string, unknown> {
|
|
167
169
|
if (!isRecord(config)) throw new ValidationError("connector config must be an object");
|
|
170
|
+
const connector = getConnector(id);
|
|
171
|
+
if (!connector) throw new ValidationError(`connector ${id} not found`);
|
|
172
|
+
validateConnectorConfig(connector.manifest, config);
|
|
168
173
|
const path = join(connectorDir(id), "config.json");
|
|
169
174
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
170
175
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
package/src/db/migrations.ts
CHANGED
|
@@ -394,6 +394,12 @@ export function applyMigrations(): void {
|
|
|
394
394
|
"UPDATE tasks SET claim_expires_at = coalesce(claimed_at, ?) + ? WHERE claimed_by IS NOT NULL AND claim_expires_at IS NULL",
|
|
395
395
|
).run(Date.now(), CLAIM_LEASE_MS);
|
|
396
396
|
|
|
397
|
+
const providerModelOverrideCols = getDb().query("PRAGMA table_info(provider_model_overrides)").all() as any[];
|
|
398
|
+
const providerModelOverrideColNames = providerModelOverrideCols.map((c: any) => c.name);
|
|
399
|
+
if (!providerModelOverrideColNames.includes("disabled")) {
|
|
400
|
+
getDb().run("ALTER TABLE provider_model_overrides ADD COLUMN disabled INTEGER NOT NULL DEFAULT 0");
|
|
401
|
+
}
|
|
402
|
+
|
|
397
403
|
// Migration: orchestrators.api_url
|
|
398
404
|
const orchCols = getDb().query("PRAGMA table_info(orchestrators)").all() as any[];
|
|
399
405
|
const orchColNames = orchCols.map((c: any) => c.name);
|
package/src/db/schema.ts
CHANGED
|
@@ -829,7 +829,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
829
829
|
provider TEXT NOT NULL,
|
|
830
830
|
alias TEXT NOT NULL,
|
|
831
831
|
entry TEXT NOT NULL DEFAULT '{}',
|
|
832
|
-
deleted INTEGER NOT NULL DEFAULT 0,
|
|
832
|
+
deleted INTEGER NOT NULL DEFAULT 0, disabled INTEGER NOT NULL DEFAULT 0,
|
|
833
833
|
updated_at INTEGER NOT NULL,
|
|
834
834
|
updated_by TEXT,
|
|
835
835
|
PRIMARY KEY (provider, alias)
|
package/src/index.ts
CHANGED
|
@@ -268,13 +268,16 @@ export function createFetchHandler(
|
|
|
268
268
|
if (!publicPaths.includes(url.pathname)) {
|
|
269
269
|
const integrationAuth = getIntegrationAuth(req);
|
|
270
270
|
const componentAuth = getComponentAuth(req);
|
|
271
|
+
const handlerOwnsScopedAuth = url.pathname === "/api/tokens/me" ||
|
|
272
|
+
url.pathname === "/api/settings" ||
|
|
273
|
+
url.pathname.startsWith("/api/settings/");
|
|
271
274
|
if (!isAuthorized(req)) {
|
|
272
275
|
if (!integrationAuth) {
|
|
273
276
|
return unauthorized(req);
|
|
274
277
|
}
|
|
275
|
-
if (!isScopedRequestAuthorized(req)) return forbidden(req);
|
|
278
|
+
if (!handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return forbidden(req);
|
|
276
279
|
}
|
|
277
|
-
if (componentAuth && !isScopedRequestAuthorized(req)) return forbidden(req);
|
|
280
|
+
if (componentAuth && !handlerOwnsScopedAuth && !isScopedRequestAuthorized(req)) return forbidden(req);
|
|
278
281
|
}
|
|
279
282
|
const response = await matched.handler(req, matched.params);
|
|
280
283
|
applyCors(req, response);
|
|
@@ -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
|
@@ -52,7 +52,7 @@ import { BLACKBOARD_TOOLS, relayBlackboardTool } from "./mcp-blackboard-tools";
|
|
|
52
52
|
import { PROJECT_TOOLS, relayProjectTool } from "./mcp-project-tools";
|
|
53
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";
|
|
@@ -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",
|
|
@@ -1140,10 +1140,10 @@ function sniffMediaType(bytes: Uint8Array, hinted?: string, filename?: string):
|
|
|
1140
1140
|
function visibleTools(auth: McpAuthContext): Array<Record<string, unknown>> {
|
|
1141
1141
|
return TOOLS
|
|
1142
1142
|
.filter((tool) => hasAnyScope(auth, tool.requiredScopes))
|
|
1143
|
-
.map(({ name, description,
|
|
1143
|
+
.map(({ name, description, alwaysLoad, ...tool }) => ({
|
|
1144
1144
|
name,
|
|
1145
1145
|
description,
|
|
1146
|
-
inputSchema,
|
|
1146
|
+
inputSchema: providerCatalogToolInputSchema(name, tool.inputSchema),
|
|
1147
1147
|
...(alwaysLoad ? { _meta: { "anthropic/alwaysLoad": true } } : {}),
|
|
1148
1148
|
}));
|
|
1149
1149
|
}
|
|
@@ -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 ?? [];
|