Skip to content

Commit 2d4b16a

Browse files
fix(cli): gate prompt loopback tools on active runtime
1 parent 3384a58 commit 2d4b16a

2 files changed

Lines changed: 94 additions & 13 deletions

File tree

src/agents/cli-runner/prepare.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,85 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
10831083
}
10841084
});
10851085

1086+
it("does not advertise loopback prompt tools when the runtime is unavailable", async () => {
1087+
const { dir, sessionFile } = createSessionFile();
1088+
try {
1089+
registerMemoryPromptSection(({ availableTools }) =>
1090+
availableTools.has("memory_search")
1091+
? ["## Memory Recall", `tools=${[...availableTools].toSorted().join(",")}`, ""]
1092+
: [],
1093+
);
1094+
const getActiveMcpLoopbackRuntime = vi.fn(() => undefined);
1095+
const ensureMcpLoopbackServer = vi.fn(async () => {
1096+
throw new Error("loopback unavailable");
1097+
});
1098+
const createMcpLoopbackServerConfig = vi.fn(createTestMcpLoopbackServerConfig);
1099+
const resolveMcpLoopbackScopedTools = vi.fn(() => ({
1100+
agentId: "main",
1101+
tools: [
1102+
{
1103+
name: "memory_search",
1104+
label: "Memory Search",
1105+
description: "Search memory",
1106+
parameters: { type: "object", properties: {} },
1107+
execute: vi.fn(),
1108+
},
1109+
],
1110+
}));
1111+
setCliRunnerPrepareTestDeps({
1112+
getActiveMcpLoopbackRuntime,
1113+
ensureMcpLoopbackServer,
1114+
createMcpLoopbackServerConfig,
1115+
resolveMcpLoopbackScopedTools,
1116+
});
1117+
cliBackendsTesting.setDepsForTest({
1118+
resolvePluginSetupCliBackend: () => undefined,
1119+
resolveRuntimeCliBackends: () => [
1120+
{
1121+
id: "native-cli",
1122+
pluginId: "native-plugin",
1123+
bundleMcp: true,
1124+
bundleMcpMode: "claude-config-file",
1125+
config: {
1126+
command: "native-cli",
1127+
args: ["--print"],
1128+
systemPromptArg: "--system-prompt",
1129+
systemPromptWhen: "first",
1130+
output: "text",
1131+
input: "arg",
1132+
sessionMode: "existing",
1133+
},
1134+
},
1135+
],
1136+
});
1137+
1138+
const context = await prepareCliRunContext({
1139+
sessionId: "session-test",
1140+
sessionKey: "agent:main:test",
1141+
sessionFile,
1142+
workspaceDir: dir,
1143+
prompt: "latest ask",
1144+
provider: "native-cli",
1145+
model: "test-model",
1146+
timeoutMs: 1_000,
1147+
runId: "run-test-loopback-prompt-tools-fallback",
1148+
config: createCliBackendConfig({ bundleMcp: true, systemPromptOverride: null }),
1149+
});
1150+
1151+
expect(ensureMcpLoopbackServer).toHaveBeenCalledTimes(1);
1152+
expect(getActiveMcpLoopbackRuntime).toHaveBeenCalledTimes(2);
1153+
expect(createMcpLoopbackServerConfig).not.toHaveBeenCalled();
1154+
expect(resolveMcpLoopbackScopedTools).not.toHaveBeenCalled();
1155+
expect(context.systemPrompt).not.toContain("## Memory Recall");
1156+
expect(context.systemPrompt).not.toContain("memory_search");
1157+
expect(context.systemPromptReport.tools.entries).toEqual([]);
1158+
expect(context.promptToolNamesHash).toBeUndefined();
1159+
expect(context.preparedBackend.env).toBeUndefined();
1160+
} finally {
1161+
fs.rmSync(dir, { recursive: true, force: true });
1162+
}
1163+
});
1164+
10861165
it("passes current turn kind into bundle MCP loopback env", async () => {
10871166
const { dir, sessionFile } = createSessionFile();
10881167
try {

src/agents/cli-runner/prepare.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -284,19 +284,21 @@ export async function prepareCliRunContext(
284284
...(preparedBackendEnv ? { env: preparedBackendEnv } : {}),
285285
...(preparedBackendCleanup ? { cleanup: preparedBackendCleanup } : {}),
286286
};
287-
const promptTools = bundleMcpEnabled
288-
? prepareDeps.resolveMcpLoopbackScopedTools({
289-
cfg: params.config ?? getRuntimeConfig(),
290-
sessionKey: params.sessionKey ?? "",
291-
messageProvider: params.messageChannel ?? params.messageProvider,
292-
accountId: params.agentAccountId,
293-
inboundEventKind: params.currentInboundEventKind,
294-
senderIsOwner: params.senderIsOwner,
295-
}).tools
296-
: [];
297-
const promptToolNamesHash = bundleMcpEnabled
298-
? hashCliSessionText(JSON.stringify(promptTools.map((tool) => tool.name).toSorted()))
299-
: undefined;
287+
const promptTools =
288+
bundleMcpEnabled && mcpLoopbackRuntime
289+
? prepareDeps.resolveMcpLoopbackScopedTools({
290+
cfg: params.config ?? getRuntimeConfig(),
291+
sessionKey: params.sessionKey ?? "",
292+
messageProvider: params.messageChannel ?? params.messageProvider,
293+
accountId: params.agentAccountId,
294+
inboundEventKind: params.currentInboundEventKind,
295+
senderIsOwner: params.senderIsOwner,
296+
}).tools
297+
: [];
298+
const promptToolNamesHash =
299+
bundleMcpEnabled && mcpLoopbackRuntime
300+
? hashCliSessionText(JSON.stringify(promptTools.map((tool) => tool.name).toSorted()))
301+
: undefined;
300302
// Pre-flight: if a saved Claude CLI sessionId points at a transcript that no
301303
// longer exists on disk (e.g. update.run aborted mid-swap, Claude CLI was
302304
// reinstalled, or the projects tree was manually pruned), `claude --resume`

0 commit comments

Comments
 (0)