Skip to content

Commit 48d9966

Browse files
clawsweeper[bot]TurboTheTurtleTakhoffman
authored
fix(cli): include loopback tools in cli prompts (#83828)
Summary: - The PR feeds loopback-scoped MCP tools into CLI system prompts and reports, persists a prompt tool-name hash for CLI session reuse, adds regression tests, and adds a changelog entry. - Reproducibility: yes. from source inspection: current main builds the CLI prompt and report with `tools: []` ... execute a live CLI turn in this read-only review, but the source path and source PR terminal proof line up. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(cli): gate prompt loopback tools on active runtime - PR branch already contained follow-up commit before automerge: fix(cli): include loopback tools in cli prompts Validation: - ClawSweeper review passed for head d196564. - Required merge gates passed before the squash merge. Prepared head SHA: d196564 Review: #83828 (comment) Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent d160342 commit 48d9966

10 files changed

Lines changed: 253 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Docs: https://docs.openclaw.ai
6363
- Browser: enforce current-tab URL allowlist checks for `/act` evaluate/batch actions and `/highlight` routes while leaving tab-management actions unblocked. (#78523)
6464
- CI: require real-behavior-proof verdict markers to come from the ClawSweeper GitHub App before accepting exact-head proof. (#83692)
6565
- Models: show the effective OpenAI/Codex auth profile in `/models` provider headers instead of falling back to the OpenAI env-key label. (#83697) Thanks @yu-xin-c.
66+
- CLI: include active bundled loopback MCP tools in CLI system prompts and reset provider-side CLI sessions when that prompt-visible tool surface changes. (#83785) Thanks @TurboTheTurtle.
6667
- Browser: keep a profile `cdpPort` when its `cdpUrl` omits a port, while still letting explicitly written URL ports win. (#82166) Thanks @Marvae.
6768
- Agents/image generation: allow distinct `image_generate` prompts to start separate session-backed background tasks while same-prompt retries still return the active task status. (#83614) Thanks @Elarwei001.
6869
- Gateway/WebChat: honor configured `channels.webchat.textChunkLimit` and `chunkMode` overrides when chunking WebChat replies. (#83713)

src/agents/cli-runner.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,9 @@ export async function runPreparedCliAgent(
511511
...(context.extraSystemPromptHash
512512
? { extraSystemPromptHash: context.extraSystemPromptHash }
513513
: {}),
514+
...(context.promptToolNamesHash
515+
? { promptToolNamesHash: context.promptToolNamesHash }
516+
: {}),
514517
...(context.preparedBackend.mcpConfigHash
515518
? { mcpConfigHash: context.preparedBackend.mcpConfigHash }
516519
: {}),

src/agents/cli-runner/claude-live-session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ function buildClaudeLiveFingerprint(params: {
279279
: undefined,
280280
authEpochHash: params.context.authEpoch ? sha256(params.context.authEpoch) : undefined,
281281
extraSystemPromptHash: params.context.extraSystemPromptHash,
282+
promptToolNamesHash: params.context.promptToolNamesHash,
282283
mcpConfigHash: params.context.preparedBackend.mcpConfigHash,
283284
skillsFingerprint,
284285
argv: stableArgv,

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

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "../../context-engine/registry.js";
1212
import type { ContextEngine } from "../../context-engine/types.js";
1313
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
14+
import { clearMemoryPluginState, registerMemoryPromptSection } from "../../plugins/memory-state.js";
1415
import { testing as cliBackendsTesting } from "../cli-backends.js";
1516
import { hashCliSessionText } from "../cli-session.js";
1617
import { buildActiveImageGenerationTaskPromptContextForSession } from "../image-generation-task-status.js";
@@ -197,6 +198,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
197198
getActiveMcpLoopbackRuntime: vi.fn(() => undefined),
198199
ensureMcpLoopbackServer: vi.fn(createTestMcpLoopbackServer),
199200
createMcpLoopbackServerConfig: vi.fn(createTestMcpLoopbackServerConfig),
201+
resolveMcpLoopbackScopedTools: vi.fn(() => ({ agentId: "main", tools: [] })),
200202
resolveOpenClawReferencePaths: vi.fn(async () => ({ docsPath: null, sourcePath: null })),
201203
});
202204
mockGetGlobalHookRunner.mockReturnValue(null);
@@ -213,6 +215,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
213215
mockBuildActiveImageGenerationTaskPromptContextForSession.mockReset();
214216
mockBuildActiveVideoGenerationTaskPromptContextForSession.mockReset();
215217
mockBuildActiveMusicGenerationTaskPromptContextForSession.mockReset();
218+
clearMemoryPluginState();
216219
vi.unstubAllEnvs();
217220
});
218221

@@ -987,6 +990,178 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
987990
}
988991
});
989992

993+
it("uses loopback-scoped tools when building bundled MCP CLI prompts", async () => {
994+
const { dir, sessionFile } = createSessionFile();
995+
try {
996+
registerMemoryPromptSection(({ availableTools }) =>
997+
availableTools.has("memory_search")
998+
? ["## Memory Recall", `tools=${[...availableTools].toSorted().join(",")}`, ""]
999+
: [],
1000+
);
1001+
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
1002+
port: 31783,
1003+
ownerToken: "owner-token",
1004+
nonOwnerToken: "non-owner-token",
1005+
}));
1006+
const ensureMcpLoopbackServer = vi.fn(createTestMcpLoopbackServer);
1007+
const createMcpLoopbackServerConfig = vi.fn(createTestMcpLoopbackServerConfig);
1008+
const resolveMcpLoopbackScopedTools = vi.fn(() => ({
1009+
agentId: "main",
1010+
tools: [
1011+
{
1012+
name: "memory_search",
1013+
label: "Memory Search",
1014+
description: "Search memory",
1015+
parameters: { type: "object", properties: {} },
1016+
execute: vi.fn(),
1017+
},
1018+
],
1019+
}));
1020+
setCliRunnerPrepareTestDeps({
1021+
getActiveMcpLoopbackRuntime,
1022+
ensureMcpLoopbackServer,
1023+
createMcpLoopbackServerConfig,
1024+
resolveMcpLoopbackScopedTools,
1025+
});
1026+
cliBackendsTesting.setDepsForTest({
1027+
resolvePluginSetupCliBackend: () => undefined,
1028+
resolveRuntimeCliBackends: () => [
1029+
{
1030+
id: "native-cli",
1031+
pluginId: "native-plugin",
1032+
bundleMcp: true,
1033+
bundleMcpMode: "claude-config-file",
1034+
config: {
1035+
command: "native-cli",
1036+
args: ["--print"],
1037+
systemPromptArg: "--system-prompt",
1038+
systemPromptWhen: "first",
1039+
output: "text",
1040+
input: "arg",
1041+
sessionMode: "existing",
1042+
},
1043+
},
1044+
],
1045+
});
1046+
1047+
const context = await prepareCliRunContext({
1048+
sessionId: "session-test",
1049+
sessionKey: "agent:main:test",
1050+
sessionFile,
1051+
workspaceDir: dir,
1052+
prompt: "latest ask",
1053+
provider: "native-cli",
1054+
model: "test-model",
1055+
timeoutMs: 1_000,
1056+
runId: "run-test-loopback-prompt-tools",
1057+
config: createCliBackendConfig({ bundleMcp: true, systemPromptOverride: null }),
1058+
cliSessionBinding: {
1059+
sessionId: "cli-session",
1060+
promptToolNamesHash: "old-tool-surface",
1061+
},
1062+
});
1063+
1064+
expect(resolveMcpLoopbackScopedTools).toHaveBeenCalledWith({
1065+
cfg: expect.any(Object),
1066+
sessionKey: "agent:main:test",
1067+
messageProvider: undefined,
1068+
accountId: undefined,
1069+
inboundEventKind: undefined,
1070+
senderIsOwner: undefined,
1071+
});
1072+
expect(context.systemPrompt).toContain("## Memory Recall");
1073+
expect(context.systemPrompt).toContain("tools=memory_search");
1074+
expect(context.systemPromptReport.tools.entries.map((entry) => entry.name)).toEqual([
1075+
"memory_search",
1076+
]);
1077+
expect(context.promptToolNamesHash).toBe(
1078+
hashCliSessionText(JSON.stringify(["memory_search"])),
1079+
);
1080+
expect(context.reusableCliSession).toEqual({ invalidatedReason: "system-prompt" });
1081+
} finally {
1082+
fs.rmSync(dir, { recursive: true, force: true });
1083+
}
1084+
});
1085+
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+
9901165
it("passes current turn kind into bundle MCP loopback env", async () => {
9911166
const { dir, sessionFile } = createSessionFile();
9921167
try {

src/agents/cli-runner/prepare.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
createMcpLoopbackServerConfig,
77
getActiveMcpLoopbackRuntime,
88
} from "../../gateway/mcp-http.loopback-runtime.js";
9+
import { resolveMcpLoopbackScopedTools } from "../../gateway/mcp-http.runtime.js";
910
import { isClaudeCliProvider } from "../../plugin-sdk/anthropic-cli.js";
1011
import type {
1112
CliBackendAuthEpochMode,
@@ -67,6 +68,7 @@ const prepareDeps = {
6768
getActiveMcpLoopbackRuntime,
6869
ensureMcpLoopbackServer,
6970
createMcpLoopbackServerConfig,
71+
resolveMcpLoopbackScopedTools,
7072
resolveOpenClawReferencePaths: async (
7173
params: Parameters<typeof import("../docs-path.js").resolveOpenClawReferencePaths>[0],
7274
) => (await import("../docs-path.js")).resolveOpenClawReferencePaths(params),
@@ -282,6 +284,21 @@ export async function prepareCliRunContext(
282284
...(preparedBackendEnv ? { env: preparedBackendEnv } : {}),
283285
...(preparedBackendCleanup ? { cleanup: preparedBackendCleanup } : {}),
284286
};
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;
285302
// Pre-flight: if a saved Claude CLI sessionId points at a transcript that no
286303
// longer exists on disk (e.g. update.run aborted mid-swap, Claude CLI was
287304
// reinstalled, or the projects tree was manually pruned), `claude --resume`
@@ -306,6 +323,7 @@ export async function prepareCliRunContext(
306323
authEpoch,
307324
authEpochVersion: CLI_AUTH_EPOCH_VERSION,
308325
extraSystemPromptHash,
326+
promptToolNamesHash,
309327
mcpConfigHash: preparedBackendFinal.mcpConfigHash,
310328
mcpResumeHash: preparedBackendFinal.mcpResumeHash,
311329
})
@@ -362,7 +380,7 @@ export async function prepareCliRunContext(
362380
docsPath: openClawReferences.docsPath ?? undefined,
363381
sourcePath: openClawReferences.sourcePath ?? undefined,
364382
skillsPrompt,
365-
tools: [],
383+
tools: promptTools,
366384
contextFiles,
367385
modelDisplay,
368386
agentId: sessionAgentId,
@@ -471,7 +489,7 @@ export async function prepareCliRunContext(
471489
bootstrapFiles,
472490
injectedFiles: contextFiles,
473491
skillsPrompt,
474-
tools: [],
492+
tools: promptTools,
475493
currentTurn: {
476494
...(params.currentInboundEventKind ? { kind: params.currentInboundEventKind } : {}),
477495
promptChars: preparedPrompt.length,
@@ -530,6 +548,7 @@ export async function prepareCliRunContext(
530548
authEpoch,
531549
authEpochVersion: CLI_AUTH_EPOCH_VERSION,
532550
extraSystemPromptHash,
551+
promptToolNamesHash,
533552
};
534553
} catch (err) {
535554
try {

src/agents/cli-runner/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,5 @@ export type PreparedCliRunContext = {
132132
authEpoch?: string;
133133
authEpochVersion: number;
134134
extraSystemPromptHash?: string;
135+
promptToolNamesHash?: string;
135136
};

src/agents/cli-session.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe("cli-session helpers", () => {
2323
authEpoch: "auth-epoch",
2424
authEpochVersion: 2,
2525
extraSystemPromptHash: "prompt-hash",
26+
promptToolNamesHash: "prompt-tools-hash",
2627
mcpConfigHash: "mcp-hash",
2728
mcpResumeHash: "mcp-resume-hash",
2829
});
@@ -36,6 +37,7 @@ describe("cli-session helpers", () => {
3637
authEpoch: "auth-epoch",
3738
authEpochVersion: 2,
3839
extraSystemPromptHash: "prompt-hash",
40+
promptToolNamesHash: "prompt-tools-hash",
3941
mcpConfigHash: "mcp-hash",
4042
mcpResumeHash: "mcp-resume-hash",
4143
});
@@ -154,6 +156,17 @@ describe("cli-session helpers", () => {
154156
mcpConfigHash: "mcp-a",
155157
}),
156158
).toEqual({ invalidatedReason: "system-prompt" });
159+
expect(
160+
resolveCliSessionReuse({
161+
binding,
162+
authProfileId: "anthropic:work",
163+
authEpoch: "auth-epoch-a",
164+
authEpochVersion: 2,
165+
extraSystemPromptHash: "prompt-a",
166+
promptToolNamesHash: "prompt-tools-b",
167+
mcpConfigHash: "mcp-a",
168+
}),
169+
).toEqual({ invalidatedReason: "system-prompt" });
157170
expect(
158171
resolveCliSessionReuse({
159172
binding,

src/agents/cli-session.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function getCliSessionBinding(
3131
authEpoch: normalizeOptionalString(fromBindings?.authEpoch),
3232
authEpochVersion: fromBindings?.authEpochVersion,
3333
extraSystemPromptHash: normalizeOptionalString(fromBindings?.extraSystemPromptHash),
34+
promptToolNamesHash: normalizeOptionalString(fromBindings?.promptToolNamesHash),
3435
mcpConfigHash: normalizeOptionalString(fromBindings?.mcpConfigHash),
3536
mcpResumeHash: normalizeOptionalString(fromBindings?.mcpResumeHash),
3637
};
@@ -87,6 +88,9 @@ export function setCliSessionBinding(
8788
...(normalizeOptionalString(binding.extraSystemPromptHash)
8889
? { extraSystemPromptHash: normalizeOptionalString(binding.extraSystemPromptHash) }
8990
: {}),
91+
...(normalizeOptionalString(binding.promptToolNamesHash)
92+
? { promptToolNamesHash: normalizeOptionalString(binding.promptToolNamesHash) }
93+
: {}),
9094
...(normalizeOptionalString(binding.mcpConfigHash)
9195
? { mcpConfigHash: normalizeOptionalString(binding.mcpConfigHash) }
9296
: {}),
@@ -130,6 +134,7 @@ export function resolveCliSessionReuse(params: {
130134
authEpoch?: string;
131135
authEpochVersion: number;
132136
extraSystemPromptHash?: string;
137+
promptToolNamesHash?: string;
133138
mcpConfigHash?: string;
134139
mcpResumeHash?: string;
135140
}): {
@@ -147,6 +152,7 @@ export function resolveCliSessionReuse(params: {
147152
const currentAuthProfileId = normalizeOptionalString(params.authProfileId);
148153
const currentAuthEpoch = normalizeOptionalString(params.authEpoch);
149154
const currentExtraSystemPromptHash = normalizeOptionalString(params.extraSystemPromptHash);
155+
const currentPromptToolNamesHash = normalizeOptionalString(params.promptToolNamesHash);
150156
const currentMcpConfigHash = normalizeOptionalString(params.mcpConfigHash);
151157
const currentMcpResumeHash = normalizeOptionalString(params.mcpResumeHash);
152158
const storedAuthProfileId = normalizeOptionalString(binding?.authProfileId);
@@ -171,6 +177,10 @@ export function resolveCliSessionReuse(params: {
171177
if (storedExtraSystemPromptHash !== currentExtraSystemPromptHash) {
172178
return { invalidatedReason: "system-prompt" };
173179
}
180+
const storedPromptToolNamesHash = normalizeOptionalString(binding?.promptToolNamesHash);
181+
if (storedPromptToolNamesHash !== currentPromptToolNamesHash) {
182+
return { invalidatedReason: "system-prompt" };
183+
}
174184
const storedMcpResumeHash = normalizeOptionalString(binding?.mcpResumeHash);
175185
if (storedMcpResumeHash && currentMcpResumeHash) {
176186
if (storedMcpResumeHash !== currentMcpResumeHash) {

src/config/sessions/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export type CliSessionBinding = {
7979
authEpoch?: string;
8080
authEpochVersion?: number;
8181
extraSystemPromptHash?: string;
82+
promptToolNamesHash?: string;
8283
mcpConfigHash?: string;
8384
mcpResumeHash?: string;
8485
};

0 commit comments

Comments
 (0)