Files
paseo/packages/protocol/src/messages.hub.test.ts
T
Mohamed BoudraandGitHub 5c7623997c feat(hub): pass execution MCP servers to agents (#2813)
* feat(hub): pass execution MCP servers to agents

Hub-owned execution capabilities need to reach provider sessions without becoming part of outward agent snapshots. Keep the configuration daemon-private so provider recovery continues to work.

* fix(hub): protect daemon MCP namespace

Hub execution MCP configuration must not replace the daemon-owned Paseo server. Validate only after owned-record lookup so idempotent replay remains stable while new launches fail before provider or durable state creation.

* fix(agent): reject unsupported MCP launches

* fix(agent): scope MCP support guard

* fix(agent): redact MCP metadata from wire payloads

* fix(agent): validate MCP support on restored sessions

* fix(session): rehydrate MCP config on resume

* fix(session): resume newest matching agent record

* fix(session): restore archive state after failed resume
2026-08-03 13:49:53 +02:00

314 lines
9.3 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { z } from "zod";
import {
HubMessageCorrelationError,
SessionInboundMessageSchema,
SessionOutboundMessageSchema,
parseHubExecutionOutboundMessage,
} from "./messages.js";
const agent = {
id: "agent-1",
provider: "codex",
cwd: "/workspace",
model: null,
createdAt: "2026-07-13T00:00:00.000Z",
updatedAt: "2026-07-13T00:00:00.000Z",
lastUserMessageAt: null,
status: "idle",
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: false,
supportsReasoningStream: true,
supportsToolInvocations: true,
supportsRewindConversation: false,
supportsRewindFiles: false,
supportsRewindBoth: false,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: null,
labels: {},
};
// Frozen at the Hub create request shape shipped before worktree.
const PreviousHubAgentCreateRequestSchema = z.object({
type: z.literal("hub.execution.agent.create.request"),
requestId: z.string(),
executionId: z.string(),
provider: z.string(),
cwd: z.string(),
workspaceId: z.string().optional(),
prompt: z.string(),
model: z.string().optional(),
modeId: z.string().optional(),
thinkingOptionId: z.string().optional(),
featureValues: z.record(z.string(), z.unknown()).optional(),
env: z.record(z.string(), z.string()).optional(),
});
// Frozen at the Hub create request shape that temporarily carried turn-based auto-archive policy.
const PreviousHubAgentCreateWithAutoArchiveRequestSchema =
PreviousHubAgentCreateRequestSchema.extend({
worktree: z
.object({
mode: z.literal("branch-off"),
newBranch: z.string(),
base: z.string().optional(),
})
.optional(),
autoArchive: z.boolean().optional(),
});
describe("Hub session protocol", () => {
test("accepts the Hub execution create request", () => {
const message = {
type: "hub.execution.agent.create.request",
requestId: "request-1",
executionId: "execution-1",
provider: "codex",
cwd: "/workspace",
prompt: "Implement the requested change",
modeId: "code",
};
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
});
test("accepts an optional MCP server configuration on Hub creates", () => {
const message = {
type: "hub.execution.agent.create.request",
requestId: "request-mcp",
executionId: "execution-mcp",
provider: "codex",
cwd: "/workspace",
prompt: "Implement the requested change",
mcpServers: {
hub: {
type: "http",
url: "https://hub.example/mcp/executions/execution-mcp",
headers: { Authorization: "Bearer hub-execution-bearer" },
},
},
};
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
expect(PreviousHubAgentCreateRequestSchema.parse(message)).toEqual({
type: "hub.execution.agent.create.request",
requestId: "request-mcp",
executionId: "execution-mcp",
provider: "codex",
cwd: "/workspace",
prompt: "Implement the requested change",
});
});
test.each([
undefined,
{ mode: "branch-off", newBranch: "hub-work", base: "main" },
{ mode: "checkout-branch", branch: "existing-work" },
{ mode: "checkout-pr", prNumber: 42 },
])("accepts Hub create worktree target %#", (worktree) => {
const message = {
type: "hub.execution.agent.create.request",
requestId: "hub-worktree",
executionId: "execution-worktree",
provider: "codex",
cwd: "/repo",
prompt: "Work in the requested target",
...(worktree ? { worktree } : {}),
};
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
});
test("accepts old Hub creates while ignoring their removed auto-archive policy", () => {
const oldRequest = {
type: "hub.execution.agent.create.request" as const,
requestId: "hub-old-policy",
executionId: "execution-old-policy",
provider: "codex",
cwd: "/repo",
prompt: "Work in the requested target",
worktree: { mode: "branch-off" as const, newBranch: "hub-work", base: "main" },
autoArchive: true,
};
expect(PreviousHubAgentCreateWithAutoArchiveRequestSchema.parse(oldRequest)).toEqual(
oldRequest,
);
expect(SessionInboundMessageSchema.parse(oldRequest)).toEqual({
type: "hub.execution.agent.create.request",
requestId: "hub-old-policy",
executionId: "execution-old-policy",
provider: "codex",
cwd: "/repo",
prompt: "Work in the requested target",
worktree: { mode: "branch-off", newBranch: "hub-work", base: "main" },
});
});
test.each(["interrupt", "archive"] as const)(
"round-trips the Hub execution %s command",
(action) => {
const request = {
type: "hub.execution.control.request" as const,
requestId: `control-${action}`,
executionId: "execution-1",
action,
};
const response = {
type: "hub.execution.control.response" as const,
payload: {
requestId: request.requestId,
executionId: request.executionId,
action,
success: true,
error: null,
},
};
expect(SessionInboundMessageSchema.parse(request)).toEqual(request);
expect(SessionOutboundMessageSchema.parse(response)).toEqual(response);
expect(parseHubExecutionOutboundMessage(response)).toEqual(response);
},
);
test("the previous Hub create parser ignores additive worktree and auto-archive fields", () => {
const newRequest = {
type: "hub.execution.agent.create.request" as const,
requestId: "hub-worktree",
executionId: "execution-worktree",
provider: "codex",
cwd: "/repo",
prompt: "Work in the requested target",
worktree: { mode: "branch-off", newBranch: "hub-work", base: "main" },
autoArchive: true,
};
expect(PreviousHubAgentCreateRequestSchema.parse(newRequest)).toEqual({
type: "hub.execution.agent.create.request",
requestId: "hub-worktree",
executionId: "execution-worktree",
provider: "codex",
cwd: "/repo",
prompt: "Work in the requested target",
});
});
test.each([
{
type: "hub.execution.agent.create.response",
payload: {
requestId: "request-1",
executionId: "execution-1",
agentId: "agent-1",
agent,
success: true,
error: null,
},
},
{
type: "hub.execution.control.response",
payload: {
requestId: "control-1",
executionId: "execution-1",
action: "archive",
success: false,
error: "Execution not found",
},
},
{
type: "hub.execution.agent.update",
payload: { executionId: "execution-1", agentId: "agent-1", agent },
},
{
type: "hub.execution.agent.stream",
payload: {
executionId: "execution-1",
agentId: "agent-1",
event: { type: "turn_started", provider: "codex" },
},
},
])("accepts outbound variant $type", (message) => {
expect(SessionOutboundMessageSchema.parse(message)).toEqual(message);
expect(parseHubExecutionOutboundMessage(message)).toEqual(message);
});
test("rejects a Hub update whose correlated agent ids disagree", () => {
const malformed = {
type: "hub.execution.agent.update",
payload: { executionId: "execution-1", agentId: "agent-2", agent },
};
expect(SessionOutboundMessageSchema.safeParse(malformed).success).toBe(true);
expect(() => parseHubExecutionOutboundMessage(malformed)).toThrow(HubMessageCorrelationError);
});
test.each([
{
type: "hub.management.daemon.connect.request",
requestId: "r1",
hubUrl: "https://hub.example",
token: "token",
},
{ type: "hub.management.daemon.get_status.request", requestId: "r2" },
{ type: "hub.management.daemon.disconnect.request", requestId: "r3", force: true },
])("accepts trusted management request $type", (message) => {
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
});
test.each([
{
type: "hub.management.daemon.connect.response",
payload: {
requestId: "r1",
status: {
state: "connected",
daemonId: "daemon-1",
hubOrigin: "https://hub.example",
scopes: ["hub.execution.*"],
connectedAt: "2026-07-13T00:00:00.000Z",
lastError: null,
},
},
},
{
type: "hub.management.daemon.get_status.response",
payload: {
requestId: "r2",
status: {
state: "not_connected",
daemonId: null,
hubOrigin: null,
scopes: [],
connectedAt: null,
lastError: null,
},
},
},
{
type: "hub.management.daemon.disconnect.response",
payload: {
requestId: "r3",
status: {
state: "disconnecting",
daemonId: "daemon-1",
hubOrigin: "https://hub.example",
scopes: ["hub.execution.*"],
connectedAt: null,
lastError: "offline",
},
warning: "pending",
},
},
])("accepts trusted management response $type", (message) => {
expect(SessionOutboundMessageSchema.parse(message)).toEqual(message);
});
});