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
This commit is contained in:
+5
-2
@@ -45,8 +45,11 @@ replays the original prompt. A duplicate create returns the existing agent witho
|
|||||||
turn.
|
turn.
|
||||||
|
|
||||||
Hub creates use the same agent creation path as trusted clients. They may select any existing
|
Hub creates use the same agent creation path as trusted clients. They may select any existing
|
||||||
worktree target shape. Execution completion policy remains outside the daemon: a completed agent
|
worktree target shape and carry optional MCP server configuration for the agent session. The daemon
|
||||||
turn does not imply that the Hub execution is terminal.
|
keeps that configuration in its private agent record so provider sessions can recover after a
|
||||||
|
restart; neither ordinary client snapshots and updates nor Hub projections expose session
|
||||||
|
configuration. Execution completion policy remains outside the daemon: a completed agent turn does
|
||||||
|
not imply that the Hub execution is terminal.
|
||||||
|
|
||||||
The Hub ends an execution by sending `hub.execution.control.request` with the durable execution ID
|
The Hub ends an execution by sending `hub.execution.control.request` with the durable execution ID
|
||||||
and either `interrupt` or `archive`. The daemon resolves the agent from the authenticated daemon
|
and either `interrupt` or `archive`. The daemon resolves the agent from the authenticated daemon
|
||||||
|
|||||||
@@ -80,6 +80,34 @@ describe("Hub session protocol", () => {
|
|||||||
expect(SessionInboundMessageSchema.parse(message)).toEqual(message);
|
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([
|
test.each([
|
||||||
undefined,
|
undefined,
|
||||||
{ mode: "branch-off", newBranch: "hub-work", base: "main" },
|
{ mode: "branch-off", newBranch: "hub-work", base: "main" },
|
||||||
|
|||||||
@@ -2483,6 +2483,7 @@ export const HubExecutionAgentCreateRequestSchema = z.object({
|
|||||||
thinkingOptionId: z.string().optional(),
|
thinkingOptionId: z.string().optional(),
|
||||||
featureValues: z.record(z.string(), z.unknown()).optional(),
|
featureValues: z.record(z.string(), z.unknown()).optional(),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
|
mcpServers: z.record(z.string(), McpServerConfigSchema).optional(),
|
||||||
worktree: CreateAgentWorktreeTargetSchema.optional(),
|
worktree: CreateAgentWorktreeTargetSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,45 @@ class TestAgentSession implements AgentSession {
|
|||||||
async close(): Promise<void> {}
|
async close(): Promise<void> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class McpCapableTestAgentSession extends TestAgentSession {
|
||||||
|
override readonly capabilities = {
|
||||||
|
...TEST_CAPABILITIES,
|
||||||
|
supportsMcpServers: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class CloseRecordingTestAgentSession extends TestAgentSession {
|
||||||
|
closed = false;
|
||||||
|
|
||||||
|
override async close(): Promise<void> {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class McpCapableTestAgentClient extends TestAgentClient {
|
||||||
|
override readonly capabilities = {
|
||||||
|
...TEST_CAPABILITIES,
|
||||||
|
supportsMcpServers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
|
this.createdConfigs.push(config);
|
||||||
|
return new McpCapableTestAgentSession(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async resumeSession(
|
||||||
|
_handle: AgentPersistenceHandle,
|
||||||
|
config?: Partial<AgentSessionConfig>,
|
||||||
|
): Promise<AgentSession> {
|
||||||
|
this.resumeOverrides.push(config);
|
||||||
|
return new McpCapableTestAgentSession({
|
||||||
|
...config,
|
||||||
|
provider: this.provider,
|
||||||
|
cwd: config?.cwd ?? process.cwd(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ControlledInterruptSession extends TestAgentSession {
|
class ControlledInterruptSession extends TestAgentSession {
|
||||||
interruptCalled = false;
|
interruptCalled = false;
|
||||||
|
|
||||||
@@ -1846,7 +1885,7 @@ test("createAgent injects paseo MCP server only into provider launch config", as
|
|||||||
|
|
||||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
this.lastConfig = config;
|
this.lastConfig = config;
|
||||||
return new TestAgentSession(config);
|
return new McpCapableTestAgentSession(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1902,6 +1941,164 @@ test("createAgent injects paseo MCP server only into provider launch config", as
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("createAgent closes and rejects a provider session that cannot honor MCP servers", async () => {
|
||||||
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
|
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||||
|
let sessionClosed = false;
|
||||||
|
let promptStarted = false;
|
||||||
|
|
||||||
|
class UnsupportedMcpSession extends TestAgentSession {
|
||||||
|
override async run(): Promise<AgentRunResult> {
|
||||||
|
promptStarted = true;
|
||||||
|
return super.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
override async startTurn(): Promise<{ turnId: string }> {
|
||||||
|
promptStarted = true;
|
||||||
|
return super.startTurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
override async close(): Promise<void> {
|
||||||
|
sessionClosed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnsupportedMcpClient extends TestAgentClient {
|
||||||
|
override readonly capabilities = {
|
||||||
|
...TEST_CAPABILITIES,
|
||||||
|
supportsMcpServers: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
|
return new UnsupportedMcpSession(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = "00000000-0000-4000-8000-000000000107";
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: { codex: new UnsupportedMcpClient() },
|
||||||
|
registry: storage,
|
||||||
|
logger,
|
||||||
|
idFactory: () => agentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(
|
||||||
|
manager.createAgent(
|
||||||
|
{
|
||||||
|
provider: "codex",
|
||||||
|
cwd: workdir,
|
||||||
|
mcpServers: {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
url: "http://127.0.0.1:3000/api/executions/test/mcp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ workspaceId: undefined },
|
||||||
|
),
|
||||||
|
).rejects.toThrow("Provider 'codex' does not support MCP servers");
|
||||||
|
|
||||||
|
expect(sessionClosed).toBe(true);
|
||||||
|
expect(promptStarted).toBe(false);
|
||||||
|
expect(manager.getAgent(agentId)).toBeNull();
|
||||||
|
expect(await storage.get(agentId)).toBeNull();
|
||||||
|
} finally {
|
||||||
|
rmSync(workdir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resumeAgentFromPersistence closes and rejects a session that cannot honor external MCP", async () => {
|
||||||
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
|
const storage = new AgentStorage(join(workdir, "agents"), logger);
|
||||||
|
const replacement = new CloseRecordingTestAgentSession({ provider: "codex", cwd: workdir });
|
||||||
|
|
||||||
|
class UnsupportedResumeClient extends TestAgentClient {
|
||||||
|
override async resumeSession(): Promise<AgentSession> {
|
||||||
|
return replacement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentId = "00000000-0000-4000-8000-000000000108";
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: { codex: new UnsupportedResumeClient() },
|
||||||
|
registry: storage,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
const handle: AgentPersistenceHandle = {
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: "persist-unsupported-mcp",
|
||||||
|
metadata: { cwd: workdir },
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(
|
||||||
|
manager.resumeAgentFromPersistence(
|
||||||
|
handle,
|
||||||
|
{
|
||||||
|
cwd: workdir,
|
||||||
|
mcpServers: {
|
||||||
|
hub: { type: "http", url: "https://hub.test/mcp/executions/resume" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
agentId,
|
||||||
|
),
|
||||||
|
).rejects.toThrow("Provider 'codex' does not support MCP servers");
|
||||||
|
|
||||||
|
expect(replacement.closed).toBe(true);
|
||||||
|
expect(manager.getAgent(agentId)).toBeNull();
|
||||||
|
expect(await storage.get(agentId)).toBeNull();
|
||||||
|
} finally {
|
||||||
|
rmSync(workdir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reloadAgentSession preserves the live session when its replacement cannot honor external MCP", async () => {
|
||||||
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
|
const original = new CloseRecordingTestAgentSession({ provider: "codex", cwd: workdir });
|
||||||
|
const replacement = new CloseRecordingTestAgentSession({ provider: "codex", cwd: workdir });
|
||||||
|
|
||||||
|
class UnsupportedReloadClient extends TestAgentClient {
|
||||||
|
override async createSession(): Promise<AgentSession> {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async resumeSession(): Promise<AgentSession> {
|
||||||
|
return replacement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = new AgentManager({
|
||||||
|
clients: { codex: new UnsupportedReloadClient() },
|
||||||
|
registry: new AgentStorage(join(workdir, "agents"), logger),
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await manager.createAgent(
|
||||||
|
{ provider: "codex", cwd: workdir },
|
||||||
|
"00000000-0000-4000-8000-000000000109",
|
||||||
|
{ workspaceId: undefined },
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
manager.reloadAgentSession(created.id, {
|
||||||
|
mcpServers: {
|
||||||
|
hub: { type: "http", url: "https://hub.test/mcp/executions/reload" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Provider 'codex' does not support MCP servers");
|
||||||
|
|
||||||
|
expect(replacement.closed).toBe(true);
|
||||||
|
expect(original.closed).toBe(false);
|
||||||
|
expect(manager.getAgent(created.id)?.session).toBe(original);
|
||||||
|
expect(manager.getAgent(created.id)?.lifecycle).toBe("idle");
|
||||||
|
} finally {
|
||||||
|
rmSync(workdir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("createAgent passes native Paseo tools through launch context without internal MCP", async () => {
|
test("createAgent passes native Paseo tools through launch context without internal MCP", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
@@ -1930,7 +2127,7 @@ test("createAgent passes native Paseo tools through launch context without inter
|
|||||||
): Promise<AgentSession> {
|
): Promise<AgentSession> {
|
||||||
this.lastConfig = config;
|
this.lastConfig = config;
|
||||||
this.lastLaunchContext = launchContext;
|
this.lastLaunchContext = launchContext;
|
||||||
return new TestAgentSession(config);
|
return new McpCapableTestAgentSession(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1984,7 +2181,7 @@ test("createAgent passes native Paseo tools through launch context without inter
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("createAgent injects the MCP auth token as a bearer header into the launch config", async () => {
|
test("createAgent allows best-effort internal MCP when the provider session reports no support", async () => {
|
||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
const storage = new AgentStorage(storagePath, logger);
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
@@ -2033,7 +2230,7 @@ test("resumeAgentFromPersistence replaces stored internal paseo MCP with current
|
|||||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||||
const storagePath = join(workdir, "agents");
|
const storagePath = join(workdir, "agents");
|
||||||
const storage = new AgentStorage(storagePath, logger);
|
const storage = new AgentStorage(storagePath, logger);
|
||||||
const client = new TestAgentClient();
|
const client = new McpCapableTestAgentClient();
|
||||||
const manager = new AgentManager({
|
const manager = new AgentManager({
|
||||||
clients: {
|
clients: {
|
||||||
codex: client,
|
codex: client,
|
||||||
@@ -2127,7 +2324,7 @@ test("createAgent preserves a user-provided paseo MCP config", async () => {
|
|||||||
|
|
||||||
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
override async createSession(config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
this.lastConfig = config;
|
this.lastConfig = config;
|
||||||
return new TestAgentSession(config);
|
return new McpCapableTestAgentSession(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2455,7 +2652,10 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p
|
|||||||
|
|
||||||
class ResumeCaptureClient implements AgentClient {
|
class ResumeCaptureClient implements AgentClient {
|
||||||
readonly provider = "codex" as const;
|
readonly provider = "codex" as const;
|
||||||
readonly capabilities = TEST_CAPABILITIES;
|
readonly capabilities = {
|
||||||
|
...TEST_CAPABILITIES,
|
||||||
|
supportsMcpServers: true,
|
||||||
|
};
|
||||||
lastResumeOverrides: Partial<AgentSessionConfig> | undefined;
|
lastResumeOverrides: Partial<AgentSessionConfig> | undefined;
|
||||||
lastResumeLaunchContext: AgentLaunchContext | undefined;
|
lastResumeLaunchContext: AgentLaunchContext | undefined;
|
||||||
|
|
||||||
@@ -2495,7 +2695,7 @@ test("resumeAgentFromPersistence keeps metadata config, applies overrides, and p
|
|||||||
provider: "codex",
|
provider: "codex",
|
||||||
cwd: overrides?.cwd ?? metadata.cwd ?? process.cwd(),
|
cwd: overrides?.cwd ?? metadata.cwd ?? process.cwd(),
|
||||||
};
|
};
|
||||||
return new TestAgentSession(merged);
|
return new McpCapableTestAgentSession(merged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1089,6 +1089,7 @@ export class AgentManager {
|
|||||||
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
|
const providerLaunchConfig = this.resolveProviderLaunchConfig(launchConfig, launchContext);
|
||||||
const createOptions = this.buildCreateSessionOptions(options);
|
const createOptions = this.buildCreateSessionOptions(options);
|
||||||
const session = await client.createSession(providerLaunchConfig, launchContext, createOptions);
|
const session = await client.createSession(providerLaunchConfig, launchContext, createOptions);
|
||||||
|
await this.requireExternalMcpSupport(session, storedConfig);
|
||||||
return this.registerSession(session, storedConfig, resolvedAgentId, {
|
return this.registerSession(session, storedConfig, resolvedAgentId, {
|
||||||
labels: options.labels,
|
labels: options.labels,
|
||||||
initialTitle: options.initialTitle,
|
initialTitle: options.initialTitle,
|
||||||
@@ -1171,6 +1172,7 @@ export class AgentManager {
|
|||||||
launchContext,
|
launchContext,
|
||||||
resumeOptions,
|
resumeOptions,
|
||||||
);
|
);
|
||||||
|
await this.requireExternalMcpSupport(session, storedConfig);
|
||||||
return this.registerSession(session, storedConfig, resolvedAgentId, {
|
return this.registerSession(session, storedConfig, resolvedAgentId, {
|
||||||
...options,
|
...options,
|
||||||
persistence: handle,
|
persistence: handle,
|
||||||
@@ -1297,6 +1299,7 @@ export class AgentManager {
|
|||||||
const session = handle
|
const session = handle
|
||||||
? await client.resumeSession(handle, providerLaunchConfig, launchContext)
|
? await client.resumeSession(handle, providerLaunchConfig, launchContext)
|
||||||
: await client.createSession(providerLaunchConfig, launchContext);
|
: await client.createSession(providerLaunchConfig, launchContext);
|
||||||
|
await this.requireExternalMcpSupport(session, storedConfig);
|
||||||
|
|
||||||
let handedToRegistration = false;
|
let handedToRegistration = false;
|
||||||
try {
|
try {
|
||||||
@@ -2895,6 +2898,20 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async requireExternalMcpSupport(
|
||||||
|
session: AgentSession,
|
||||||
|
storedConfig: AgentSessionConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
Object.keys(storedConfig.mcpServers ?? {}).length === 0 ||
|
||||||
|
session.capabilities.supportsMcpServers === true
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.closeUnregisteredSession(session);
|
||||||
|
throw new Error(`Provider '${storedConfig.provider}' does not support MCP servers`);
|
||||||
|
}
|
||||||
|
|
||||||
private async initializeAgentTimelineForRegister(params: {
|
private async initializeAgentTimelineForRegister(params: {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
now: Date;
|
now: Date;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import { AGENT_LIFECYCLE_STATUSES } from "./agent-manager.js";
|
import { AGENT_LIFECYCLE_STATUSES } from "./agent-manager.js";
|
||||||
import {
|
import {
|
||||||
|
buildStoredAgentPayload,
|
||||||
toAgentPayload,
|
toAgentPayload,
|
||||||
toRecentProviderSessionDescriptorPayload,
|
toRecentProviderSessionDescriptorPayload,
|
||||||
toStoredAgentRecord,
|
toStoredAgentRecord,
|
||||||
@@ -328,7 +329,16 @@ describe("toAgentPayload", () => {
|
|||||||
provider: "codex",
|
provider: "codex",
|
||||||
sessionId: "persist-99",
|
sessionId: "persist-99",
|
||||||
nativeHandle: { id: "native" } as unknown,
|
nativeHandle: { id: "native" } as unknown,
|
||||||
metadata: { restored: new Date("2025-03-01T00:00:00.000Z"), empty: {} },
|
metadata: {
|
||||||
|
restored: new Date("2025-03-01T00:00:00.000Z"),
|
||||||
|
empty: {},
|
||||||
|
mcpServers: {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
headers: { Authorization: "Bearer projection-secret" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const payload = toAgentPayload(agent);
|
const payload = toAgentPayload(agent);
|
||||||
@@ -342,6 +352,62 @@ describe("toAgentPayload", () => {
|
|||||||
expect(agent.persistence!.sessionId).toBe("persist-99");
|
expect(agent.persistence!.sessionId).toBe("persist-99");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes empty persistence metadata after projecting MCP configuration", () => {
|
||||||
|
const payload = toAgentPayload(
|
||||||
|
createManagedAgent({
|
||||||
|
provider: "codex",
|
||||||
|
config: { provider: "codex" },
|
||||||
|
persistence: {
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: "persist-mcp-only",
|
||||||
|
metadata: { mcpServers: { hub: { type: "http", url: "https://hub.test/mcp" } } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(payload.persistence).toEqual({
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: "persist-mcp-only",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips MCP metadata from stored wire payloads while preserving private persistence", () => {
|
||||||
|
const record = toStoredAgentRecord(
|
||||||
|
createManagedAgent({
|
||||||
|
provider: "codex",
|
||||||
|
config: { provider: "codex" },
|
||||||
|
persistence: {
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: "persist-stored",
|
||||||
|
metadata: {
|
||||||
|
conversationId: "conversation-stored",
|
||||||
|
mcpServers: {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
headers: { Authorization: "Bearer stored-projection-secret" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = buildStoredAgentPayload(record, ["codex"]);
|
||||||
|
|
||||||
|
expect(record.persistence?.metadata).toEqual({
|
||||||
|
conversationId: "conversation-stored",
|
||||||
|
mcpServers: {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
headers: { Authorization: "Bearer stored-projection-secret" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(payload.persistence?.metadata).toEqual({
|
||||||
|
conversationId: "conversation-stored",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("omits lastUsage when not available", () => {
|
it("omits lastUsage when not available", () => {
|
||||||
const agent = createManagedAgent({ lastUsage: undefined });
|
const agent = createManagedAgent({ lastUsage: undefined });
|
||||||
const payload = toAgentPayload(agent);
|
const payload = toAgentPayload(agent);
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export function toAgentPayload(
|
|||||||
availableModes: cloneAvailableModes(agent.availableModes),
|
availableModes: cloneAvailableModes(agent.availableModes),
|
||||||
features: normalizeFeatures(agent.features),
|
features: normalizeFeatures(agent.features),
|
||||||
pendingPermissions: sanitizePendingPermissions(agent.pendingPermissions),
|
pendingPermissions: sanitizePendingPermissions(agent.pendingPermissions),
|
||||||
persistence: sanitizePersistenceHandle(agent.persistence),
|
persistence: projectPersistenceHandleForWire(agent.persistence),
|
||||||
title: options?.title ?? null,
|
title: options?.title ?? null,
|
||||||
labels: agent.labels,
|
labels: agent.labels,
|
||||||
};
|
};
|
||||||
@@ -213,7 +213,9 @@ export function buildStoredAgentPayload(
|
|||||||
|
|
||||||
const runtimeInfo = buildStoredRuntimeInfo(record);
|
const runtimeInfo = buildStoredRuntimeInfo(record);
|
||||||
const providerAvailable = isStoredAgentProviderAvailable(record, validProviders);
|
const providerAvailable = isStoredAgentProviderAvailable(record, validProviders);
|
||||||
const persistence = buildStoredPersistenceHandle(record, validProviders);
|
const persistence = projectPersistenceHandleForWire(
|
||||||
|
buildStoredPersistenceHandle(record, validProviders),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
@@ -365,6 +367,20 @@ function sanitizePersistenceHandle(
|
|||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function projectPersistenceHandleForWire(
|
||||||
|
handle: AgentPersistenceHandle | null,
|
||||||
|
): AgentPersistenceHandle | null {
|
||||||
|
const projected = sanitizePersistenceHandle(handle);
|
||||||
|
if (!projected?.metadata) {
|
||||||
|
return projected;
|
||||||
|
}
|
||||||
|
delete projected.metadata.mcpServers;
|
||||||
|
if (Object.keys(projected.metadata).length === 0) {
|
||||||
|
delete projected.metadata;
|
||||||
|
}
|
||||||
|
return projected;
|
||||||
|
}
|
||||||
|
|
||||||
function cloneCapabilities(capabilities: AgentCapabilityFlags): AgentCapabilityFlags {
|
function cloneCapabilities(capabilities: AgentCapabilityFlags): AgentCapabilityFlags {
|
||||||
return { ...capabilities };
|
return { ...capabilities };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
DaemonClient,
|
DaemonClient,
|
||||||
} from "./test-utils/index.js";
|
} from "./test-utils/index.js";
|
||||||
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
|
import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js";
|
||||||
|
import { createTestAgentClients } from "./test-utils/fake-agent-client.js";
|
||||||
import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-configs.js";
|
import { getFullAccessConfig, getAskModeConfig } from "./daemon-e2e/agent-configs.js";
|
||||||
import { parsePcm16MonoWav, wordSimilarity } from "./test-utils/dictation-e2e.js";
|
import { parsePcm16MonoWav, wordSimilarity } from "./test-utils/dictation-e2e.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -315,6 +316,88 @@ class StubAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RecordingMcpResumeClient implements AgentClient {
|
||||||
|
readonly provider = "codex" as const;
|
||||||
|
readonly capabilities;
|
||||||
|
readonly resumeOverrides: Array<Partial<AgentSessionConfig> | undefined> = [];
|
||||||
|
private readonly inner = createTestAgentClients({ supportsMcpServers: true }).codex;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.capabilities = this.inner.capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAvailable() {
|
||||||
|
return this.inner.isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
createSession(config: AgentSessionConfig) {
|
||||||
|
return this.inner.createSession(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeSession(
|
||||||
|
handle: AgentPersistenceHandle,
|
||||||
|
overrides?: Partial<AgentSessionConfig>,
|
||||||
|
): Promise<AgentSession> {
|
||||||
|
this.resumeOverrides.push(overrides);
|
||||||
|
return this.inner.resumeSession(handle, overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCatalog(...args: Parameters<AgentClient["fetchCatalog"]>) {
|
||||||
|
return this.inner.fetchCatalog(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnsupportedMcpResumeSession extends StubAgentSession {
|
||||||
|
closed = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
sessionId: "unsupported-mcp-resume-session",
|
||||||
|
supportsStreaming: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override async close(): Promise<void> {
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RejectingMcpResumeClient implements AgentClient {
|
||||||
|
readonly provider = "codex" as const;
|
||||||
|
readonly capabilities;
|
||||||
|
readonly nativeArchiveCalls: string[] = [];
|
||||||
|
readonly replacement = new UnsupportedMcpResumeSession();
|
||||||
|
private readonly inner = createTestAgentClients({ supportsMcpServers: true }).codex;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.capabilities = this.inner.capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAvailable() {
|
||||||
|
return this.inner.isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
createSession(config: AgentSessionConfig) {
|
||||||
|
return this.inner.createSession(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeSession(): Promise<AgentSession> {
|
||||||
|
return this.replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchCatalog(...args: Parameters<AgentClient["fetchCatalog"]>) {
|
||||||
|
return this.inner.fetchCatalog(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveNativeSession(): Promise<void> {
|
||||||
|
this.nativeArchiveCalls.push("archive");
|
||||||
|
}
|
||||||
|
|
||||||
|
async unarchiveNativeSession(): Promise<void> {
|
||||||
|
this.nativeArchiveCalls.push("unarchive");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test("createAgent fails when the initial turn cannot start", async () => {
|
test("createAgent fails when the initial turn cannot start", async () => {
|
||||||
const testAgent = new StubAgentClient({
|
const testAgent = new StubAgentClient({
|
||||||
sessionId: "start-turn-failure-session",
|
sessionId: "start-turn-failure-session",
|
||||||
@@ -860,6 +943,116 @@ test("resume_agent auto-unarchives archived agents", async () => {
|
|||||||
}
|
}
|
||||||
}, 180000);
|
}, 180000);
|
||||||
|
|
||||||
|
test("resume_agent rehydrates the newest private MCP config from a redacted persistence handle", async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
const provider = new RecordingMcpResumeClient();
|
||||||
|
const localCtx = await createDaemonTestContext({
|
||||||
|
agentClients: { codex: provider },
|
||||||
|
});
|
||||||
|
const bearerA = "durable-resume-bearer-a";
|
||||||
|
const bearerB = "durable-resume-bearer-b";
|
||||||
|
const externalMcpA = {
|
||||||
|
hub: {
|
||||||
|
type: "http" as const,
|
||||||
|
url: "https://hub.test/mcp/executions/durable-resume",
|
||||||
|
headers: { Authorization: `Bearer ${bearerA}` },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const externalMcpB = {
|
||||||
|
hub: {
|
||||||
|
...externalMcpA.hub,
|
||||||
|
headers: { Authorization: `Bearer ${bearerB}` },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await localCtx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd,
|
||||||
|
mcpServers: externalMcpA,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const projectedHandle = created.persistence;
|
||||||
|
if (!projectedHandle) {
|
||||||
|
throw new Error("Expected a projected persistence handle");
|
||||||
|
}
|
||||||
|
expect(projectedHandle.metadata).not.toHaveProperty("mcpServers");
|
||||||
|
expect(JSON.stringify(projectedHandle)).not.toContain(bearerA);
|
||||||
|
|
||||||
|
await localCtx.client.archiveAgent(created.id);
|
||||||
|
const resumedWithOverride = await localCtx.client.resumeAgent(projectedHandle, {
|
||||||
|
mcpServers: externalMcpB,
|
||||||
|
});
|
||||||
|
const newestProjectedHandle = resumedWithOverride.persistence;
|
||||||
|
if (!newestProjectedHandle) {
|
||||||
|
throw new Error("Expected a projected persistence handle after resume");
|
||||||
|
}
|
||||||
|
expect(provider.resumeOverrides[0]?.mcpServers?.hub).toEqual(externalMcpB.hub);
|
||||||
|
expect(JSON.stringify(resumedWithOverride)).not.toContain(bearerA);
|
||||||
|
expect(JSON.stringify(resumedWithOverride)).not.toContain(bearerB);
|
||||||
|
|
||||||
|
await localCtx.client.archiveAgent(resumedWithOverride.id);
|
||||||
|
const resumedNewest = await localCtx.client.resumeAgent(newestProjectedHandle);
|
||||||
|
|
||||||
|
expect(provider.resumeOverrides).toHaveLength(2);
|
||||||
|
expect(provider.resumeOverrides[1]?.mcpServers?.hub).toEqual(externalMcpB.hub);
|
||||||
|
expect(provider.resumeOverrides[1]?.mcpServers?.hub).not.toEqual(externalMcpA.hub);
|
||||||
|
expect(resumedNewest.persistence?.metadata).not.toHaveProperty("mcpServers");
|
||||||
|
expect(JSON.stringify(resumedNewest)).not.toContain(bearerA);
|
||||||
|
expect(JSON.stringify(resumedNewest)).not.toContain(bearerB);
|
||||||
|
} finally {
|
||||||
|
await localCtx.cleanup();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resume_agent restores archive state when an MCP-capable provider returns an unsupported session", async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
const provider = new RejectingMcpResumeClient();
|
||||||
|
const localCtx = await createDaemonTestContext({
|
||||||
|
agentClients: { codex: provider },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const created = await localCtx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
provider: "codex",
|
||||||
|
cwd,
|
||||||
|
mcpServers: {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
url: "https://hub.test/mcp/executions/rejected-resume",
|
||||||
|
headers: { Authorization: "Bearer rejected-resume-secret" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const handle = created.persistence;
|
||||||
|
if (!handle) {
|
||||||
|
throw new Error("Expected a projected persistence handle");
|
||||||
|
}
|
||||||
|
const archived = await localCtx.client.archiveAgent(created.id);
|
||||||
|
|
||||||
|
await expect(localCtx.client.resumeAgent(handle)).rejects.toMatchObject({
|
||||||
|
name: "DaemonRpcError",
|
||||||
|
code: "agent_resume_failed",
|
||||||
|
requestType: "resume_agent_request",
|
||||||
|
});
|
||||||
|
|
||||||
|
const archivedAgents = await localCtx.client.fetchAgents({
|
||||||
|
filter: { includeArchived: true },
|
||||||
|
});
|
||||||
|
const original = archivedAgents.entries.find((entry) => entry.agent.id === created.id)?.agent;
|
||||||
|
expect(original?.archivedAt).toBe(archived.archivedAt);
|
||||||
|
expect(provider.nativeArchiveCalls).toEqual(["archive", "unarchive", "archive"]);
|
||||||
|
expect(provider.replacement.closed).toBe(true);
|
||||||
|
} finally {
|
||||||
|
await localCtx.cleanup();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("update_agent persists unloaded title and labels across auto-unarchive", async () => {
|
test("update_agent persists unloaded title and labels across auto-unarchive", async () => {
|
||||||
const cwd = tmpCwd();
|
const cwd = tmpCwd();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -27,6 +27,114 @@ test("sequential replay after reconstruction keeps one durable owned agent", asy
|
|||||||
expect(reconstructed.durableAgentCount).toBe(1);
|
expect(reconstructed.durableAgentCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Hub MCP configuration reaches the provider alongside Paseo MCP without entering snapshots", async () => {
|
||||||
|
const hub = await HubRelationshipHarness.startWithAgentMcp();
|
||||||
|
await hub.beginConnect().result;
|
||||||
|
hub.connectLatestSocket();
|
||||||
|
relationship = hub;
|
||||||
|
const bearer = "hub-execution-bearer";
|
||||||
|
hub.beginOwnedCreate("mcp-create", "mcp-execution", {
|
||||||
|
mcpServers: {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
url: "https://hub.test/mcp/executions/mcp-execution",
|
||||||
|
headers: { Authorization: `Bearer ${bearer}` },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await hub.ownedCreateResult("mcp-create");
|
||||||
|
|
||||||
|
expect(hub.latestProviderCreateConfig()?.mcpServers).toMatchObject({
|
||||||
|
paseo: { type: "http" },
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
url: "https://hub.test/mcp/executions/mcp-execution",
|
||||||
|
headers: { Authorization: `Bearer ${bearer}` },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
type: "hub.execution.agent.create.response",
|
||||||
|
payload: { success: true, agent: { provider: "codex" } },
|
||||||
|
});
|
||||||
|
expect(response.payload.agent).not.toHaveProperty("config");
|
||||||
|
expect(response.payload.agent).not.toHaveProperty("mcpServers");
|
||||||
|
expect(response.payload.agent.persistence?.metadata).toEqual({
|
||||||
|
conversationId: response.payload.agent.persistence?.sessionId,
|
||||||
|
cwd: response.payload.agent.cwd,
|
||||||
|
});
|
||||||
|
expect(JSON.stringify(response.payload.agent)).not.toContain(bearer);
|
||||||
|
|
||||||
|
const update = hub.hubMessages().find((message) => message.type === "hub.execution.agent.update");
|
||||||
|
expect(update).toMatchObject({
|
||||||
|
type: "hub.execution.agent.update",
|
||||||
|
payload: { agent: { provider: "codex" } },
|
||||||
|
});
|
||||||
|
if (!update || update.type !== "hub.execution.agent.update") {
|
||||||
|
throw new Error("Expected a Hub execution agent update");
|
||||||
|
}
|
||||||
|
expect(update.payload.agent).not.toHaveProperty("config");
|
||||||
|
expect(update.payload.agent).not.toHaveProperty("mcpServers");
|
||||||
|
expect(update.payload.agent.persistence?.metadata).toEqual({
|
||||||
|
conversationId: update.payload.agent.persistence?.sessionId,
|
||||||
|
cwd: update.payload.agent.cwd,
|
||||||
|
});
|
||||||
|
expect(JSON.stringify(update.payload.agent)).not.toContain(bearer);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("new Hub executions cannot override the daemon-owned Paseo MCP server", async () => {
|
||||||
|
const hub = await launchRelationship();
|
||||||
|
hub.beginOwnedCreate("reserved-mcp-create", "reserved-mcp-execution", {
|
||||||
|
mcpServers: {
|
||||||
|
paseo: { type: "http", url: "https://hub.test/replace-paseo" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await hub.ownedCreateResult("reserved-mcp-create");
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
type: "hub.execution.agent.create.response",
|
||||||
|
payload: {
|
||||||
|
success: false,
|
||||||
|
executionId: "reserved-mcp-execution",
|
||||||
|
agentId: null,
|
||||||
|
agent: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(hub.providerCreations()).toBe(0);
|
||||||
|
expect(hub.activeOwnedAgentIds()).toEqual([]);
|
||||||
|
expect(await hub.durableOwnedAgentIds()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reserved Paseo MCP input does not invalidate replay of an owned execution", async () => {
|
||||||
|
const hub = await launchRelationship();
|
||||||
|
hub.beginOwnedCreate("original-create", "replayed-execution");
|
||||||
|
const original = await hub.ownedCreateResult("original-create");
|
||||||
|
expect(original).toMatchObject({
|
||||||
|
type: "hub.execution.agent.create.response",
|
||||||
|
payload: { success: true, executionId: "replayed-execution" },
|
||||||
|
});
|
||||||
|
const providerCreations = hub.providerCreations();
|
||||||
|
|
||||||
|
hub.beginOwnedCreate("replay-create", "replayed-execution", {
|
||||||
|
mcpServers: {
|
||||||
|
paseo: { type: "http", url: "https://hub.test/replace-paseo" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const replay = await hub.ownedCreateResult("replay-create");
|
||||||
|
|
||||||
|
expect(replay).toMatchObject({
|
||||||
|
type: "hub.execution.agent.create.response",
|
||||||
|
payload: {
|
||||||
|
success: true,
|
||||||
|
executionId: "replayed-execution",
|
||||||
|
agentId: original.payload.agentId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(hub.providerCreations()).toBe(providerCreations);
|
||||||
|
expect(await hub.durableOwnedAgentIds()).toEqual([original.payload.agentId]);
|
||||||
|
});
|
||||||
|
|
||||||
test("removing a daemon-owned agent removes its execution association", async () => {
|
test("removing a daemon-owned agent removes its execution association", async () => {
|
||||||
const hub = await launchRelationship();
|
const hub = await launchRelationship();
|
||||||
const created = await hub.createOwnedConcurrently();
|
const created = await hub.createOwnedConcurrently();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
} from "@getpaseo/protocol/messages";
|
} from "@getpaseo/protocol/messages";
|
||||||
|
|
||||||
import type { AgentManager, AgentManagerEvent, ManagedAgent } from "../agent/agent-manager.js";
|
import type { AgentManager, AgentManagerEvent, ManagedAgent } from "../agent/agent-manager.js";
|
||||||
|
import type { McpServerConfig } from "../agent/agent-sdk-types.js";
|
||||||
import type { AgentStorage, StoredAgentRecord } from "../agent/agent-storage.js";
|
import type { AgentStorage, StoredAgentRecord } from "../agent/agent-storage.js";
|
||||||
import type { BoundCreateAgentCommand } from "../agent/create-agent/create.js";
|
import type { BoundCreateAgentCommand } from "../agent/create-agent/create.js";
|
||||||
import type { CreatePaseoWorktreeWorkflowResult } from "../worktree-session.js";
|
import type { CreatePaseoWorktreeWorkflowResult } from "../worktree-session.js";
|
||||||
@@ -25,6 +26,7 @@ export interface HubExecutionAgentCreateInput {
|
|||||||
thinkingOptionId?: string;
|
thinkingOptionId?: string;
|
||||||
featureValues?: Record<string, unknown>;
|
featureValues?: Record<string, unknown>;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
mcpServers?: Record<string, McpServerConfig>;
|
||||||
worktree?: CreateAgentWorktreeTarget;
|
worktree?: CreateAgentWorktreeTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +177,7 @@ export class DaemonExecutions implements HubExecutionAgents {
|
|||||||
return this.resolveRecord(existing);
|
return this.resolveRecord(existing);
|
||||||
}
|
}
|
||||||
this.requireAuthority(authorityGeneration);
|
this.requireAuthority(authorityGeneration);
|
||||||
|
requireHubMcpNamespace(input.mcpServers);
|
||||||
|
|
||||||
let createdWorktree: CreatePaseoWorktreeWorkflowResult | null = null;
|
let createdWorktree: CreatePaseoWorktreeWorkflowResult | null = null;
|
||||||
let createdAgentId: string | null = null;
|
let createdAgentId: string | null = null;
|
||||||
@@ -192,6 +195,7 @@ export class DaemonExecutions implements HubExecutionAgents {
|
|||||||
thinking: input.thinkingOptionId,
|
thinking: input.thinkingOptionId,
|
||||||
features: input.featureValues,
|
features: input.featureValues,
|
||||||
env: input.env,
|
env: input.env,
|
||||||
|
...(input.mcpServers ? { config: { mcpServers: input.mcpServers } } : {}),
|
||||||
worktree: toCreateAgentWorktree(input.worktree),
|
worktree: toCreateAgentWorktree(input.worktree),
|
||||||
background: true,
|
background: true,
|
||||||
notifyOnFinish: false,
|
notifyOnFinish: false,
|
||||||
@@ -348,6 +352,12 @@ export class DaemonExecutions implements HubExecutionAgents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requireHubMcpNamespace(mcpServers: Record<string, McpServerConfig> | undefined): void {
|
||||||
|
if (mcpServers && Object.hasOwn(mcpServers, "paseo")) {
|
||||||
|
throw new Error('Hub execution MCP server name "paseo" is reserved by the daemon');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ownedCreatedWorktree(
|
function ownedCreatedWorktree(
|
||||||
worktree: CreatePaseoWorktreeWorkflowResult | null,
|
worktree: CreatePaseoWorktreeWorkflowResult | null,
|
||||||
): CreatePaseoWorktreeWorkflowResult | null {
|
): CreatePaseoWorktreeWorkflowResult | null {
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export class HubExecutionController {
|
|||||||
thinkingOptionId: message.thinkingOptionId,
|
thinkingOptionId: message.thinkingOptionId,
|
||||||
featureValues: message.featureValues,
|
featureValues: message.featureValues,
|
||||||
env: message.env,
|
env: message.env,
|
||||||
|
mcpServers: message.mcpServers,
|
||||||
worktree: message.worktree,
|
worktree: message.worktree,
|
||||||
});
|
});
|
||||||
if (this.closed) return;
|
if (this.closed) return;
|
||||||
|
|||||||
@@ -463,29 +463,48 @@ export class HubRelationshipHarness {
|
|||||||
private failNextSessionClose = false;
|
private failNextSessionClose = false;
|
||||||
private observedEnrollments = 0;
|
private observedEnrollments = 0;
|
||||||
private observedSockets = 0;
|
private observedSockets = 0;
|
||||||
private readonly codex = new ControlledAgentClient(
|
private readonly codex: ControlledAgentClient;
|
||||||
createTestAgentClients({
|
|
||||||
closeSession: async () => {
|
|
||||||
if (!this.failNextSessionClose) return;
|
|
||||||
this.failNextSessionClose = false;
|
|
||||||
throw new Error("Requested provider session close failure");
|
|
||||||
},
|
|
||||||
onStartTurn: (prompt) => {
|
|
||||||
const promptText = typeof prompt === "string" ? prompt : JSON.stringify(prompt);
|
|
||||||
if (this.promptsToFail.delete(promptText)) {
|
|
||||||
throw new Error("Requested provider prompt startup failure");
|
|
||||||
}
|
|
||||||
this.providerPrompts.push(prompt);
|
|
||||||
},
|
|
||||||
}).codex,
|
|
||||||
);
|
|
||||||
|
|
||||||
private constructor(private readonly archiveWatchFiles: ArchiveWatchFiles) {}
|
private constructor(
|
||||||
|
private readonly archiveWatchFiles: ArchiveWatchFiles,
|
||||||
|
private readonly mcpEnabled: boolean,
|
||||||
|
) {
|
||||||
|
this.codex = new ControlledAgentClient(
|
||||||
|
createTestAgentClients({
|
||||||
|
supportsMcpServers: mcpEnabled,
|
||||||
|
closeSession: async () => {
|
||||||
|
if (!this.failNextSessionClose) return;
|
||||||
|
this.failNextSessionClose = false;
|
||||||
|
throw new Error("Requested provider session close failure");
|
||||||
|
},
|
||||||
|
onStartTurn: (prompt) => {
|
||||||
|
const promptText = typeof prompt === "string" ? prompt : JSON.stringify(prompt);
|
||||||
|
if (this.promptsToFail.delete(promptText)) {
|
||||||
|
throw new Error("Requested provider prompt startup failure");
|
||||||
|
}
|
||||||
|
this.providerPrompts.push(prompt);
|
||||||
|
},
|
||||||
|
}).codex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static async start(
|
static async start(
|
||||||
archiveWatchFiles: ArchiveWatchFiles = nodeArchiveWatchFiles,
|
archiveWatchFiles: ArchiveWatchFiles = nodeArchiveWatchFiles,
|
||||||
): Promise<HubRelationshipHarness> {
|
): Promise<HubRelationshipHarness> {
|
||||||
const harness = new HubRelationshipHarness(archiveWatchFiles);
|
return this.launch(archiveWatchFiles, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async startWithAgentMcp(
|
||||||
|
archiveWatchFiles: ArchiveWatchFiles = nodeArchiveWatchFiles,
|
||||||
|
): Promise<HubRelationshipHarness> {
|
||||||
|
return this.launch(archiveWatchFiles, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async launch(
|
||||||
|
archiveWatchFiles: ArchiveWatchFiles,
|
||||||
|
mcpEnabled: boolean,
|
||||||
|
): Promise<HubRelationshipHarness> {
|
||||||
|
const harness = new HubRelationshipHarness(archiveWatchFiles, mcpEnabled);
|
||||||
await harness.createHome();
|
await harness.createHome();
|
||||||
await harness.startDaemon();
|
await harness.startDaemon();
|
||||||
return harness;
|
return harness;
|
||||||
@@ -675,6 +694,7 @@ export class HubRelationshipHarness {
|
|||||||
worktree?: CreateAgentWorktreeTarget;
|
worktree?: CreateAgentWorktreeTarget;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
modeId?: string;
|
modeId?: string;
|
||||||
|
mcpServers?: AgentSessionConfig["mcpServers"];
|
||||||
} = {},
|
} = {},
|
||||||
): void {
|
): void {
|
||||||
const { prompt = "Create through the Hub", ...requestOptions } = options;
|
const { prompt = "Create through the Hub", ...requestOptions } = options;
|
||||||
@@ -826,6 +846,14 @@ export class HubRelationshipHarness {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
latestProviderCreateConfig(): AgentSessionConfig | null {
|
||||||
|
return this.codex.createdConfigs.at(-1) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hubMessages(): SessionOutboundMessage[] {
|
||||||
|
return this.remote.sockets.flatMap(({ socket }) => socket.sent);
|
||||||
|
}
|
||||||
|
|
||||||
latestCreatedCwd(): string | null {
|
latestCreatedCwd(): string | null {
|
||||||
return this.codex.createdConfigs.at(-1)?.cwd ?? null;
|
return this.codex.createdConfigs.at(-1)?.cwd ?? null;
|
||||||
}
|
}
|
||||||
@@ -1285,7 +1313,7 @@ export class HubRelationshipHarness {
|
|||||||
paseoHome: this.paseoHome,
|
paseoHome: this.paseoHome,
|
||||||
corsAllowedOrigins: [],
|
corsAllowedOrigins: [],
|
||||||
hostnames: true,
|
hostnames: true,
|
||||||
mcpEnabled: false,
|
mcpEnabled: this.mcpEnabled,
|
||||||
staticDir,
|
staticDir,
|
||||||
mcpDebug: false,
|
mcpDebug: false,
|
||||||
agentClients: {
|
agentClients: {
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ import { CursorError } from "./pagination/cursor.js";
|
|||||||
import { SortablePager, type SortSpec } from "./pagination/sortable-pager.js";
|
import { SortablePager, type SortSpec } from "./pagination/sortable-pager.js";
|
||||||
import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js";
|
import type { SpeechToTextProvider, TextToSpeechProvider } from "./speech/speech-provider.js";
|
||||||
import type { TurnDetectionProvider } from "./speech/turn-detection-provider.js";
|
import type { TurnDetectionProvider } from "./speech/turn-detection-provider.js";
|
||||||
import { isStoredAgentProviderAvailable, toAgentPersistenceHandle } from "./persistence-hooks.js";
|
import {
|
||||||
|
buildConfigOverrides,
|
||||||
|
isStoredAgentProviderAvailable,
|
||||||
|
toAgentPersistenceHandle,
|
||||||
|
} from "./persistence-hooks.js";
|
||||||
import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent/agent-loading.js";
|
import { ensureAgentLoaded, ensureUnarchivedAgentLoaded } from "./agent/agent-loading.js";
|
||||||
import {
|
import {
|
||||||
formatSystemNotificationPrompt,
|
formatSystemNotificationPrompt,
|
||||||
@@ -87,7 +91,11 @@ import {
|
|||||||
setAgentModeCommand,
|
setAgentModeCommand,
|
||||||
updateAgentCommand,
|
updateAgentCommand,
|
||||||
} from "./agent/lifecycle-command.js";
|
} from "./agent/lifecycle-command.js";
|
||||||
import { buildStoredAgentPayload, toAgentPayload } from "./agent/agent-projections.js";
|
import {
|
||||||
|
buildStoredAgentPayload,
|
||||||
|
resolveStoredAgentPayloadUpdatedAt,
|
||||||
|
toAgentPayload,
|
||||||
|
} from "./agent/agent-projections.js";
|
||||||
import {
|
import {
|
||||||
appendTimelineItemIfAgentKnown,
|
appendTimelineItemIfAgentKnown,
|
||||||
emitLiveTimelineItemIfAgentKnown,
|
emitLiveTimelineItemIfAgentKnown,
|
||||||
@@ -2535,17 +2543,43 @@ export class Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unarchiveAgentByHandle(handle: AgentPersistenceHandle): Promise<void> {
|
private async unarchiveAgentByHandle(handle: AgentPersistenceHandle): Promise<{
|
||||||
|
record: StoredAgentRecord;
|
||||||
|
didUnarchive: boolean;
|
||||||
|
originalArchivedAt: string | null;
|
||||||
|
} | null> {
|
||||||
const records = await this.agentStorage.list();
|
const records = await this.agentStorage.list();
|
||||||
const matched = records.find(
|
const matched = records
|
||||||
(record) =>
|
.filter(
|
||||||
record.persistence?.provider === handle.provider &&
|
(record) =>
|
||||||
record.persistence?.sessionId === handle.sessionId,
|
record.persistence?.provider === handle.provider &&
|
||||||
);
|
record.persistence?.sessionId === handle.sessionId,
|
||||||
|
)
|
||||||
|
.reduce<StoredAgentRecord | null>((latest, candidate) => {
|
||||||
|
if (!latest) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
const updatedDelta =
|
||||||
|
Date.parse(resolveStoredAgentPayloadUpdatedAt(candidate)) -
|
||||||
|
Date.parse(resolveStoredAgentPayloadUpdatedAt(latest));
|
||||||
|
if (updatedDelta !== 0) {
|
||||||
|
return updatedDelta > 0 ? candidate : latest;
|
||||||
|
}
|
||||||
|
return Date.parse(candidate.createdAt) > Date.parse(latest.createdAt) ? candidate : latest;
|
||||||
|
}, null);
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
await unarchiveAgentState(this.agentStorage, this.agentManager, matched.id);
|
const didUnarchive = await unarchiveAgentState(
|
||||||
|
this.agentStorage,
|
||||||
|
this.agentManager,
|
||||||
|
matched.id,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
record: matched,
|
||||||
|
didUnarchive,
|
||||||
|
originalArchivedAt: matched.archivedAt ?? null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUpdateAgentRequest(
|
private async handleUpdateAgentRequest(
|
||||||
@@ -3304,8 +3338,19 @@ export class Session {
|
|||||||
`Resuming agent ${handle.sessionId} (${handle.provider})`,
|
`Resuming agent ${handle.sessionId} (${handle.provider})`,
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
await this.unarchiveAgentByHandle(handle);
|
const matched = await this.unarchiveAgentByHandle(handle);
|
||||||
const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides);
|
const effectiveOverrides = matched
|
||||||
|
? { ...buildConfigOverrides(matched.record), ...overrides }
|
||||||
|
: overrides;
|
||||||
|
let snapshot: ManagedAgent;
|
||||||
|
try {
|
||||||
|
snapshot = await this.agentManager.resumeAgentFromPersistence(handle, effectiveOverrides);
|
||||||
|
} catch (error) {
|
||||||
|
if (matched?.didUnarchive && matched.originalArchivedAt) {
|
||||||
|
await this.agentManager.archiveSnapshot(matched.record.id, matched.originalArchivedAt);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
|
await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id);
|
||||||
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
|
await this.agentManager.hydrateTimelineFromProvider(snapshot.id);
|
||||||
await this.agentUpdates.forwardLiveAgent(snapshot);
|
await this.agentUpdates.forwardLiveAgent(snapshot);
|
||||||
|
|||||||
@@ -99,10 +99,14 @@ function buildHarness() {
|
|||||||
const projectByWorkspaceId = new Map<string, ProjectPlacementPayload | null>();
|
const projectByWorkspaceId = new Map<string, ProjectPlacementPayload | null>();
|
||||||
let providerVisible: (provider: string) => boolean = () => true;
|
let providerVisible: (provider: string) => boolean = () => true;
|
||||||
let buildAgentPayloadError: Error | null = null;
|
let buildAgentPayloadError: Error | null = null;
|
||||||
|
let enrichProjectedPayload = false;
|
||||||
|
|
||||||
const service = createAgentUpdatesService({
|
const service = createAgentUpdatesService({
|
||||||
emit: (message) => emitted.push(message),
|
emit: (message) => emitted.push(message),
|
||||||
enrichAgentPayload: async (payload) => {
|
enrichAgentPayload: async (payload) => {
|
||||||
|
if (enrichProjectedPayload) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
const queuedPayload = queuedPayloadBuilds.shift();
|
const queuedPayload = queuedPayloadBuilds.shift();
|
||||||
if (queuedPayload) {
|
if (queuedPayload) {
|
||||||
return queuedPayload;
|
return queuedPayload;
|
||||||
@@ -152,6 +156,9 @@ function buildHarness() {
|
|||||||
setProviderVisible(fn: (provider: string) => boolean) {
|
setProviderVisible(fn: (provider: string) => boolean) {
|
||||||
providerVisible = fn;
|
providerVisible = fn;
|
||||||
},
|
},
|
||||||
|
useProjectedPayload() {
|
||||||
|
enrichProjectedPayload = true;
|
||||||
|
},
|
||||||
failBuildAgentPayload(error: Error) {
|
failBuildAgentPayload(error: Error) {
|
||||||
buildAgentPayloadError = error;
|
buildAgentPayloadError = error;
|
||||||
},
|
},
|
||||||
@@ -329,6 +336,56 @@ describe("forwardLiveAgent", () => {
|
|||||||
expect(h.workspaceUpdates).toEqual(["ws-1"]);
|
expect(h.workspaceUpdates).toEqual(["ws-1"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("never projects MCP credentials into ordinary client updates", async () => {
|
||||||
|
const h = buildHarness();
|
||||||
|
h.service.beginSubscription({ subscriptionId: "sub", filter: {} });
|
||||||
|
h.service.flushBootstrapped("sub");
|
||||||
|
h.register(makeAgentPayload({ id: "a", workspaceId: "ws-1" }));
|
||||||
|
h.useProjectedPayload();
|
||||||
|
const agent = h.managed("a");
|
||||||
|
agent.config.mcpServers = {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
url: "https://hub.test/mcp/executions/execution-1",
|
||||||
|
headers: { Authorization: "Bearer execution-secret" },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
agent.persistence = {
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: "session-a",
|
||||||
|
metadata: {
|
||||||
|
conversationId: "conversation-a",
|
||||||
|
mcpServers: agent.config.mcpServers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(agent.config).toMatchObject({
|
||||||
|
provider: "codex",
|
||||||
|
mcpServers: {
|
||||||
|
hub: {
|
||||||
|
type: "http",
|
||||||
|
url: "https://hub.test/mcp/executions/execution-1",
|
||||||
|
headers: { Authorization: "Bearer execution-secret" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await h.service.forwardLiveAgent(agent);
|
||||||
|
|
||||||
|
const update = h.agentUpdates()[0];
|
||||||
|
expect(update).toEqual({
|
||||||
|
kind: "upsert",
|
||||||
|
agent: expect.objectContaining({ id: "a" }),
|
||||||
|
project: makeProject(),
|
||||||
|
});
|
||||||
|
if (update?.kind !== "upsert") {
|
||||||
|
throw new Error("Expected an agent upsert");
|
||||||
|
}
|
||||||
|
expect(update.agent).not.toHaveProperty("config");
|
||||||
|
expect(update.agent).not.toHaveProperty("mcpServers");
|
||||||
|
expect(update.agent.persistence?.metadata).toEqual({ conversationId: "conversation-a" });
|
||||||
|
expect(JSON.stringify(update.agent)).not.toContain("execution-secret");
|
||||||
|
});
|
||||||
|
|
||||||
test("emits a remove when the agent's workspace resolves to no project", async () => {
|
test("emits a remove when the agent's workspace resolves to no project", async () => {
|
||||||
const h = buildHarness();
|
const h = buildHarness();
|
||||||
h.service.beginSubscription({ subscriptionId: "sub", filter: {} });
|
h.service.beginSubscription({ subscriptionId: "sub", filter: {} });
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ interface Deferred<T> {
|
|||||||
interface FakeAgentSessionOptions {
|
interface FakeAgentSessionOptions {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
config: AgentSessionConfig;
|
config: AgentSessionConfig;
|
||||||
|
supportsMcpServers?: boolean;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
memoryMarker?: string | null;
|
memoryMarker?: string | null;
|
||||||
closeSession?: () => Promise<void>;
|
closeSession?: () => Promise<void>;
|
||||||
@@ -63,6 +64,7 @@ interface FakeAgentSessionOptions {
|
|||||||
export interface TestAgentClientOptions {
|
export interface TestAgentClientOptions {
|
||||||
closeSession?: () => Promise<void>;
|
closeSession?: () => Promise<void>;
|
||||||
onStartTurn?: (prompt: AgentPromptInput) => void;
|
onStartTurn?: (prompt: AgentPromptInput) => void;
|
||||||
|
supportsMcpServers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDeferred<T>(): Deferred<T> {
|
function createDeferred<T>(): Deferred<T> {
|
||||||
@@ -317,7 +319,7 @@ function buildLargeTimelineItem(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FakeAgentSession implements AgentSession {
|
class FakeAgentSession implements AgentSession {
|
||||||
readonly capabilities = TEST_CAPABILITIES;
|
readonly capabilities: AgentCapabilityFlags;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
private readonly providerName: string;
|
private readonly providerName: string;
|
||||||
private readonly config: AgentSessionConfig;
|
private readonly config: AgentSessionConfig;
|
||||||
@@ -334,6 +336,10 @@ class FakeAgentSession implements AgentSession {
|
|||||||
private readonly onStartTurn: ((prompt: AgentPromptInput) => void) | undefined;
|
private readonly onStartTurn: ((prompt: AgentPromptInput) => void) | undefined;
|
||||||
|
|
||||||
constructor(options: FakeAgentSessionOptions) {
|
constructor(options: FakeAgentSessionOptions) {
|
||||||
|
this.capabilities = {
|
||||||
|
...TEST_CAPABILITIES,
|
||||||
|
supportsMcpServers: options.supportsMcpServers === true,
|
||||||
|
};
|
||||||
this.providerName = options.providerName;
|
this.providerName = options.providerName;
|
||||||
this.config = options.config;
|
this.config = options.config;
|
||||||
this.id = options.sessionId ?? randomUUID();
|
this.id = options.sessionId ?? randomUUID();
|
||||||
@@ -851,10 +857,14 @@ class FakeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describePersistence(): AgentPersistenceHandle | null {
|
describePersistence(): AgentPersistenceHandle | null {
|
||||||
|
const metadata = {
|
||||||
|
...(this.memoryMarker ? { marker: this.memoryMarker } : {}),
|
||||||
|
...(this.config.mcpServers ? { mcpServers: this.config.mcpServers } : {}),
|
||||||
|
};
|
||||||
return buildPersistence(
|
return buildPersistence(
|
||||||
this.providerName,
|
this.providerName,
|
||||||
this.id,
|
this.id,
|
||||||
this.memoryMarker ? { marker: this.memoryMarker } : undefined,
|
Object.keys(metadata).length > 0 ? metadata : undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1171,11 +1181,16 @@ class FakeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FakeAgentClient implements AgentClient {
|
class FakeAgentClient implements AgentClient {
|
||||||
readonly capabilities = TEST_CAPABILITIES;
|
readonly capabilities: AgentCapabilityFlags;
|
||||||
constructor(
|
constructor(
|
||||||
public readonly provider: string,
|
public readonly provider: string,
|
||||||
private readonly options: TestAgentClientOptions,
|
private readonly options: TestAgentClientOptions,
|
||||||
) {}
|
) {
|
||||||
|
this.capabilities = {
|
||||||
|
...TEST_CAPABILITIES,
|
||||||
|
supportsMcpServers: options.supportsMcpServers === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async createSession(
|
async createSession(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
@@ -1184,6 +1199,7 @@ class FakeAgentClient implements AgentClient {
|
|||||||
return new FakeAgentSession({
|
return new FakeAgentSession({
|
||||||
providerName: this.provider,
|
providerName: this.provider,
|
||||||
config: { ...config },
|
config: { ...config },
|
||||||
|
supportsMcpServers: this.options.supportsMcpServers,
|
||||||
closeSession: this.options.closeSession,
|
closeSession: this.options.closeSession,
|
||||||
onStartTurn: this.options.onStartTurn,
|
onStartTurn: this.options.onStartTurn,
|
||||||
});
|
});
|
||||||
@@ -1206,6 +1222,7 @@ class FakeAgentClient implements AgentClient {
|
|||||||
return new FakeAgentSession({
|
return new FakeAgentSession({
|
||||||
providerName: this.provider,
|
providerName: this.provider,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
supportsMcpServers: this.options.supportsMcpServers,
|
||||||
sessionId: handle.sessionId,
|
sessionId: handle.sessionId,
|
||||||
memoryMarker: typeof marker === "string" ? marker : null,
|
memoryMarker: typeof marker === "string" ? marker : null,
|
||||||
closeSession: this.options.closeSession,
|
closeSession: this.options.closeSession,
|
||||||
|
|||||||
Reference in New Issue
Block a user