agent-relay-server 0.62.3 → 0.63.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 +1 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-DRrxfjyT.js → MessageBubble-Cl6ufPHw.js} +2 -2
- package/public/assets/MessageBubble-Cl6ufPHw.js.map +1 -0
- package/public/assets/{PlanGraphPanel-Dp2eYazz.js → PlanGraphPanel-BnfjmkQ8.js} +2 -2
- package/public/assets/{PlanGraphPanel-Dp2eYazz.js.map → PlanGraphPanel-BnfjmkQ8.js.map} +1 -1
- package/public/assets/{activity--t0U2yzq.js → activity-aFxvGLtd.js} +2 -2
- package/public/assets/{activity--t0U2yzq.js.map → activity-aFxvGLtd.js.map} +1 -1
- package/public/assets/{agent-profiles-BMtJYzxn.js → agent-profiles-B4mleUDz.js} +2 -2
- package/public/assets/{agent-profiles-BMtJYzxn.js.map → agent-profiles-B4mleUDz.js.map} +1 -1
- package/public/assets/{agents-Ck9AePlc.js → agents-DW5djToQ.js} +2 -2
- package/public/assets/{agents-Ck9AePlc.js.map → agents-DW5djToQ.js.map} +1 -1
- package/public/assets/{analytics-CzfK387L.js → analytics-CRi6thtg.js} +2 -2
- package/public/assets/{analytics-CzfK387L.js.map → analytics-CRi6thtg.js.map} +1 -1
- package/public/assets/{automation-Cgw8vtE8.js → automation-Do4OpauI.js} +2 -2
- package/public/assets/{automation-Cgw8vtE8.js.map → automation-Do4OpauI.js.map} +1 -1
- package/public/assets/{branch-state-badge-DnEEPtOP.js → branch-state-badge-D3W3w7zz.js} +2 -2
- package/public/assets/{branch-state-badge-DnEEPtOP.js.map → branch-state-badge-D3W3w7zz.js.map} +1 -1
- package/public/assets/{channels-Dc3L2MQs.js → channels-BytdR9NS.js} +2 -2
- package/public/assets/{channels-Dc3L2MQs.js.map → channels-BytdR9NS.js.map} +1 -1
- package/public/assets/{chat-Fc6iJ7sa.js → chat-CvaQ2czL.js} +2 -2
- package/public/assets/{chat-Fc6iJ7sa.js.map → chat-CvaQ2czL.js.map} +1 -1
- package/public/assets/{connectors-Dc2ttGWs.js → connectors-BLQDp8Bc.js} +2 -2
- package/public/assets/{connectors-Dc2ttGWs.js.map → connectors-BLQDp8Bc.js.map} +1 -1
- package/public/assets/{display-DzNvOu1j.js → display-B06I5sqa.js} +3 -3
- package/public/assets/display-B06I5sqa.js.map +1 -0
- package/public/assets/{formatted-body-impl-Bg-gx7rG.js → formatted-body-impl-0Lf8e1Ic.js} +2 -2
- package/public/assets/{formatted-body-impl-Bg-gx7rG.js.map → formatted-body-impl-0Lf8e1Ic.js.map} +1 -1
- package/public/assets/{index-BeDncjN4.js → index-C3su8ezq.js} +6 -6
- package/public/assets/{index-BeDncjN4.js.map → index-C3su8ezq.js.map} +1 -1
- package/public/assets/{insights-c08aW-7n.js → insights-64uExH1E.js} +2 -2
- package/public/assets/{insights-c08aW-7n.js.map → insights-64uExH1E.js.map} +1 -1
- package/public/assets/{integrations-BwpgcqnF.js → integrations-HyC1EaIp.js} +2 -2
- package/public/assets/{integrations-BwpgcqnF.js.map → integrations-HyC1EaIp.js.map} +1 -1
- package/public/assets/{maintenance-CTKP0kWB.js → maintenance-EVBpKH07.js} +2 -2
- package/public/assets/{maintenance-CTKP0kWB.js.map → maintenance-EVBpKH07.js.map} +1 -1
- package/public/assets/{managed-agents-BofCxo8d.js → managed-agents-BLZPxGuw.js} +2 -2
- package/public/assets/{managed-agents-BofCxo8d.js.map → managed-agents-BLZPxGuw.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CQ-6BIb8.js → markdown-preview-impl-Bg6rCWYN.js} +2 -2
- package/public/assets/{markdown-preview-impl-CQ-6BIb8.js.map → markdown-preview-impl-Bg6rCWYN.js.map} +1 -1
- package/public/assets/{memory-DvfXjMoi.js → memory-Bjr8bgjj.js} +2 -2
- package/public/assets/{memory-DvfXjMoi.js.map → memory-Bjr8bgjj.js.map} +1 -1
- package/public/assets/{messages-C9VsPzZ_.js → messages-CRYw1F4z.js} +2 -2
- package/public/assets/{messages-C9VsPzZ_.js.map → messages-CRYw1F4z.js.map} +1 -1
- package/public/assets/{orchestrators-MffVueeE.js → orchestrators-D-co1vdp.js} +2 -2
- package/public/assets/{orchestrators-MffVueeE.js.map → orchestrators-D-co1vdp.js.map} +1 -1
- package/public/assets/{overview-BSVajefM.js → overview-Ddy5czri.js} +2 -2
- package/public/assets/{overview-BSVajefM.js.map → overview-Ddy5czri.js.map} +1 -1
- package/public/assets/{pairs-B5711-Cs.js → pairs-CFVM8uHL.js} +2 -2
- package/public/assets/{pairs-B5711-Cs.js.map → pairs-CFVM8uHL.js.map} +1 -1
- package/public/assets/{security-DkUwK9Vj.js → security-Df4_76bh.js} +2 -2
- package/public/assets/{security-DkUwK9Vj.js.map → security-Df4_76bh.js.map} +1 -1
- package/public/assets/{select-DrM2RRiT.js → select-Cq3BBnL9.js} +2 -2
- package/public/assets/{select-DrM2RRiT.js.map → select-Cq3BBnL9.js.map} +1 -1
- package/public/assets/{settings-DdKCb-vy.js → settings-DOLEYXZL.js} +2 -2
- package/public/assets/{settings-DdKCb-vy.js.map → settings-DOLEYXZL.js.map} +1 -1
- package/public/assets/{store-iTiheuTD.js → store-C1E8jmNE.js} +4 -4
- package/public/assets/store-C1E8jmNE.js.map +1 -0
- package/public/assets/{tasks-e1y1PFfO.js → tasks-BcsmPi9_.js} +2 -2
- package/public/assets/{tasks-e1y1PFfO.js.map → tasks-BcsmPi9_.js.map} +1 -1
- package/public/assets/{teams-BUcxoGC_.js → teams-BQrV3eKI.js} +2 -2
- package/public/assets/{teams-BUcxoGC_.js.map → teams-BQrV3eKI.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CmZjQOB2.js → terminal-viewer-impl-Dle04qpH.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CmZjQOB2.js.map → terminal-viewer-impl-Dle04qpH.js.map} +1 -1
- package/public/assets/{work-queue-CXBQx43n.js → work-queue-BLhTLg4o.js} +2 -2
- package/public/assets/{work-queue-CXBQx43n.js.map → work-queue-BLhTLg4o.js.map} +1 -1
- package/public/assets/{workspaces-D2EfZh9a.js → workspaces-4NCNTmRQ.js} +3 -3
- package/public/assets/{workspaces-D2EfZh9a.js.map → workspaces-4NCNTmRQ.js.map} +1 -1
- package/public/index.html +3 -3
- package/src/agent-ref.ts +8 -6
- package/src/automations/constants.ts +12 -0
- package/src/automations/cron.ts +129 -0
- package/src/automations/crud.ts +84 -0
- package/src/automations/dispatch.ts +275 -0
- package/src/automations/index.ts +5 -0
- package/src/automations/reconcile.ts +245 -0
- package/src/automations/storage.ts +181 -0
- package/src/automations/types.ts +16 -0
- package/src/automations/validation.ts +140 -0
- package/src/automations.ts +1 -1045
- package/src/db/activity.ts +1 -1
- package/src/db/agent-predicates.ts +9 -0
- package/src/db/agent-search.ts +2 -2
- package/src/db/agents.ts +13 -41
- package/src/db/artifacts.ts +8 -8
- package/src/db/channel-identity.ts +38 -0
- package/src/db/channels.ts +37 -61
- package/src/db/continuation-archives.ts +4 -4
- package/src/db/delivery.ts +7 -12
- package/src/db/inbox.ts +1 -1
- package/src/db/integrations.ts +4 -4
- package/src/db/issue-lifecycle.ts +6 -6
- package/src/db/mappers.ts +1 -2
- package/src/db/merge-lease.ts +3 -3
- package/src/db/message-claims.ts +16 -0
- package/src/db/message-queries.ts +8 -0
- package/src/db/message-reads.ts +26 -9
- package/src/db/messages.ts +17 -41
- package/src/db/notification-subscriptions.ts +2 -2
- package/src/db/orchestrators.ts +4 -4
- package/src/db/pair-maintenance.ts +9 -0
- package/src/db/pairs.ts +10 -18
- package/src/db/plans.ts +1 -1
- package/src/db/policy-runtime.ts +14 -0
- package/src/db/project-entries.ts +1 -1
- package/src/db/projects.ts +2 -2
- package/src/db/shared.ts +7 -0
- package/src/db/stats.ts +2 -2
- package/src/db/task-core.ts +18 -0
- package/src/db/task-maintenance.ts +24 -0
- package/src/db/tasks.ts +5 -17
- package/src/db/teams.ts +3 -3
- package/src/db/workspaces.ts +6 -6
- package/src/issue-tracker/github.ts +3 -3
- package/src/maintenance/constants.ts +35 -0
- package/src/maintenance/events.ts +32 -0
- package/src/maintenance/index.ts +7 -0
- package/src/maintenance/jobs.ts +340 -0
- package/src/maintenance/orphaned-sessions.ts +86 -0
- package/src/maintenance/scheduler.ts +231 -0
- package/src/maintenance/teams.ts +65 -0
- package/src/maintenance/types.ts +31 -0
- package/src/maintenance/workspaces.ts +251 -0
- package/src/maintenance.ts +1 -1099
- package/src/mcp/identity.ts +131 -0
- package/src/mcp/index.ts +208 -0
- package/src/mcp/tools-messaging.ts +459 -0
- package/src/mcp/tools-spawn.ts +145 -0
- package/src/mcp/tools-workspace.ts +227 -0
- package/src/mcp/types.ts +24 -0
- package/src/mcp/validation.ts +104 -0
- package/src/mcp.ts +1 -1319
- package/src/notification-router.ts +2 -2
- package/src/notification-rules.ts +2 -2
- package/src/project-ingest.ts +3 -3
- package/src/project-instructions.ts +1 -1
- package/src/provider-config-store.ts +3 -3
- package/src/provider-state.ts +1 -1
- package/src/reviewer-pipeline.ts +1 -1
- package/src/self-resume.ts +3 -7
- package/src/services/agent-action.ts +2 -2
- package/src/services/issue-activity.ts +0 -4
- package/src/services/issue-associations.ts +2 -2
- package/src/services/issue-lifecycle.ts +7 -7
- package/src/services/managed-running.ts +1 -1
- package/src/services/parity-harness.ts +3 -3
- package/src/services/plan-graph.ts +3 -3
- package/src/services/send-message.ts +16 -12
- package/src/services/shutdown-agent.ts +2 -2
- package/src/services/team-templates.ts +2 -2
- package/src/services/team.ts +2 -2
- package/src/settings/modules.ts +1 -1
- package/src/settings/registry.ts +3 -3
- package/src/spawn-command.ts +1 -1
- package/src/sse.ts +0 -4
- package/src/workspace-actions.ts +1 -1
- package/src/workspace-probe.ts +3 -7
- package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
- package/public/assets/display-DzNvOu1j.js.map +0 -1
- package/public/assets/store-iTiheuTD.js.map +0 -1
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { getArtifactStorage, maxArtifactBytes, normalizeDigest } from "../artifact-storage";
|
|
3
|
+
import { bytesToStream } from "../http-body";
|
|
4
|
+
import { MAX_BODY_BYTES } from "../config";
|
|
5
|
+
import { getManagedAgentState, getSpawnPolicy, listSpawnPolicies } from "../config-store";
|
|
6
|
+
import {
|
|
7
|
+
createArtifact,
|
|
8
|
+
getAgent,
|
|
9
|
+
getArtifact,
|
|
10
|
+
getMessage,
|
|
11
|
+
getOrchestrator,
|
|
12
|
+
getTask,
|
|
13
|
+
getThread,
|
|
14
|
+
linkArtifact,
|
|
15
|
+
listArtifactsForEntity,
|
|
16
|
+
listOrchestrators,
|
|
17
|
+
searchAgents,
|
|
18
|
+
upsertArtifactBlob,
|
|
19
|
+
ValidationError,
|
|
20
|
+
type AgentSearchFilter,
|
|
21
|
+
type AgentSearchSort,
|
|
22
|
+
} from "../db";
|
|
23
|
+
import { type DeliveryReceipt } from "../agent-ref";
|
|
24
|
+
import { emitRelayEvent } from "../events";
|
|
25
|
+
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
26
|
+
import { authContextFromMcp } from "../services/auth-context";
|
|
27
|
+
import { ServiceAuthError } from "../services/errors";
|
|
28
|
+
import { sendMessageService } from "../services/send-message";
|
|
29
|
+
import { optionalEnum } from "../validation";
|
|
30
|
+
import { isRecord } from "agent-relay-sdk";
|
|
31
|
+
import type { ArtifactKind, ArtifactSensitivity, AttachmentRef, Message, SendMessageInput } from "../types";
|
|
32
|
+
import type { McpAuthContext, ToolDefinition } from "./types";
|
|
33
|
+
import { callerAgentId } from "./identity";
|
|
34
|
+
import { enumField, optionalAttachments, optionalBoolean, optionalFutureTimestamp, optionalPositiveInt, optionalRecord, optionalString, positiveId, recordField, stringField } from "./validation";
|
|
35
|
+
|
|
36
|
+
const VALID_ARTIFACT_KINDS = ["image", "audio", "video", "document", "archive", "other"] as const;
|
|
37
|
+
const VALID_ARTIFACT_SENSITIVITIES = ["public", "normal", "sensitive", "secret"] as const;
|
|
38
|
+
const VALID_ARTIFACT_ROLES = ["media", "patch", "report", "log", "output", "input"] as const;
|
|
39
|
+
const VALID_ARTIFACT_ENTITY_TYPES = ["message", "task", "recipeRun", "recipeStep", "channelEvent"] as const;
|
|
40
|
+
|
|
41
|
+
export const MESSAGING_TOOLS: ToolDefinition[] = [
|
|
42
|
+
{
|
|
43
|
+
name: "relay_send_message",
|
|
44
|
+
description: "Send an Agent Relay message. Returns a delivery receipt (delivered/expectReply/recipients): if expectReply is false, no live recipient exists — don't wait for a reply. An ambiguous target is rejected up front. An unknown target is stored for delivery once that id registers (the receipt reports delivered:false with empty recipients and a reason) — never silently dropped.",
|
|
45
|
+
requiredScopes: ["messages:write", "message:send"],
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
from: { type: "string", description: "Deprecated/optional. Your identity is taken from your auth token; ignored for managed agents. You never need to set this." },
|
|
50
|
+
to: { type: "string", description: "Target: an agent id, label, name, or a unique id segment (e.g. the number part) — resolved automatically. Or a fan-out selector: tag:, cap:, label:, team:, policy:, or broadcast." },
|
|
51
|
+
body: { type: "string" },
|
|
52
|
+
subject: { type: "string" },
|
|
53
|
+
channel: { type: "string" },
|
|
54
|
+
claimable: { type: "boolean" },
|
|
55
|
+
idempotencyKey: { type: "string" },
|
|
56
|
+
attachments: { type: "array", items: { type: "object" } },
|
|
57
|
+
payload: { type: "object" },
|
|
58
|
+
meta: { type: "object" },
|
|
59
|
+
},
|
|
60
|
+
required: ["to", "body"],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "relay_reply",
|
|
66
|
+
description: "Reply to an Agent Relay message by id, preserving reply/thread/channel context. Returns a delivery receipt; expectReply:false means the original sender is no longer reachable.",
|
|
67
|
+
requiredScopes: ["messages:write", "message:send"],
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
from: { type: "string", description: "Deprecated/optional. Your identity is taken from your auth token; ignored for managed agents. You never need to set this." },
|
|
72
|
+
messageId: { type: "integer", minimum: 1 },
|
|
73
|
+
body: { type: "string" },
|
|
74
|
+
subject: { type: "string" },
|
|
75
|
+
format: { type: "string", enum: ["text", "markdown", "markdownv2"] },
|
|
76
|
+
attachments: { type: "array", items: { type: "object" } },
|
|
77
|
+
payload: { type: "object" },
|
|
78
|
+
meta: { type: "object" },
|
|
79
|
+
},
|
|
80
|
+
required: ["messageId", "body"],
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "relay_get_message",
|
|
86
|
+
description: "Fetch a full Agent Relay message body with optional attachments and thread context.",
|
|
87
|
+
requiredScopes: ["messages:read", "message:read"],
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
messageId: { type: "integer", minimum: 1 },
|
|
92
|
+
includeArtifacts: { type: "boolean", default: true },
|
|
93
|
+
includeThread: { type: "boolean", default: true },
|
|
94
|
+
},
|
|
95
|
+
required: ["messageId"],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "relay_get_thread",
|
|
101
|
+
description: "Fetch the full Agent Relay thread containing a message.",
|
|
102
|
+
requiredScopes: ["messages:read", "message:read"],
|
|
103
|
+
inputSchema: {
|
|
104
|
+
type: "object",
|
|
105
|
+
properties: {
|
|
106
|
+
messageId: { type: "integer", minimum: 1 },
|
|
107
|
+
},
|
|
108
|
+
required: ["messageId"],
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "relay_upload_artifact",
|
|
114
|
+
description: "Upload long text or base64 content as an Agent Relay artifact.",
|
|
115
|
+
requiredScopes: ["artifact:write"],
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
content: { type: "string", description: "UTF-8 text content to store." },
|
|
120
|
+
base64: { type: "string", description: "Base64-encoded binary content to store." },
|
|
121
|
+
filename: { type: "string" },
|
|
122
|
+
mediaType: { type: "string" },
|
|
123
|
+
kind: { type: "string", enum: VALID_ARTIFACT_KINDS },
|
|
124
|
+
sensitivity: { type: "string", enum: VALID_ARTIFACT_SENSITIVITIES },
|
|
125
|
+
role: { type: "string", enum: VALID_ARTIFACT_ROLES },
|
|
126
|
+
title: { type: "string" },
|
|
127
|
+
metadata: { type: "object" },
|
|
128
|
+
expiresAt: { type: "integer", minimum: 1 },
|
|
129
|
+
digest: { type: "string", description: "Optional sha256 digest for integrity verification." },
|
|
130
|
+
},
|
|
131
|
+
additionalProperties: false,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "relay_attach_artifact",
|
|
136
|
+
description: "Attach an existing Relay artifact to a message, task, recipe run, recipe step, or channel event.",
|
|
137
|
+
requiredScopes: ["artifact:write"],
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: {
|
|
141
|
+
artifactId: { type: "string" },
|
|
142
|
+
entityType: { type: "string", enum: VALID_ARTIFACT_ENTITY_TYPES },
|
|
143
|
+
entityId: { oneOf: [{ type: "string" }, { type: "integer", minimum: 1 }] },
|
|
144
|
+
role: { type: "string", enum: VALID_ARTIFACT_ROLES },
|
|
145
|
+
title: { type: "string" },
|
|
146
|
+
},
|
|
147
|
+
required: ["artifactId", "entityType", "entityId"],
|
|
148
|
+
additionalProperties: false,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "relay_agent_status",
|
|
153
|
+
description: "Inspect Relay agent, orchestrator, and managed spawn-policy status. The default agent list shows only RUNNING agents (the ones you can actually message/reply/pair with); pass includeOffline:true for the full roster.",
|
|
154
|
+
requiredScopes: ["agents:read", "agent:read"],
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: "object",
|
|
157
|
+
properties: {
|
|
158
|
+
agentId: { type: "string" },
|
|
159
|
+
policyName: { type: "string" },
|
|
160
|
+
orchestratorId: { type: "string" },
|
|
161
|
+
includeOffline: { type: "boolean", description: "Include offline/stale agents in the default list (default false)." },
|
|
162
|
+
},
|
|
163
|
+
additionalProperties: false,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "relay_find_agents",
|
|
168
|
+
description: "Search the fleet for agents by label, capability, provider, tag, machine, status, or kind, sorted by last-active/created/label. Defaults to running agents only. Use this to find a reachable, capable peer before messaging or pairing — instead of dumping the whole roster.",
|
|
169
|
+
requiredScopes: ["agents:read", "agent:read"],
|
|
170
|
+
inputSchema: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
label: { type: "string", description: "Substring match on label or name." },
|
|
174
|
+
capability: { type: ["string", "array"], items: { type: "string" }, description: "One or more capabilities (OR within)." },
|
|
175
|
+
provider: { type: ["string", "array"], items: { type: "string" }, description: "Model provider, e.g. claude or codex." },
|
|
176
|
+
tag: { type: ["string", "array"], items: { type: "string" } },
|
|
177
|
+
machine: { type: ["string", "array"], items: { type: "string" } },
|
|
178
|
+
kind: { type: ["string", "array"], items: { type: "string" } },
|
|
179
|
+
status: { type: ["string", "array"], items: { type: "string" }, description: "online/idle/busy/offline/stale, the 'running' shorthand, or 'all'. Default 'running'." },
|
|
180
|
+
spawnedBy: { type: "string", description: "Parent agent id, or 'me' for your own spawned children (#221)." },
|
|
181
|
+
teamId: { type: "string", description: "Durable team id for roster filtering." },
|
|
182
|
+
sortBy: { type: "string", enum: ["lastActive", "created", "label"] },
|
|
183
|
+
order: { type: "string", enum: ["asc", "desc"] },
|
|
184
|
+
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
185
|
+
},
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
function runMcpSend(auth: McpAuthContext, input: SendMessageInput): Message & { delivery: DeliveryReceipt } {
|
|
192
|
+
try {
|
|
193
|
+
const result = sendMessageService(
|
|
194
|
+
input,
|
|
195
|
+
authContextFromMcp({ kind: auth.kind, actor: auth.actor, scopes: auth.scopes, component: auth.component }),
|
|
196
|
+
{ integration: auth.integration },
|
|
197
|
+
);
|
|
198
|
+
return { ...result.message, delivery: result.receipt };
|
|
199
|
+
} catch (e) {
|
|
200
|
+
if (e instanceof ServiceAuthError) throw new McpAuthError(e.message);
|
|
201
|
+
throw e;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function relaySendMessage(auth: McpAuthContext, args: Record<string, unknown>): Message & { delivery: DeliveryReceipt } {
|
|
206
|
+
const attachments = optionalAttachments(args.attachments);
|
|
207
|
+
const payload = payloadWithAttachments(optionalRecord(args.payload, "payload"), attachments);
|
|
208
|
+
const input: SendMessageInput = {
|
|
209
|
+
from: optionalString(args.from, "from", 200) ?? "",
|
|
210
|
+
to: stringField(args.to, "to", { required: true, max: 200 }),
|
|
211
|
+
body: stringField(args.body, "body", { required: true, maxBytes: MAX_BODY_BYTES }),
|
|
212
|
+
subject: optionalString(args.subject, "subject", 200),
|
|
213
|
+
channel: optionalString(args.channel, "channel", 120),
|
|
214
|
+
claimable: optionalBoolean(args.claimable, "claimable"),
|
|
215
|
+
idempotencyKey: optionalString(args.idempotencyKey, "idempotencyKey", 240),
|
|
216
|
+
attachments,
|
|
217
|
+
payload,
|
|
218
|
+
meta: optionalRecord(args.meta, "meta"),
|
|
219
|
+
};
|
|
220
|
+
return runMcpSend(auth, input);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function relayReply(auth: McpAuthContext, args: Record<string, unknown>): Message & { delivery: DeliveryReceipt } {
|
|
224
|
+
const messageId = positiveId(args.messageId, "messageId");
|
|
225
|
+
const parent = getMessage(messageId);
|
|
226
|
+
if (!parent) throw new McpNotFoundError(`message ${messageId} not found`);
|
|
227
|
+
const attachments = optionalAttachments(args.attachments);
|
|
228
|
+
const payload = optionalRecord(args.payload, "payload") ?? {};
|
|
229
|
+
const format = optionalEnum(args.format, "format", ["text", "markdown", "markdownv2"] as const);
|
|
230
|
+
const replyPayload = payloadWithAttachments({
|
|
231
|
+
...payload,
|
|
232
|
+
...(format ? { message: { ...(isRecord(payload.message) ? payload.message : {}), format } } : {}),
|
|
233
|
+
}, attachments);
|
|
234
|
+
return runMcpSend(auth, {
|
|
235
|
+
from: optionalString(args.from, "from", 200) ?? "",
|
|
236
|
+
to: "",
|
|
237
|
+
body: stringField(args.body, "body", { required: true, maxBytes: MAX_BODY_BYTES }),
|
|
238
|
+
subject: optionalString(args.subject, "subject", 200),
|
|
239
|
+
replyTo: parent.id,
|
|
240
|
+
attachments,
|
|
241
|
+
payload: replyPayload,
|
|
242
|
+
meta: optionalRecord(args.meta, "meta"),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function relayGetMessage(args: Record<string, unknown>): Record<string, unknown> {
|
|
247
|
+
const messageId = positiveId(args.messageId, "messageId");
|
|
248
|
+
const message = getMessage(messageId);
|
|
249
|
+
if (!message) throw new McpNotFoundError(`message ${messageId} not found`);
|
|
250
|
+
const includeArtifacts = args.includeArtifacts !== false;
|
|
251
|
+
const includeThread = args.includeThread !== false;
|
|
252
|
+
return {
|
|
253
|
+
message,
|
|
254
|
+
...(includeArtifacts ? { artifacts: listArtifactsForEntity("message", message.id) } : {}),
|
|
255
|
+
...(includeThread ? { thread: getThread(message.id) } : {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function relayGetThread(args: Record<string, unknown>): Record<string, unknown> {
|
|
260
|
+
const messageId = positiveId(args.messageId, "messageId");
|
|
261
|
+
const thread = getThread(messageId);
|
|
262
|
+
if (!thread.length) throw new McpNotFoundError(`message ${messageId} not found`);
|
|
263
|
+
return { thread };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function relayUploadArtifact(auth: McpAuthContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
267
|
+
const bytes = artifactBytes(args);
|
|
268
|
+
const maxBytes = maxArtifactBytes();
|
|
269
|
+
if (bytes.byteLength > maxBytes) throw new ValidationError(`artifact exceeds ${maxBytes} bytes`);
|
|
270
|
+
const filename = optionalString(args.filename, "filename", 240);
|
|
271
|
+
const hintedMediaType = optionalString(args.mediaType, "mediaType", 160);
|
|
272
|
+
const mediaType = sniffMediaType(bytes, hintedMediaType, filename);
|
|
273
|
+
const storage = getArtifactStorage();
|
|
274
|
+
const digest = optionalString(args.digest, "digest", 80);
|
|
275
|
+
const stored = await storage.store(bytesToStream(bytes), {
|
|
276
|
+
mediaType,
|
|
277
|
+
size: bytes.byteLength,
|
|
278
|
+
digest: digest ? normalizeDigest(digest) : undefined,
|
|
279
|
+
});
|
|
280
|
+
upsertArtifactBlob({
|
|
281
|
+
digest: stored.digest,
|
|
282
|
+
storageUri: stored.storageUri,
|
|
283
|
+
mediaType,
|
|
284
|
+
size: stored.size,
|
|
285
|
+
});
|
|
286
|
+
const artifact = createArtifact({
|
|
287
|
+
blobDigest: stored.digest,
|
|
288
|
+
mediaType,
|
|
289
|
+
kind: (optionalEnum(args.kind, "kind", VALID_ARTIFACT_KINDS) as ArtifactKind | undefined) ?? inferArtifactKind(mediaType, filename),
|
|
290
|
+
filename,
|
|
291
|
+
size: stored.size,
|
|
292
|
+
sensitivity: optionalEnum(args.sensitivity, "sensitivity", VALID_ARTIFACT_SENSITIVITIES) as ArtifactSensitivity | undefined,
|
|
293
|
+
createdBy: auth.actor,
|
|
294
|
+
expiresAt: optionalFutureTimestamp(args.expiresAt, "expiresAt"),
|
|
295
|
+
metadata: optionalRecord(args.metadata, "metadata"),
|
|
296
|
+
});
|
|
297
|
+
emitRelayEvent({ type: "artifact.created", source: "mcp", subject: artifact.id, data: { artifact, actor: auth.actor } });
|
|
298
|
+
return {
|
|
299
|
+
artifact,
|
|
300
|
+
attachment: {
|
|
301
|
+
artifactId: artifact.id,
|
|
302
|
+
kind: artifact.kind,
|
|
303
|
+
role: optionalEnum(args.role, "role", VALID_ARTIFACT_ROLES),
|
|
304
|
+
title: optionalString(args.title, "title", 240) ?? artifact.filename,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function relayAttachArtifact(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
310
|
+
const artifactId = stringField(args.artifactId, "artifactId", { required: true, max: 120 });
|
|
311
|
+
const artifact = getArtifact(artifactId);
|
|
312
|
+
if (!artifact) throw new McpNotFoundError(`artifact ${artifactId} not found`);
|
|
313
|
+
const entityType = enumField(args.entityType, "entityType", VALID_ARTIFACT_ENTITY_TYPES);
|
|
314
|
+
const rawEntityId = args.entityId;
|
|
315
|
+
if (typeof rawEntityId !== "string" && typeof rawEntityId !== "number") throw new ValidationError("entityId must be a string or integer");
|
|
316
|
+
const entityId = typeof rawEntityId === "number" ? positiveId(rawEntityId, "entityId") : stringField(rawEntityId, "entityId", { required: true, max: 200 });
|
|
317
|
+
assertArtifactEntityExists(entityType, entityId);
|
|
318
|
+
const link = linkArtifact({
|
|
319
|
+
artifactId,
|
|
320
|
+
entityType,
|
|
321
|
+
entityId,
|
|
322
|
+
role: optionalEnum(args.role, "role", VALID_ARTIFACT_ROLES),
|
|
323
|
+
title: optionalString(args.title, "title", 240),
|
|
324
|
+
createdBy: auth.actor,
|
|
325
|
+
});
|
|
326
|
+
return { artifact: getArtifact(artifactId) ?? artifact, link };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function relayAgentStatus(args: Record<string, unknown>): Record<string, unknown> {
|
|
330
|
+
const agentId = optionalString(args.agentId, "agentId", 240);
|
|
331
|
+
if (agentId) {
|
|
332
|
+
const agent = getAgent(agentId);
|
|
333
|
+
if (!agent) throw new McpNotFoundError(`agent ${agentId} not found`);
|
|
334
|
+
return { agent };
|
|
335
|
+
}
|
|
336
|
+
const policyName = optionalString(args.policyName, "policyName", 120);
|
|
337
|
+
if (policyName) return policyStatus(policyName);
|
|
338
|
+
const orchestratorId = optionalString(args.orchestratorId, "orchestratorId", 200);
|
|
339
|
+
if (orchestratorId) {
|
|
340
|
+
const orchestrator = getOrchestrator(orchestratorId);
|
|
341
|
+
if (!orchestrator) throw new McpNotFoundError(`orchestrator ${orchestratorId} not found`);
|
|
342
|
+
return { orchestrator };
|
|
343
|
+
}
|
|
344
|
+
const includeOffline = optionalBoolean(args.includeOffline, "includeOffline") === true;
|
|
345
|
+
return {
|
|
346
|
+
agents: searchAgents({ status: includeOffline ? "all" : "running" }),
|
|
347
|
+
spawnPolicies: listSpawnPolicies().map((entry) => policyStatusPayload(entry.value)),
|
|
348
|
+
orchestrators: listOrchestrators(),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function optionalStringOrArray(value: unknown, field: string): string | string[] | undefined {
|
|
353
|
+
if (value === undefined || value === null) return undefined;
|
|
354
|
+
if (typeof value === "string") return value;
|
|
355
|
+
if (Array.isArray(value)) {
|
|
356
|
+
return value.map((item, i) => stringField(item, `${field}[${i}]`, { required: true, max: 200 }));
|
|
357
|
+
}
|
|
358
|
+
throw new ValidationError(`${field} must be a string or string array`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function relayFindAgents(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
362
|
+
const spawnedByArg = optionalString(args.spawnedBy, "spawnedBy", 240);
|
|
363
|
+
const spawnedBy = spawnedByArg === "me" ? callerAgentId(auth) ?? "\0unresolved" : spawnedByArg;
|
|
364
|
+
const filter: AgentSearchFilter = {
|
|
365
|
+
label: optionalString(args.label, "label", 200),
|
|
366
|
+
capability: optionalStringOrArray(args.capability, "capability"),
|
|
367
|
+
provider: optionalStringOrArray(args.provider, "provider"),
|
|
368
|
+
tag: optionalStringOrArray(args.tag, "tag"),
|
|
369
|
+
machine: optionalStringOrArray(args.machine, "machine"),
|
|
370
|
+
kind: optionalStringOrArray(args.kind, "kind"),
|
|
371
|
+
spawnedBy,
|
|
372
|
+
teamId: optionalString(args.teamId, "teamId", 120),
|
|
373
|
+
status: optionalStringOrArray(args.status, "status") ?? "running",
|
|
374
|
+
};
|
|
375
|
+
const sort: AgentSearchSort = {
|
|
376
|
+
sortBy: optionalEnum(args.sortBy, "sortBy", ["lastActive", "created", "label"] as const),
|
|
377
|
+
order: optionalEnum(args.order, "order", ["asc", "desc"] as const),
|
|
378
|
+
limit: optionalPositiveInt(args.limit, "limit"),
|
|
379
|
+
};
|
|
380
|
+
const agents = searchAgents(filter, sort);
|
|
381
|
+
return { agents, count: agents.length };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function payloadWithAttachments(
|
|
385
|
+
payload: Record<string, unknown> | undefined,
|
|
386
|
+
attachments: AttachmentRef[] | undefined,
|
|
387
|
+
): Record<string, unknown> | undefined {
|
|
388
|
+
if (!attachments) return payload;
|
|
389
|
+
if (payload?.attachments !== undefined) throw new ValidationError("attachments must be provided either top-level or in payload.attachments, not both");
|
|
390
|
+
return { ...(payload ?? {}), attachments };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function policyStatus(policyName: string): Record<string, unknown> {
|
|
394
|
+
const entry = getSpawnPolicy(policyName);
|
|
395
|
+
if (!entry) throw new McpNotFoundError(`spawn policy ${policyName} not found`);
|
|
396
|
+
return policyStatusPayload(entry.value);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function policyStatusPayload(policy: NonNullable<ReturnType<typeof getSpawnPolicy>>["value"]): Record<string, unknown> {
|
|
400
|
+
return {
|
|
401
|
+
policy,
|
|
402
|
+
state: getManagedAgentState(policy.name) ?? {
|
|
403
|
+
policyName: policy.name,
|
|
404
|
+
status: "stopped",
|
|
405
|
+
orchestratorId: policy.orchestratorId,
|
|
406
|
+
provider: policy.provider,
|
|
407
|
+
restartCount: 0,
|
|
408
|
+
consecutiveFailures: 0,
|
|
409
|
+
updatedAt: 0,
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function artifactBytes(args: Record<string, unknown>): Uint8Array {
|
|
415
|
+
const hasContent = args.content !== undefined && args.content !== null;
|
|
416
|
+
const hasBase64 = args.base64 !== undefined && args.base64 !== null;
|
|
417
|
+
if (hasContent === hasBase64) throw new ValidationError("provide exactly one of content or base64");
|
|
418
|
+
if (hasContent) {
|
|
419
|
+
if (typeof args.content !== "string" || args.content.length === 0) throw new ValidationError("content required");
|
|
420
|
+
return new TextEncoder().encode(args.content);
|
|
421
|
+
}
|
|
422
|
+
const encoded = stringField(args.base64, "base64", { required: true }).replace(/\s+/g, "");
|
|
423
|
+
if (encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) throw new ValidationError("base64 must be valid base64 content");
|
|
424
|
+
return new Uint8Array(Buffer.from(encoded, "base64"));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function assertArtifactEntityExists(entityType: string, entityId: string | number): void {
|
|
428
|
+
if (entityType === "message" && !getMessage(Number(entityId))) throw new McpNotFoundError(`message ${entityId} not found`);
|
|
429
|
+
if (entityType === "task" && !getTask(Number(entityId))) throw new McpNotFoundError(`task ${entityId} not found`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function inferArtifactKind(mediaType: string, filename?: string): ArtifactKind {
|
|
433
|
+
const lowerName = filename?.toLowerCase() ?? "";
|
|
434
|
+
if (mediaType.startsWith("image/")) return "image";
|
|
435
|
+
if (mediaType.startsWith("audio/")) return "audio";
|
|
436
|
+
if (mediaType.startsWith("video/")) return "video";
|
|
437
|
+
if (["application/zip", "application/gzip", "application/x-tar"].includes(mediaType) || /\.(zip|tgz|tar|gz)$/.test(lowerName)) return "archive";
|
|
438
|
+
if (
|
|
439
|
+
mediaType.startsWith("text/") ||
|
|
440
|
+
["application/pdf", "application/json", "application/xml"].includes(mediaType) ||
|
|
441
|
+
/\.(pdf|txt|md|json|csv|diff|patch)$/.test(lowerName)
|
|
442
|
+
) return "document";
|
|
443
|
+
return "other";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function sniffMediaType(bytes: Uint8Array, hinted?: string, filename?: string): string {
|
|
447
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "image/jpeg";
|
|
448
|
+
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) return "image/png";
|
|
449
|
+
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) return "image/gif";
|
|
450
|
+
if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) return "application/pdf";
|
|
451
|
+
if (bytes[0] === 0x50 && bytes[1] === 0x4b) return "application/zip";
|
|
452
|
+
const lower = filename?.toLowerCase() ?? "";
|
|
453
|
+
if (lower.endsWith(".svg")) return "image/svg+xml";
|
|
454
|
+
if (lower.endsWith(".md")) return "text/markdown";
|
|
455
|
+
if (lower.endsWith(".json")) return "application/json";
|
|
456
|
+
if (lower.endsWith(".csv")) return "text/csv";
|
|
457
|
+
if (lower.endsWith(".diff") || lower.endsWith(".patch")) return "text/x-diff";
|
|
458
|
+
return hinted || "text/plain";
|
|
459
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { spawnAgent, type SpawnAgentInput } from "../services/spawn-agent";
|
|
2
|
+
import { authContextFromMcp } from "../services/auth-context";
|
|
3
|
+
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent } from "../services/shutdown-agent";
|
|
4
|
+
import { buildSpawnTargets } from "../spawn-targets";
|
|
5
|
+
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
6
|
+
import { parseProjectInstructions, PROJECT_INSTRUCTION_POLICY_NAMES } from "../project-instructions";
|
|
7
|
+
import { ValidationError } from "../db";
|
|
8
|
+
import { optionalEnum } from "../validation";
|
|
9
|
+
import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_AGENT_LIFECYCLES, VALID_EFFORTS, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
10
|
+
import type { ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
11
|
+
import type { AgentLifecycle, SpawnApprovalMode, SpawnProvider, WorkspaceMode } from "../types";
|
|
12
|
+
import type { McpAuthContext, ToolDefinition } from "./types";
|
|
13
|
+
import { callerAgentId } from "./identity";
|
|
14
|
+
import { enumField, optionalNonNegativeInt, optionalPositiveInt, optionalString, optionalStringArray } from "./validation";
|
|
15
|
+
|
|
16
|
+
export const SPAWN_TOOLS: ToolDefinition[] = [
|
|
17
|
+
{
|
|
18
|
+
name: "relay_spawn_agent",
|
|
19
|
+
description: "Spawn a long-living provider agent through Relay's orchestrator, optionally handing it its first task via `prompt` in the same call. Defaults to your own host (override with orchestratorId) and returns the resolved agent id once it registers. Gated: requires the command:spawn scope, granted only to agents whose profile sets maxSpawnedAgents>0, up to that live-children quota. Spawned agents cannot themselves spawn (no grandchildren).",
|
|
20
|
+
requiredScopes: ["command:spawn"],
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
provider: { type: "string", enum: SPAWN_PROVIDERS },
|
|
25
|
+
orchestratorId: { type: "string", description: "Target host. Defaults to the host that owns cwd, else YOUR OWN host — only set it to spawn onto a different machine." },
|
|
26
|
+
cwd: { type: "string", description: "Working directory for the agent. Defaults to YOUR OWN cwd (the repo you're working in) when omitted, else the orchestrator's base dir. Must resolve within the target orchestrator's base directory (enforced server-side). NOTE: workspaceMode `isolated` requires a git repo — the resolved cwd must be inside one or the spawn is rejected." },
|
|
27
|
+
label: { type: "string" },
|
|
28
|
+
model: { type: "string" },
|
|
29
|
+
effort: { type: "string", enum: VALID_EFFORTS },
|
|
30
|
+
approvalMode: { type: "string", enum: APPROVAL_MODES, description: "Permission posture for the worker. Defaults to YOUR OWN approval mode when omitted (a headless `guarded` worker wedges on the first approval prompt), so a coordinator running `open` spawns workers that can act autonomously. Pass an explicit value to narrow a child (e.g. `read-only` reviewer)." },
|
|
31
|
+
prompt: { type: "string", description: "Initial task/message delivered to the agent on launch — spawn and hand it its first instruction in one call (no separate follow-up message needed)." },
|
|
32
|
+
expectReply: { type: "boolean", description: "Require the worker to report back: it gets a pending reply obligation to you and CANNOT go idle until it delivers its result (which routes to your inbox and wakes you). Defaults true when you pass a prompt — set false for a true fire-and-forget worker that needn't report (e.g. one that only lands a branch)." },
|
|
33
|
+
systemPromptAppend: { type: "string" },
|
|
34
|
+
projectInstructions: { type: "object", description: "#409 — project instruction projection (default OFF). When set, relay composes a knowledge preamble from the spawn cwd's resolved project (own + inherited entries) and prepends it to systemPromptAppend, instead of relying on host CLAUDE.md. `policy: \"all\"` includes ingested host content; `\"vanilla\"` excludes it (manual+promoted only). Omit to keep current behavior.", properties: { policy: { type: "string", enum: PROJECT_INSTRUCTION_POLICY_NAMES } }, additionalProperties: false },
|
|
35
|
+
profile: { type: "string", description: "Agent profile name to apply (env, instructions, permissions, MCP/skills, spawn quota)." },
|
|
36
|
+
tags: { type: "array", items: { type: "string" } },
|
|
37
|
+
capabilities: { type: "array", items: { type: "string" } },
|
|
38
|
+
providerArgs: { type: "array", items: { type: "string" } },
|
|
39
|
+
policyName: { type: "string" },
|
|
40
|
+
spawnRequestId: { type: "string" },
|
|
41
|
+
lifecycle: { type: "string", enum: VALID_AGENT_LIFECYCLES, description: "Lifecycle mode. `persistent` keeps the agent until explicitly stopped. `transient` lets Relay auto-shutdown the child after it is idle and, for isolated workers, after its branch has landed. Default: persistent." },
|
|
42
|
+
workspaceMode: { type: "string", enum: VALID_WORKSPACE_MODES, description: "Git workspace for the worker. `isolated` = its own worktree on an `agent/session-*` branch off main that auto-lands when ready (delegate a unit of work, get a branch worker). `shared` = edits your live checkout (a sidecar). `inherit` = let the orchestrator decide. DEFAULT for agent-initiated spawns is `isolated` so a delegated worker lands to main instead of editing your tree; pass `shared`/`inherit` to override." },
|
|
43
|
+
waitForRegistrationMs: { type: "integer", minimum: 0, maximum: 30000, description: "How long to wait for the spawned agent to register before returning, so the response carries its resolved agent id (default 8000; 0 = return immediately with just spawnRequestId)." },
|
|
44
|
+
},
|
|
45
|
+
required: ["provider"],
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "relay_spawn_targets",
|
|
51
|
+
description: "Return the LIVE spawn capability matrix before you spawn: which orchestrators/hosts are online, which providers actually run on each (orchestrator-probed, not a static guess), each provider's models with per-model effort levels + defaults, your own host (isSelf) and remaining spawn quota, and named profiles you can spawn by role. Call this first whenever you decide to spawn — it removes the guesswork that otherwise only surfaces as a runtime rejection. Requires the command:spawn scope.",
|
|
52
|
+
requiredScopes: ["command:spawn"],
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {},
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "relay_shutdown_agent",
|
|
61
|
+
description: "Shut down an agent through Relay's orchestrator. Gated: requires the command:shutdown scope; an agent caller may only target its OWN live spawned children, addressed by agentId or spawnRequestId (policyName/tmuxSession are admin-only broad targets). Admin tokens keep full reach.",
|
|
62
|
+
requiredScopes: ["command:shutdown"],
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
agentId: { type: "string", description: "Target child by agent id. For an agent caller, agentId or spawnRequestId identifies your spawned child." },
|
|
67
|
+
policyName: { type: "string", description: "Broad target by managed-agent policy — admin/full-reach callers only." },
|
|
68
|
+
spawnRequestId: { type: "string", description: "Target child by the idempotency key relay_spawn_agent returned — resolved to your spawned child server-side." },
|
|
69
|
+
tmuxSession: { type: "string", description: "Broad target by tmux session — admin/full-reach callers only." },
|
|
70
|
+
orchestratorId: { type: "string" },
|
|
71
|
+
reason: { type: "string" },
|
|
72
|
+
timeoutMs: { type: "integer", minimum: 1 },
|
|
73
|
+
},
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export async function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
80
|
+
const input: SpawnAgentInput = {
|
|
81
|
+
provider: enumField(args.provider, "provider", SPAWN_PROVIDERS) as SpawnProvider,
|
|
82
|
+
orchestratorId: optionalString(args.orchestratorId, "orchestratorId", 200),
|
|
83
|
+
cwd: optionalString(args.cwd, "cwd", 500),
|
|
84
|
+
model: optionalString(args.model, "model", 120),
|
|
85
|
+
effort: optionalString(args.effort, "effort", 120) as ProviderEffort | undefined,
|
|
86
|
+
approvalMode: optionalEnum(args.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined,
|
|
87
|
+
workspaceMode: optionalEnum(args.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
|
|
88
|
+
lifecycle: (optionalEnum(args.lifecycle, "lifecycle", VALID_AGENT_LIFECYCLES) as AgentLifecycle | undefined) ?? "persistent",
|
|
89
|
+
label: optionalString(args.label, "label", 120),
|
|
90
|
+
policyName: optionalString(args.policyName, "policyName", 120),
|
|
91
|
+
profile: optionalString(args.profile, "profile", 120),
|
|
92
|
+
prompt: optionalString(args.prompt, "prompt", 16_000),
|
|
93
|
+
expectReply: typeof args.expectReply === "boolean" ? args.expectReply : undefined,
|
|
94
|
+
systemPromptAppend: optionalString(args.systemPromptAppend, "systemPromptAppend", 64_000),
|
|
95
|
+
projectInstructions: parseProjectInstructions(args.projectInstructions, (m) => new ValidationError(m)),
|
|
96
|
+
tags: optionalStringArray(args.tags, "tags"),
|
|
97
|
+
capabilities: optionalStringArray(args.capabilities, "capabilities"),
|
|
98
|
+
providerArgs: optionalStringArray(args.providerArgs, "providerArgs"),
|
|
99
|
+
spawnRequestId: optionalString(args.spawnRequestId, "spawnRequestId", 160),
|
|
100
|
+
requestedVia: "mcp",
|
|
101
|
+
waitForRegistrationMs: optionalNonNegativeInt(args.waitForRegistrationMs, "waitForRegistrationMs") ?? 8000,
|
|
102
|
+
};
|
|
103
|
+
const result = await spawnAgent(input, authContextFromMcp(auth));
|
|
104
|
+
return { ...result };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function relaySpawnTargets(auth: McpAuthContext): Record<string, unknown> {
|
|
108
|
+
return buildSpawnTargets({
|
|
109
|
+
callerId: callerAgentId(auth),
|
|
110
|
+
quota: auth.component?.constraints?.maxSpawnedAgents ?? 0,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function relayShutdownAgent(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
115
|
+
const agentId = optionalString(args.agentId, "agentId", 240);
|
|
116
|
+
const policyName = optionalString(args.policyName, "policyName", 120);
|
|
117
|
+
const spawnRequestId = optionalString(args.spawnRequestId, "spawnRequestId", 160);
|
|
118
|
+
const tmuxSession = optionalString(args.tmuxSession, "tmuxSession", 200);
|
|
119
|
+
if (!agentId && !policyName && !spawnRequestId && !tmuxSession) {
|
|
120
|
+
throw new ValidationError("agentId, policyName, spawnRequestId, or tmuxSession required");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = shutdownAgent(
|
|
125
|
+
{
|
|
126
|
+
agentId,
|
|
127
|
+
policyName,
|
|
128
|
+
spawnRequestId,
|
|
129
|
+
tmuxSession,
|
|
130
|
+
orchestratorId: optionalString(args.orchestratorId, "orchestratorId", 200),
|
|
131
|
+
graceful: true,
|
|
132
|
+
timeoutMs: optionalPositiveInt(args.timeoutMs, "timeoutMs") ?? 10_000,
|
|
133
|
+
reason: optionalString(args.reason, "reason", 200) ?? "mcp-shutdown",
|
|
134
|
+
requestedVia: "mcp",
|
|
135
|
+
requestedBy: auth.actor,
|
|
136
|
+
},
|
|
137
|
+
authContextFromMcp(auth),
|
|
138
|
+
);
|
|
139
|
+
return { ok: true, action: "shutdown", orchestratorId: result.orchestratorId, command: result.command };
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (e instanceof ShutdownAuthError) throw new McpAuthError(e.message);
|
|
142
|
+
if (e instanceof ShutdownTargetError) throw new McpNotFoundError(e.message);
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
}
|