agent-relay-server 0.18.0 → 0.19.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/package.json +2 -2
- package/public/index.html +14 -10
- package/runner/src/config.ts +2 -1
- package/src/automations.ts +13 -28
- package/src/bus.ts +2 -2
- package/src/cli.ts +68 -7
- package/src/config-store.ts +9 -27
- package/src/daemon.ts +1 -4
- package/src/db.ts +9 -2
- package/src/dev.ts +1 -4
- package/src/http-body.ts +49 -0
- package/src/index.ts +2 -1
- package/src/maintenance.ts +3 -3
- package/src/mcp.ts +17 -68
- package/src/memory-broker-smoke.ts +2 -2
- package/src/memory-command-broker.ts +2 -2
- package/src/memory-http-broker.ts +2 -2
- package/src/orchestrator-lookup.ts +29 -0
- package/src/recipe-validator.ts +2 -2
- package/src/routes.ts +180 -179
- package/src/setup.ts +1 -4
- package/src/spawn-command.ts +2 -1
- package/src/steward.ts +2 -1
- package/src/upgrade.ts +38 -12
- package/src/validation.ts +54 -2
package/src/mcp.ts
CHANGED
|
@@ -3,6 +3,9 @@ import { getArtifactStorage, maxArtifactBytes, normalizeDigest } from "./artifac
|
|
|
3
3
|
import { createCommand } from "./commands-db";
|
|
4
4
|
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams, type SpawnModelParams } from "./spawn-command";
|
|
5
5
|
import { isPathWithinBase } from "./utils";
|
|
6
|
+
import { optionalEnum } from "./validation";
|
|
7
|
+
import { listManagedOrchestratorsForAgent } from "./orchestrator-lookup";
|
|
8
|
+
import { bytesToStream, readBodyBytes } from "./http-body";
|
|
6
9
|
import { MAX_BODY_BYTES, VERSION } from "./config";
|
|
7
10
|
import { getManagedAgentState, getSpawnPolicy, listSpawnPolicies } from "./config-store";
|
|
8
11
|
import {
|
|
@@ -34,7 +37,7 @@ import {
|
|
|
34
37
|
} from "./security";
|
|
35
38
|
import type { ActivityKind, AgentCard, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider } from "./types";
|
|
36
39
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
37
|
-
import { isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
|
|
40
|
+
import { errMessage, isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
|
|
38
41
|
import { childRunnerRuntimeTokenEnv, runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
39
42
|
|
|
40
43
|
type JsonRpcId = string | number | null;
|
|
@@ -272,7 +275,7 @@ export async function postMcp(req: Request): Promise<Response> {
|
|
|
272
275
|
}
|
|
273
276
|
return Response.json(jsonRpcError(id, -32601, `unknown MCP method: ${method}`));
|
|
274
277
|
} catch (error) {
|
|
275
|
-
const message =
|
|
278
|
+
const message = errMessage(error);
|
|
276
279
|
const status = error instanceof McpAuthError ? 403 : error instanceof McpNotFoundError ? 404 : 400;
|
|
277
280
|
const code = error instanceof McpAuthError ? -32001 : error instanceof McpNotFoundError ? -32004 : -32602;
|
|
278
281
|
return Response.json(jsonRpcError(id, code, message, { status }));
|
|
@@ -284,23 +287,11 @@ async function parseJsonRpcRequest(req: Request): Promise<
|
|
|
284
287
|
| { ok: false; status: number; error: string }
|
|
285
288
|
> {
|
|
286
289
|
if (!req.body) return { ok: false, status: 400, error: "JSON-RPC body required" };
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
while (true) {
|
|
291
|
-
const { done, value } = await reader.read();
|
|
292
|
-
if (done) break;
|
|
293
|
-
if (!value) continue;
|
|
294
|
-
total += value.byteLength;
|
|
295
|
-
const maxBodyBytes = Math.max(MAX_BODY_BYTES, maxArtifactBytes() + MCP_BODY_OVERHEAD_BYTES);
|
|
296
|
-
if (total > maxBodyBytes) {
|
|
297
|
-
await reader.cancel().catch(() => {});
|
|
298
|
-
return { ok: false, status: 413, error: `request body exceeds ${maxBodyBytes} bytes` };
|
|
299
|
-
}
|
|
300
|
-
chunks.push(value);
|
|
301
|
-
}
|
|
290
|
+
const maxBodyBytes = Math.max(MAX_BODY_BYTES, maxArtifactBytes() + MCP_BODY_OVERHEAD_BYTES);
|
|
291
|
+
const read = await readBodyBytes(req.body, maxBodyBytes);
|
|
292
|
+
if (!read.ok) return read;
|
|
302
293
|
try {
|
|
303
|
-
const body = JSON.parse(new TextDecoder().decode(
|
|
294
|
+
const body = JSON.parse(new TextDecoder().decode(read.bytes)) as unknown;
|
|
304
295
|
if (!isRecord(body)) return { ok: false, status: 400, error: "JSON-RPC body must be an object" };
|
|
305
296
|
return { ok: true, request: body as JsonRpcRequest };
|
|
306
297
|
} catch {
|
|
@@ -308,17 +299,6 @@ async function parseJsonRpcRequest(req: Request): Promise<
|
|
|
308
299
|
}
|
|
309
300
|
}
|
|
310
301
|
|
|
311
|
-
function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
312
|
-
const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
313
|
-
const output = new Uint8Array(total);
|
|
314
|
-
let offset = 0;
|
|
315
|
-
for (const chunk of chunks) {
|
|
316
|
-
output.set(chunk, offset);
|
|
317
|
-
offset += chunk.byteLength;
|
|
318
|
-
}
|
|
319
|
-
return output;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
302
|
function mcpAuthContext(req: Request): McpAuthContext {
|
|
323
303
|
const component = getComponentAuth(req);
|
|
324
304
|
if (component) return { actor: component.sub, kind: "component", scopes: component.scope, component };
|
|
@@ -354,7 +334,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
354
334
|
auditToolCall(auth, name, "ok", result);
|
|
355
335
|
return toolResult(result);
|
|
356
336
|
} catch (error) {
|
|
357
|
-
const message =
|
|
337
|
+
const message = errMessage(error);
|
|
358
338
|
auditToolCall(auth, name, error instanceof McpAuthError ? "denied" : "error", undefined, message);
|
|
359
339
|
throw error;
|
|
360
340
|
}
|
|
@@ -732,7 +712,7 @@ function selectControlOrchestrator(input: {
|
|
|
732
712
|
return orchestrator;
|
|
733
713
|
}
|
|
734
714
|
const agent = input.agentId ? getAgent(input.agentId) : null;
|
|
735
|
-
const orchestrator = agent ? managedControlOrchestrator(agent, input) :
|
|
715
|
+
const orchestrator = agent ? managedControlOrchestrator(agent, input) : (listManagedOrchestratorsForAgent(input)[0] ?? null);
|
|
736
716
|
if (!orchestrator) throw new McpNotFoundError("no orchestrator found for agent control target");
|
|
737
717
|
if (orchestrator.status !== "online") throw new ValidationError("orchestrator is offline");
|
|
738
718
|
return orchestrator;
|
|
@@ -742,29 +722,13 @@ function managedControlOrchestrator(
|
|
|
742
722
|
agent: AgentCard,
|
|
743
723
|
input: { policyName?: string; spawnRequestId?: string; tmuxSession?: string },
|
|
744
724
|
): NonNullable<ReturnType<typeof getOrchestrator>> | null {
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
const metaSpawnRequestId = input.spawnRequestId ?? (typeof agent.meta?.spawnRequestId === "string" ? agent.meta.spawnRequestId : "");
|
|
748
|
-
return findManagedOrchestrator({
|
|
725
|
+
const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
|
|
726
|
+
return listManagedOrchestratorsForAgent({
|
|
749
727
|
agentId: agent.id,
|
|
750
|
-
policyName:
|
|
751
|
-
spawnRequestId:
|
|
752
|
-
tmuxSession: input.tmuxSession ??
|
|
753
|
-
}) ?? (agent.machine ? getOrchestrator(agent.machine) : null);
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function findManagedOrchestrator(input: {
|
|
757
|
-
agentId?: string;
|
|
758
|
-
policyName?: string;
|
|
759
|
-
spawnRequestId?: string;
|
|
760
|
-
tmuxSession?: string;
|
|
761
|
-
}): NonNullable<ReturnType<typeof getOrchestrator>> | null {
|
|
762
|
-
return listOrchestrators().find((orchestrator) => orchestrator.managedAgents.some((managed) =>
|
|
763
|
-
(!!input.agentId && managed.agentId === input.agentId) ||
|
|
764
|
-
(!!input.tmuxSession && managed.tmuxSession === input.tmuxSession) ||
|
|
765
|
-
(!!input.policyName && managed.policyName === input.policyName) ||
|
|
766
|
-
(!!input.spawnRequestId && managed.spawnRequestId === input.spawnRequestId)
|
|
767
|
-
)) ?? null;
|
|
728
|
+
policyName: input.policyName ?? str(agent.meta?.policyName),
|
|
729
|
+
spawnRequestId: input.spawnRequestId ?? str(agent.meta?.spawnRequestId),
|
|
730
|
+
tmuxSession: input.tmuxSession ?? str(agent.meta?.tmuxSession),
|
|
731
|
+
})[0] ?? (agent.machine ? getOrchestrator(agent.machine) : null);
|
|
768
732
|
}
|
|
769
733
|
|
|
770
734
|
function providerSelection(provider: SpawnProvider, args: Record<string, unknown>): SpawnModelParams {
|
|
@@ -773,15 +737,6 @@ function providerSelection(provider: SpawnProvider, args: Record<string, unknown
|
|
|
773
737
|
return resolveSpawnModelParams(provider, model, effort);
|
|
774
738
|
}
|
|
775
739
|
|
|
776
|
-
function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
777
|
-
return new ReadableStream<Uint8Array>({
|
|
778
|
-
start(controller) {
|
|
779
|
-
controller.enqueue(bytes);
|
|
780
|
-
controller.close();
|
|
781
|
-
},
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
|
|
785
740
|
function artifactBytes(args: Record<string, unknown>): Uint8Array {
|
|
786
741
|
const hasContent = args.content !== undefined && args.content !== null;
|
|
787
742
|
const hasBase64 = args.base64 !== undefined && args.base64 !== null;
|
|
@@ -956,12 +911,6 @@ function optionalBoolean(value: unknown, field: string): boolean | undefined {
|
|
|
956
911
|
return value;
|
|
957
912
|
}
|
|
958
913
|
|
|
959
|
-
function optionalEnum<T extends readonly string[]>(value: unknown, field: string, valid: T): T[number] | undefined {
|
|
960
|
-
if (value === undefined || value === null) return undefined;
|
|
961
|
-
if (typeof value !== "string" || !valid.includes(value as T[number])) throw new ValidationError(`${field} must be one of: ${valid.join(", ")}`);
|
|
962
|
-
return value as T[number];
|
|
963
|
-
}
|
|
964
|
-
|
|
965
914
|
function enumField<T extends readonly string[]>(value: unknown, field: string, valid: T): T[number] {
|
|
966
915
|
const cleaned = optionalEnum(value, field, valid);
|
|
967
916
|
if (!cleaned) throw new ValidationError(`${field} required`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
1
|
+
import { errMessage, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
2
2
|
|
|
3
3
|
interface MemoryBrokerSmokeOptions {
|
|
4
4
|
baseUrl?: string;
|
|
@@ -137,7 +137,7 @@ async function step<T>(steps: MemoryBrokerSmokeStep[], name: string, detail: str
|
|
|
137
137
|
steps.push({ name, ok: true, detail });
|
|
138
138
|
return result;
|
|
139
139
|
} catch (error) {
|
|
140
|
-
const message =
|
|
140
|
+
const message = errMessage(error);
|
|
141
141
|
steps.push({ name, ok: false, detail: message });
|
|
142
142
|
throw error;
|
|
143
143
|
}
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
normalizeMemorySearchResult,
|
|
21
21
|
} from "./memory-broker-contract";
|
|
22
22
|
import { normalizeContextPackage } from "./memory-http-broker";
|
|
23
|
-
import { isRecord } from "agent-relay-sdk";
|
|
23
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
24
24
|
|
|
25
25
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
26
26
|
|
|
@@ -105,7 +105,7 @@ export class CommandMemoryBroker implements MemoryBroker {
|
|
|
105
105
|
return unwrapResult(parseJson(stdout, operation));
|
|
106
106
|
} catch (error) {
|
|
107
107
|
if (error instanceof MemoryBrokerContractError) throw error;
|
|
108
|
-
throw new MemoryBrokerContractError(`command memory broker ${operation} failed: ${
|
|
108
|
+
throw new MemoryBrokerContractError(`command memory broker ${operation} failed: ${errMessage(error)}`);
|
|
109
109
|
} finally {
|
|
110
110
|
clearTimeout(timer);
|
|
111
111
|
}
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
normalizeMemoryBrokerCapabilities,
|
|
20
20
|
normalizeMemorySearchResult,
|
|
21
21
|
} from "./memory-broker-contract";
|
|
22
|
-
import { isRecord } from "agent-relay-sdk";
|
|
22
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
23
23
|
|
|
24
24
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
25
25
|
|
|
@@ -99,7 +99,7 @@ export class HttpMemoryBroker implements MemoryBroker {
|
|
|
99
99
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
100
100
|
throw new MemoryBrokerContractError(`http memory broker ${operation} timed out after ${this.timeoutMs}ms`);
|
|
101
101
|
}
|
|
102
|
-
throw new MemoryBrokerContractError(`http memory broker ${operation} failed: ${
|
|
102
|
+
throw new MemoryBrokerContractError(`http memory broker ${operation} failed: ${errMessage(error)}`);
|
|
103
103
|
} finally {
|
|
104
104
|
clearTimeout(timer);
|
|
105
105
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { listOrchestrators } from "./db";
|
|
2
|
+
|
|
3
|
+
export interface ManagedAgentMatch {
|
|
4
|
+
agentId?: string;
|
|
5
|
+
sessionName?: string;
|
|
6
|
+
tmuxSession?: string;
|
|
7
|
+
policyName?: string;
|
|
8
|
+
spawnRequestId?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Every orchestrator that owns a managed agent matching ANY provided identity
|
|
13
|
+
* key. Single home for the managed-agent→orchestrator lookup that routes.ts and
|
|
14
|
+
* mcp.ts each open-coded. routes also matches `sessionName`; mcp never passed it,
|
|
15
|
+
* so leaving the key undefined reproduces mcp's behavior exactly. Callers layer
|
|
16
|
+
* their own online / runner-managed / machine-fallback policy on top — routes
|
|
17
|
+
* picks the first online candidate, mcp takes the first match.
|
|
18
|
+
*/
|
|
19
|
+
export function listManagedOrchestratorsForAgent(match: ManagedAgentMatch): ReturnType<typeof listOrchestrators> {
|
|
20
|
+
return listOrchestrators().filter((orch) =>
|
|
21
|
+
orch.managedAgents.some((managed) =>
|
|
22
|
+
(!!match.agentId && managed.agentId === match.agentId) ||
|
|
23
|
+
(!!match.sessionName && managed.sessionName === match.sessionName) ||
|
|
24
|
+
(!!match.tmuxSession && managed.tmuxSession === match.tmuxSession) ||
|
|
25
|
+
(!!match.policyName && managed.policyName === match.policyName) ||
|
|
26
|
+
(!!match.spawnRequestId && managed.spawnRequestId === match.spawnRequestId),
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/recipe-validator.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MemoryType, Recipe, RecipeAgent, RecipeMemoryPolicy } from "./types";
|
|
2
2
|
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
3
|
-
import { isRecord } from "agent-relay-sdk";
|
|
3
|
+
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
4
4
|
|
|
5
5
|
class RecipeValidationError extends Error {}
|
|
6
6
|
|
|
@@ -52,7 +52,7 @@ function validateAgent(value: unknown): RecipeAgent {
|
|
|
52
52
|
try {
|
|
53
53
|
resolveProviderSelection({ provider, model, effort });
|
|
54
54
|
} catch (error) {
|
|
55
|
-
throw new RecipeValidationError(
|
|
55
|
+
throw new RecipeValidationError(errMessage(error));
|
|
56
56
|
}
|
|
57
57
|
return {
|
|
58
58
|
role: requiredString(value.role, "agent.role"),
|