agent-relay-server 0.16.0 → 0.18.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 +201 -1
- package/package.json +2 -2
- package/public/index.html +100 -25
- package/public/sw.js +51 -16
- package/runner/src/adapter.ts +1 -4
- package/runner/src/config.ts +1 -4
- package/scripts/orchestrator-spawn-smoke.ts +2 -1
- package/src/automations.ts +8 -31
- package/src/bus.ts +2 -17
- package/src/cli.ts +179 -3
- package/src/command-events.ts +26 -0
- package/src/config-store.ts +64 -22
- package/src/connectors.ts +1 -4
- package/src/contracts.ts +2 -8
- package/src/db.ts +36 -18
- package/src/index.ts +99 -4
- package/src/lifecycle-manager.ts +11 -24
- package/src/maintenance.ts +26 -20
- package/src/managed-policy.ts +8 -26
- package/src/mcp.ts +19 -43
- package/src/memory-broker-smoke.ts +3 -1
- package/src/memory-command-broker.ts +1 -4
- package/src/memory-http-broker.ts +1 -4
- package/src/memory-service.ts +1 -4
- package/src/memory-sqlite-broker.ts +1 -8
- package/src/provider-catalog-store.ts +3 -11
- package/src/recipe-loader.ts +1 -4
- package/src/recipe-validator.ts +1 -4
- package/src/routes.ts +290 -139
- package/src/security.ts +3 -7
- package/src/spawn-command.ts +150 -0
- package/src/sse.ts +1 -4
- package/src/steward.ts +16 -21
- package/src/upgrade.ts +3 -2
- package/src/utils.ts +38 -0
- package/src/validation.ts +28 -0
- package/src/workspace-claim.ts +29 -0
- package/src/workspace-merge.ts +21 -9
package/src/mcp.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
2
|
import { getArtifactStorage, maxArtifactBytes, normalizeDigest } from "./artifact-storage";
|
|
5
3
|
import { createCommand } from "./commands-db";
|
|
4
|
+
import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams, type SpawnModelParams } from "./spawn-command";
|
|
5
|
+
import { isPathWithinBase } from "./utils";
|
|
6
6
|
import { MAX_BODY_BYTES, VERSION } from "./config";
|
|
7
7
|
import { getManagedAgentState, getSpawnPolicy, listSpawnPolicies } from "./config-store";
|
|
8
8
|
import {
|
|
@@ -33,7 +33,8 @@ import {
|
|
|
33
33
|
isIntegrationAllowed,
|
|
34
34
|
} from "./security";
|
|
35
35
|
import type { ActivityKind, AgentCard, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider } from "./types";
|
|
36
|
-
import {
|
|
36
|
+
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
37
|
+
import { isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
|
|
37
38
|
import { childRunnerRuntimeTokenEnv, runnerRuntimeTokenEnv } from "./runtime-tokens";
|
|
38
39
|
|
|
39
40
|
type JsonRpcId = string | number | null;
|
|
@@ -66,9 +67,7 @@ const VALID_ARTIFACT_KINDS = ["image", "audio", "video", "document", "archive",
|
|
|
66
67
|
const VALID_ARTIFACT_SENSITIVITIES = ["public", "normal", "sensitive", "secret"] as const;
|
|
67
68
|
const VALID_ARTIFACT_ROLES = ["media", "patch", "report", "log", "output", "input"] as const;
|
|
68
69
|
const VALID_ARTIFACT_ENTITY_TYPES = ["message", "task", "recipeRun", "recipeStep", "channelEvent"] as const;
|
|
69
|
-
|
|
70
|
-
const VALID_SPAWN_APPROVALS = ["open", "guarded", "read-only"] as const;
|
|
71
|
-
const VALID_PROVIDER_EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
70
|
+
|
|
72
71
|
|
|
73
72
|
const TOOLS: ToolDefinition[] = [
|
|
74
73
|
{
|
|
@@ -201,13 +200,13 @@ const TOOLS: ToolDefinition[] = [
|
|
|
201
200
|
inputSchema: {
|
|
202
201
|
type: "object",
|
|
203
202
|
properties: {
|
|
204
|
-
provider: { type: "string", enum:
|
|
203
|
+
provider: { type: "string", enum: SPAWN_PROVIDERS },
|
|
205
204
|
orchestratorId: { type: "string" },
|
|
206
205
|
cwd: { type: "string" },
|
|
207
206
|
label: { type: "string" },
|
|
208
207
|
model: { type: "string" },
|
|
209
|
-
effort: { type: "string", enum:
|
|
210
|
-
approvalMode: { type: "string", enum:
|
|
208
|
+
effort: { type: "string", enum: VALID_EFFORTS },
|
|
209
|
+
approvalMode: { type: "string", enum: APPROVAL_MODES },
|
|
211
210
|
prompt: { type: "string" },
|
|
212
211
|
systemPromptAppend: { type: "string" },
|
|
213
212
|
tags: { type: "array", items: { type: "string" } },
|
|
@@ -519,16 +518,16 @@ function relayAgentStatus(args: Record<string, unknown>): Record<string, unknown
|
|
|
519
518
|
}
|
|
520
519
|
|
|
521
520
|
function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
522
|
-
const provider = enumField(args.provider, "provider",
|
|
521
|
+
const provider = enumField(args.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider;
|
|
523
522
|
const cwd = optionalString(args.cwd, "cwd", 500);
|
|
524
523
|
const orchestrator = selectSpawnOrchestrator(provider, optionalString(args.orchestratorId, "orchestratorId", 200), cwd);
|
|
525
524
|
const resolvedCwd = cwd || orchestrator.baseDir;
|
|
526
|
-
if (cwd && !
|
|
525
|
+
if (cwd && !isPathWithinBase(cwd, orchestrator.baseDir)) {
|
|
527
526
|
throw new ValidationError(`cwd must be within orchestrator base directory: ${orchestrator.baseDir}`);
|
|
528
527
|
}
|
|
529
528
|
const selection = providerSelection(provider, args);
|
|
530
|
-
const approvalMode = optionalEnum(args.approvalMode, "approvalMode",
|
|
531
|
-
const spawnRequestId = optionalString(args.spawnRequestId, "spawnRequestId", 160) ??
|
|
529
|
+
const approvalMode = optionalEnum(args.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined ?? "guarded";
|
|
530
|
+
const spawnRequestId = optionalString(args.spawnRequestId, "spawnRequestId", 160) ?? generateSpawnRequestId();
|
|
532
531
|
const label = optionalString(args.label, "label", 120);
|
|
533
532
|
const policyName = optionalString(args.policyName, "policyName", 120);
|
|
534
533
|
assertComponentResourceAllowed(auth, {
|
|
@@ -558,12 +557,9 @@ function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): R
|
|
|
558
557
|
source: "system",
|
|
559
558
|
target: orchestrator.agentId,
|
|
560
559
|
correlationId: spawnRequestId,
|
|
561
|
-
params: {
|
|
562
|
-
action: "spawn",
|
|
560
|
+
params: buildSpawnCommand({
|
|
563
561
|
provider,
|
|
564
|
-
|
|
565
|
-
providerModel: selection.providerModel,
|
|
566
|
-
effort: selection.effort,
|
|
562
|
+
modelParams: selection,
|
|
567
563
|
cwd: resolvedCwd,
|
|
568
564
|
label,
|
|
569
565
|
tags: optionalStringArray(args.tags, "tags") ?? [],
|
|
@@ -580,7 +576,7 @@ function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): R
|
|
|
580
576
|
requestedVia: "mcp",
|
|
581
577
|
requestedAt: Date.now(),
|
|
582
578
|
orchestratorId: orchestrator.id,
|
|
583
|
-
},
|
|
579
|
+
}),
|
|
584
580
|
});
|
|
585
581
|
emitCommand(command);
|
|
586
582
|
return { ok: true, orchestratorId: orchestrator.id, provider, command };
|
|
@@ -714,7 +710,7 @@ function selectSpawnOrchestrator(provider: SpawnProvider, orchestratorId?: strin
|
|
|
714
710
|
}
|
|
715
711
|
const candidates = listOrchestrators().filter((item) => item.status === "online" && item.providers.includes(provider));
|
|
716
712
|
if (cwd) {
|
|
717
|
-
const match = candidates.find((item) =>
|
|
713
|
+
const match = candidates.find((item) => isPathWithinBase(cwd, item.baseDir));
|
|
718
714
|
if (match) return match;
|
|
719
715
|
}
|
|
720
716
|
const orchestrator = candidates[0];
|
|
@@ -771,26 +767,10 @@ function findManagedOrchestrator(input: {
|
|
|
771
767
|
)) ?? null;
|
|
772
768
|
}
|
|
773
769
|
|
|
774
|
-
function providerSelection(provider: SpawnProvider, args: Record<string, unknown>):
|
|
770
|
+
function providerSelection(provider: SpawnProvider, args: Record<string, unknown>): SpawnModelParams {
|
|
775
771
|
const model = optionalString(args.model, "model", 120);
|
|
776
|
-
const effort = optionalEnum(args.effort, "effort",
|
|
777
|
-
|
|
778
|
-
const resolved = resolveProviderSelection({ provider, model, effort });
|
|
779
|
-
return {
|
|
780
|
-
model: resolved.modelAlias,
|
|
781
|
-
providerModel: resolved.providerModel,
|
|
782
|
-
effort: resolved.effort,
|
|
783
|
-
};
|
|
784
|
-
} catch (error) {
|
|
785
|
-
throw new ValidationError(error instanceof Error ? error.message : String(error));
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function pathWithinBase(path: string, baseDir: string): boolean {
|
|
790
|
-
const base = resolve(baseDir);
|
|
791
|
-
const target = resolve(path);
|
|
792
|
-
const rel = relative(base, target);
|
|
793
|
-
return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
|
|
772
|
+
const effort = optionalEnum(args.effort, "effort", VALID_EFFORTS) as ProviderEffort | undefined;
|
|
773
|
+
return resolveSpawnModelParams(provider, model, effort);
|
|
794
774
|
}
|
|
795
775
|
|
|
796
776
|
function bytesToStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
@@ -937,10 +917,6 @@ function auditToolCall(
|
|
|
937
917
|
}
|
|
938
918
|
}
|
|
939
919
|
|
|
940
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
941
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
920
|
function recordField(value: unknown, field: string): Record<string, unknown> {
|
|
945
921
|
if (!isRecord(value)) throw new ValidationError(`${field} must be an object`);
|
|
946
922
|
return value;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
2
|
+
|
|
1
3
|
interface MemoryBrokerSmokeOptions {
|
|
2
4
|
baseUrl?: string;
|
|
3
5
|
token?: string;
|
|
@@ -39,7 +41,7 @@ export async function runMemoryBrokerSmoke(options: MemoryBrokerSmokeOptions = {
|
|
|
39
41
|
|
|
40
42
|
const request = async <T>(method: string, path: string, body?: unknown): Promise<T> => {
|
|
41
43
|
const headers: Record<string, string> = {};
|
|
42
|
-
if (token) headers[
|
|
44
|
+
if (token) headers[RELAY_TOKEN_HEADER] = token;
|
|
43
45
|
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
44
46
|
const response = await fetchImpl(new URL(path, baseUrl), {
|
|
45
47
|
method,
|
|
@@ -20,6 +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
24
|
|
|
24
25
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
25
26
|
|
|
@@ -155,7 +156,3 @@ function numberField(value: unknown, field: string): number {
|
|
|
155
156
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new MemoryBrokerContractError(`${field} must be a number`);
|
|
156
157
|
return value;
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
160
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
161
|
-
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
normalizeMemoryBrokerCapabilities,
|
|
20
20
|
normalizeMemorySearchResult,
|
|
21
21
|
} from "./memory-broker-contract";
|
|
22
|
+
import { isRecord } from "agent-relay-sdk";
|
|
22
23
|
|
|
23
24
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
24
25
|
|
|
@@ -182,7 +183,3 @@ function requireRecord(value: unknown, field: string): Record<string, unknown> {
|
|
|
182
183
|
if (!isRecord(value)) throw new MemoryBrokerContractError(`${field} must be an object`);
|
|
183
184
|
return value;
|
|
184
185
|
}
|
|
185
|
-
|
|
186
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
187
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
188
|
-
}
|
package/src/memory-service.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCommand } from "./commands-db";
|
|
2
2
|
import { getAgent, ValidationError } from "./db";
|
|
3
|
+
import { isRecord } from "agent-relay-sdk";
|
|
3
4
|
import { assembleContextPackage, contextBudgetForAgent, estimateMemoryTokens } from "./memory-broker";
|
|
4
5
|
import { createMemoryBrokerRegistry, memoryBrokerConfigFromEnv } from "./memory-broker-registry";
|
|
5
6
|
import { filterMemoriesForInjection, formatUntrustedMemoryBlock } from "./memory-broker-contract";
|
|
@@ -349,7 +350,3 @@ function stringArray(value: unknown): string[] | undefined {
|
|
|
349
350
|
function isPositiveInteger(value: unknown): value is number {
|
|
350
351
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
351
352
|
}
|
|
352
|
-
|
|
353
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
354
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
355
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { Database } from "bun:sqlite";
|
|
3
3
|
import { getDb, ValidationError } from "./db";
|
|
4
|
+
import { parseJson } from "./utils";
|
|
4
5
|
import type {
|
|
5
6
|
ActiveMemoryClearReason,
|
|
6
7
|
ContextPackage,
|
|
@@ -327,14 +328,6 @@ function rowToMemory(row: MemoryRow): Memory {
|
|
|
327
328
|
});
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
function parseJson<T>(raw: string, fallback: T): T {
|
|
331
|
-
try {
|
|
332
|
-
return JSON.parse(raw) as T;
|
|
333
|
-
} catch {
|
|
334
|
-
return fallback;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
331
|
function tagOverlap(tags: string[], queryTags: Set<string>): number {
|
|
339
332
|
if (queryTags.size === 0) return 0;
|
|
340
333
|
const memoryTags = new Set(tags.map((tag) => tag.toLowerCase()));
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
type ProviderCatalogEntry,
|
|
4
4
|
type ProviderModelCatalogEntry,
|
|
5
5
|
} from "agent-relay-sdk/provider-catalog";
|
|
6
|
+
import { SPAWN_PROVIDERS } from "agent-relay-sdk";
|
|
6
7
|
import type { SpawnProvider } from "./types";
|
|
7
8
|
import { getDb, ValidationError } from "./db";
|
|
8
|
-
|
|
9
|
-
const VALID_PROVIDERS: SpawnProvider[] = ["claude", "codex"];
|
|
9
|
+
import { parseJson } from "./utils";
|
|
10
10
|
|
|
11
11
|
interface ProviderModelOverrideRow {
|
|
12
12
|
provider: SpawnProvider;
|
|
@@ -111,7 +111,7 @@ export function mergeProviderCatalog(
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function normalizeOverrideInput(input: ProviderModelOverrideInput): ProviderModelOverrideInput {
|
|
114
|
-
if (!
|
|
114
|
+
if (!SPAWN_PROVIDERS.includes(input.provider)) throw new ValidationError("provider must be claude or codex");
|
|
115
115
|
const alias = input.alias.trim();
|
|
116
116
|
if (!alias) throw new ValidationError("model alias is required");
|
|
117
117
|
if (input.entry.alias !== alias) throw new ValidationError("model entry alias must match override alias");
|
|
@@ -154,11 +154,3 @@ function cloneProviderEntry(entry: ProviderCatalogEntry): ProviderCatalogEntry {
|
|
|
154
154
|
function cloneModelEntry(entry: ProviderModelCatalogEntry): ProviderModelCatalogEntry {
|
|
155
155
|
return JSON.parse(JSON.stringify(entry)) as ProviderModelCatalogEntry;
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
function parseJson<T>(raw: string, fallback: T): T {
|
|
159
|
-
try {
|
|
160
|
-
return JSON.parse(raw) as T;
|
|
161
|
-
} catch {
|
|
162
|
-
return fallback;
|
|
163
|
-
}
|
|
164
|
-
}
|
package/src/recipe-loader.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
3
|
import { validateRecipe } from "./recipe-validator";
|
|
4
4
|
import type { Recipe } from "./types";
|
|
5
|
+
import { isRecord } from "agent-relay-sdk";
|
|
5
6
|
|
|
6
7
|
interface LoadedRecipe {
|
|
7
8
|
name: string;
|
|
@@ -113,7 +114,3 @@ function parseScalar(value: string): unknown {
|
|
|
113
114
|
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
114
115
|
return trimmed.replace(/^["']|["']$/g, "");
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
118
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
119
|
-
}
|
package/src/recipe-validator.ts
CHANGED
|
@@ -1,5 +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
4
|
|
|
4
5
|
class RecipeValidationError extends Error {}
|
|
5
6
|
|
|
@@ -131,7 +132,3 @@ function optionalPositiveInteger(value: unknown, field: string): number | undefi
|
|
|
131
132
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new RecipeValidationError(`${field} must be a positive integer`);
|
|
132
133
|
return value;
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
136
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
137
|
-
}
|