Keep Paseo MCP config out of agent storage
Runtime-injected MCP endpoints can change with daemon listen settings, so compute them for launches instead of persisting them.
This commit is contained in:
@@ -542,7 +542,7 @@ test("createAgent injects daemon append system prompt at runtime only", async ()
|
||||
|
||||
expect(client.createdConfigs[0]?.systemPrompt).toBe("Agent instructions.");
|
||||
expect(client.createdConfigs[0]?.daemonAppendSystemPrompt).toBe("Daemon instructions.");
|
||||
expect(snapshot.config.daemonAppendSystemPrompt).toBe("Daemon instructions.");
|
||||
expect(snapshot.config).not.toHaveProperty("daemonAppendSystemPrompt");
|
||||
expect(record?.config?.systemPrompt).toBe("Agent instructions.");
|
||||
expect(record?.config).not.toHaveProperty("daemonAppendSystemPrompt");
|
||||
});
|
||||
@@ -948,7 +948,7 @@ test("createAgent passes persistSession to provider create options", async () =>
|
||||
rmSync(workdir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("createAgent injects paseo MCP server when manager has an MCP base URL", async () => {
|
||||
test("createAgent injects paseo MCP server only into provider launch config", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
@@ -985,6 +985,12 @@ test("createAgent injects paseo MCP server when manager has an MCP base URL", as
|
||||
});
|
||||
|
||||
expect(snapshot.config.mcpServers).toEqual({
|
||||
custom: {
|
||||
type: "stdio",
|
||||
command: "custom-mcp",
|
||||
},
|
||||
});
|
||||
expect(client.lastConfig?.mcpServers).toEqual({
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: `http://127.0.0.1:6767/mcp/agents?callerAgentId=${snapshot.id}`,
|
||||
@@ -994,7 +1000,102 @@ test("createAgent injects paseo MCP server when manager has an MCP base URL", as
|
||||
command: "custom-mcp",
|
||||
},
|
||||
});
|
||||
expect(client.lastConfig?.mcpServers).toEqual(snapshot.config.mcpServers);
|
||||
|
||||
const stored = await storage.get(snapshot.id);
|
||||
expect(stored?.config?.mcpServers).toEqual({
|
||||
custom: {
|
||||
type: "stdio",
|
||||
command: "custom-mcp",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("resumeAgentFromPersistence replaces stored internal paseo MCP with current runtime URL", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
const client = new TestAgentClient();
|
||||
const manager = new AgentManager({
|
||||
clients: {
|
||||
codex: client,
|
||||
},
|
||||
registry: storage,
|
||||
logger,
|
||||
mcpBaseUrl: "http://127.0.0.1:6768/mcp/agents",
|
||||
idFactory: () => "00000000-0000-4000-8000-000000000105",
|
||||
});
|
||||
const handle: AgentPersistenceHandle = {
|
||||
provider: "codex",
|
||||
sessionId: "session-123",
|
||||
metadata: {
|
||||
cwd: workdir,
|
||||
},
|
||||
};
|
||||
|
||||
const snapshot = await manager.resumeAgentFromPersistence(handle, {
|
||||
cwd: workdir,
|
||||
mcpServers: {
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=stale-agent",
|
||||
},
|
||||
custom: {
|
||||
type: "stdio",
|
||||
command: "custom-mcp",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(client.resumeOverrides[0]?.mcpServers).toEqual({
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: `http://127.0.0.1:6768/mcp/agents?callerAgentId=${snapshot.id}`,
|
||||
},
|
||||
custom: {
|
||||
type: "stdio",
|
||||
command: "custom-mcp",
|
||||
},
|
||||
});
|
||||
expect(snapshot.config.mcpServers).toEqual({
|
||||
custom: {
|
||||
type: "stdio",
|
||||
command: "custom-mcp",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("resumeAgentFromPersistence drops stored internal paseo MCP when runtime injection is disabled", async () => {
|
||||
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-"));
|
||||
const storagePath = join(workdir, "agents");
|
||||
const storage = new AgentStorage(storagePath, logger);
|
||||
const client = new TestAgentClient();
|
||||
const manager = new AgentManager({
|
||||
clients: {
|
||||
codex: client,
|
||||
},
|
||||
registry: storage,
|
||||
logger,
|
||||
});
|
||||
const handle: AgentPersistenceHandle = {
|
||||
provider: "codex",
|
||||
sessionId: "session-123",
|
||||
metadata: {
|
||||
cwd: workdir,
|
||||
},
|
||||
};
|
||||
|
||||
const snapshot = await manager.resumeAgentFromPersistence(handle, {
|
||||
cwd: workdir,
|
||||
mcpServers: {
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=stale-agent",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(client.resumeOverrides[0]?.mcpServers).toBeUndefined();
|
||||
expect(snapshot.config.mcpServers).toBeUndefined();
|
||||
});
|
||||
|
||||
test("createAgent preserves a user-provided paseo MCP config", async () => {
|
||||
|
||||
@@ -57,6 +57,7 @@ import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest
|
||||
import { IMPORTABLE_PROVIDERS } from "./provider-registry.js";
|
||||
import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
|
||||
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
|
||||
import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js";
|
||||
|
||||
const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000;
|
||||
const INTERRUPT_SESSION_TIMEOUT_MS = 2_000;
|
||||
@@ -74,6 +75,11 @@ const STORED_AGENT_CAPABILITIES: AgentCapabilityFlags = {
|
||||
|
||||
type TimeoutResult = "completed" | "timed_out";
|
||||
|
||||
interface PreparedSessionConfig {
|
||||
storedConfig: AgentSessionConfig;
|
||||
launchConfig: AgentSessionConfig;
|
||||
}
|
||||
|
||||
interface TimeoutOptions {
|
||||
operation: Promise<void>;
|
||||
timeoutMs: number;
|
||||
@@ -105,7 +111,7 @@ function buildStoredAgentConfig(record: StoredAgentRecord): AgentSessionConfig {
|
||||
config.systemPrompt = record.config.systemPrompt;
|
||||
}
|
||||
if (record.config.mcpServers != null) config.mcpServers = record.config.mcpServers;
|
||||
return config;
|
||||
return stripInternalPaseoMcpServer(config);
|
||||
}
|
||||
|
||||
export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus };
|
||||
@@ -811,30 +817,15 @@ export class AgentManager {
|
||||
},
|
||||
): Promise<ManagedAgent> {
|
||||
const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent");
|
||||
const injectedConfig =
|
||||
this.mcpBaseUrl == null
|
||||
? config
|
||||
: {
|
||||
...config,
|
||||
mcpServers: {
|
||||
paseo: {
|
||||
type: "http" as const,
|
||||
url: `${this.mcpBaseUrl}?callerAgentId=${resolvedAgentId}`,
|
||||
},
|
||||
...config.mcpServers,
|
||||
},
|
||||
};
|
||||
this.requireEnabledProvider(injectedConfig.provider);
|
||||
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
|
||||
await this.normalizeConfig(injectedConfig),
|
||||
);
|
||||
const { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId);
|
||||
this.requireEnabledProvider(storedConfig.provider);
|
||||
const launchContext = this.buildLaunchContext(resolvedAgentId, options?.env);
|
||||
const client = await this.requireAvailableClient({
|
||||
provider: normalizedConfig.provider,
|
||||
provider: storedConfig.provider,
|
||||
});
|
||||
const createOptions = this.buildCreateSessionOptions(options);
|
||||
const session = await client.createSession(normalizedConfig, launchContext, createOptions);
|
||||
return this.registerSession(session, normalizedConfig, resolvedAgentId, {
|
||||
const session = await client.createSession(launchConfig, launchContext, createOptions);
|
||||
return this.registerSession(session, storedConfig, resolvedAgentId, {
|
||||
labels: options?.labels,
|
||||
workspaceId: options?.workspaceId,
|
||||
initialTitle: options?.initialTitle,
|
||||
@@ -872,26 +863,10 @@ export class AgentManager {
|
||||
...overrides,
|
||||
provider: handle.provider,
|
||||
} as AgentSessionConfig;
|
||||
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
|
||||
await this.normalizeConfig(mergedConfig),
|
||||
const { storedConfig, launchConfig } = await this.prepareSessionConfig(
|
||||
mergedConfig,
|
||||
resolvedAgentId,
|
||||
);
|
||||
const resumeOverrides: Partial<AgentSessionConfig> = { ...overrides };
|
||||
let hasResumeOverrides = overrides !== undefined;
|
||||
|
||||
if (normalizedConfig.model !== mergedConfig.model) {
|
||||
resumeOverrides.model = normalizedConfig.model;
|
||||
hasResumeOverrides = true;
|
||||
}
|
||||
|
||||
if (normalizedConfig.modeId !== mergedConfig.modeId) {
|
||||
resumeOverrides.modeId = normalizedConfig.modeId;
|
||||
hasResumeOverrides = true;
|
||||
}
|
||||
|
||||
if (metadata.daemonAppendSystemPrompt !== normalizedConfig.daemonAppendSystemPrompt) {
|
||||
resumeOverrides.daemonAppendSystemPrompt = normalizedConfig.daemonAppendSystemPrompt;
|
||||
hasResumeOverrides = true;
|
||||
}
|
||||
|
||||
const launchContext = this.buildLaunchContext(resolvedAgentId);
|
||||
const client = this.requireClient(handle.provider);
|
||||
@@ -901,12 +876,8 @@ export class AgentManager {
|
||||
`Provider '${handle.provider}' is not available. Please ensure the CLI is installed.`,
|
||||
);
|
||||
}
|
||||
const session = await client.resumeSession(
|
||||
handle,
|
||||
hasResumeOverrides ? resumeOverrides : undefined,
|
||||
launchContext,
|
||||
);
|
||||
return this.registerSession(session, normalizedConfig, resolvedAgentId, options);
|
||||
const session = await client.resumeSession(handle, launchConfig, launchContext);
|
||||
return this.registerSession(session, storedConfig, resolvedAgentId, options);
|
||||
}
|
||||
|
||||
// Hot-reload an active agent session with config overrides. By default the
|
||||
@@ -938,14 +909,12 @@ export class AgentManager {
|
||||
...overrides,
|
||||
provider,
|
||||
} as AgentSessionConfig;
|
||||
const normalizedConfig = this.applyDaemonAppendSystemPrompt(
|
||||
await this.normalizeConfig(refreshConfig),
|
||||
);
|
||||
const { storedConfig, launchConfig } = await this.prepareSessionConfig(refreshConfig, agentId);
|
||||
const launchContext = this.buildLaunchContext(agentId);
|
||||
|
||||
const session = handle
|
||||
? await client.resumeSession(handle, normalizedConfig, launchContext)
|
||||
: await client.createSession(normalizedConfig, launchContext);
|
||||
? await client.resumeSession(handle, launchConfig, launchContext)
|
||||
: await client.createSession(launchConfig, launchContext);
|
||||
|
||||
this.agentStreamCoalescer.flushAndDiscard(agentId);
|
||||
// Remove the existing agent entry before swapping sessions
|
||||
@@ -966,7 +935,7 @@ export class AgentManager {
|
||||
}
|
||||
|
||||
// Preserve existing labels and timeline during reload.
|
||||
return this.registerSession(session, normalizedConfig, agentId, {
|
||||
return this.registerSession(session, storedConfig, agentId, {
|
||||
labels: existing.labels,
|
||||
createdAt: existing.createdAt,
|
||||
updatedAt: existing.updatedAt,
|
||||
@@ -3489,6 +3458,21 @@ export class AgentManager {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private async prepareSessionConfig(
|
||||
config: AgentSessionConfig,
|
||||
agentId: string,
|
||||
): Promise<PreparedSessionConfig> {
|
||||
const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config));
|
||||
const launchConfig = this.applyDaemonAppendSystemPrompt(
|
||||
withRuntimePaseoMcpServer({
|
||||
config: storedConfig,
|
||||
agentId,
|
||||
mcpBaseUrl: this.mcpBaseUrl,
|
||||
}),
|
||||
);
|
||||
return { storedConfig, launchConfig };
|
||||
}
|
||||
|
||||
private applyDaemonAppendSystemPrompt(config: AgentSessionConfig): AgentSessionConfig {
|
||||
const daemonAppendSystemPrompt = this.appendSystemPrompt.trim();
|
||||
const next = { ...config };
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { AgentSessionConfig, McpServerConfig } from "./agent-sdk-types.js";
|
||||
|
||||
const PASEO_MCP_SERVER_NAME = "paseo";
|
||||
const PASEO_MCP_PATHNAME = "/mcp/agents";
|
||||
|
||||
export function stripInternalPaseoMcpServer(config: AgentSessionConfig): AgentSessionConfig {
|
||||
const mcpServers = config.mcpServers;
|
||||
if (!mcpServers) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const paseoServer = mcpServers[PASEO_MCP_SERVER_NAME];
|
||||
if (!paseoServer || !isInternalPaseoMcpServer(paseoServer)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const nextMcpServers = { ...mcpServers };
|
||||
delete nextMcpServers[PASEO_MCP_SERVER_NAME];
|
||||
|
||||
const next = { ...config };
|
||||
if (Object.keys(nextMcpServers).length > 0) {
|
||||
next.mcpServers = nextMcpServers;
|
||||
} else {
|
||||
delete next.mcpServers;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function withRuntimePaseoMcpServer(params: {
|
||||
config: AgentSessionConfig;
|
||||
agentId: string;
|
||||
mcpBaseUrl: string | null;
|
||||
}): AgentSessionConfig {
|
||||
const storedConfig = stripInternalPaseoMcpServer(params.config);
|
||||
if (!params.mcpBaseUrl || storedConfig.mcpServers?.[PASEO_MCP_SERVER_NAME]) {
|
||||
return storedConfig;
|
||||
}
|
||||
|
||||
return {
|
||||
...storedConfig,
|
||||
mcpServers: {
|
||||
[PASEO_MCP_SERVER_NAME]: {
|
||||
type: "http",
|
||||
url: `${params.mcpBaseUrl}?callerAgentId=${params.agentId}`,
|
||||
},
|
||||
...storedConfig.mcpServers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isInternalPaseoMcpServer(config: McpServerConfig): boolean {
|
||||
if (config.type !== "http" && config.type !== "sse") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(config.url).pathname === PASEO_MCP_PATHNAME;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,54 @@ describe("persistence hooks", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("buildConfigOverrides drops persisted internal paseo MCP server", () => {
|
||||
const record = createRecord({
|
||||
config: {
|
||||
modeId: "default",
|
||||
model: "gpt-5.4-mini",
|
||||
mcpServers: {
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=stale-agent",
|
||||
},
|
||||
custom: {
|
||||
type: "stdio",
|
||||
command: "custom-mcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(buildConfigOverrides(record).mcpServers).toEqual({
|
||||
custom: {
|
||||
type: "stdio",
|
||||
command: "custom-mcp",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("buildConfigOverrides preserves user-provided paseo MCP server", () => {
|
||||
const record = createRecord({
|
||||
config: {
|
||||
modeId: "default",
|
||||
model: "gpt-5.4-mini",
|
||||
mcpServers: {
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: "https://example.com/custom-paseo",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(buildConfigOverrides(record).mcpServers).toEqual({
|
||||
paseo: {
|
||||
type: "http",
|
||||
url: "https://example.com/custom-paseo",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("buildSessionConfig accepts providers from the canonical manifest", () => {
|
||||
const record = createRecord({
|
||||
provider: "claude",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AgentManager } from "./agent/agent-manager.js";
|
||||
import { stripInternalPaseoMcpServer } from "./agent/runtime-mcp-config.js";
|
||||
import type {
|
||||
AgentPersistenceHandle,
|
||||
AgentProvider,
|
||||
@@ -49,7 +50,8 @@ export function attachAgentStoragePersistence(
|
||||
}
|
||||
|
||||
export function buildConfigOverrides(record: StoredAgentRecord): Partial<AgentSessionConfig> {
|
||||
return {
|
||||
return stripInternalPaseoMcpServer({
|
||||
provider: record.provider,
|
||||
cwd: record.cwd,
|
||||
modeId: record.lastModeId ?? record.config?.modeId ?? undefined,
|
||||
model: record.config?.model ?? undefined,
|
||||
@@ -58,7 +60,7 @@ export function buildConfigOverrides(record: StoredAgentRecord): Partial<AgentSe
|
||||
extra: record.config?.extra ?? undefined,
|
||||
systemPrompt: record.config?.systemPrompt ?? undefined,
|
||||
mcpServers: record.config?.mcpServers ?? undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function buildSessionConfig(
|
||||
@@ -71,7 +73,7 @@ export function buildSessionConfig(
|
||||
return null;
|
||||
}
|
||||
const overrides = buildConfigOverrides(record);
|
||||
return {
|
||||
return stripInternalPaseoMcpServer({
|
||||
provider: record.provider,
|
||||
cwd: record.cwd,
|
||||
modeId: overrides.modeId,
|
||||
@@ -81,7 +83,7 @@ export function buildSessionConfig(
|
||||
extra: overrides.extra,
|
||||
systemPrompt: overrides.systemPrompt,
|
||||
mcpServers: overrides.mcpServers,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function isStoredAgentProviderAvailable(
|
||||
|
||||
Reference in New Issue
Block a user