mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-04 12:53:34 +00:00
fix(agents): make cli binding tool-scope hash session-stable
This commit is contained in:
@@ -2117,6 +2117,131 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("reuses CLI session bindings across owner sender flips with stable prompt tool scope", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
try {
|
||||
const getActiveMcpLoopbackRuntime = vi.fn(() => ({
|
||||
port: 31783,
|
||||
ownerToken: "loopback-owner-token",
|
||||
nonOwnerToken: "loopback-non-owner-token",
|
||||
}));
|
||||
const resolveMcpLoopbackScopedTools = vi.fn((scope: { senderIsOwner?: boolean }) => ({
|
||||
agentId: "main",
|
||||
tools: [
|
||||
{
|
||||
name: "message",
|
||||
label: "Message",
|
||||
description: "Send a message",
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: vi.fn(),
|
||||
},
|
||||
...(scope.senderIsOwner === false
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: "gateway",
|
||||
label: "Gateway",
|
||||
description: "Manage the gateway",
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: vi.fn(),
|
||||
},
|
||||
]),
|
||||
],
|
||||
}));
|
||||
setCliRunnerPrepareTestDeps({
|
||||
getActiveMcpLoopbackRuntime,
|
||||
resolveMcpLoopbackScopedTools,
|
||||
});
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolvePluginSetupCliBackend: () => undefined,
|
||||
resolveRuntimeCliBackends: () => [
|
||||
{
|
||||
id: "native-cli",
|
||||
pluginId: "native-plugin",
|
||||
bundleMcp: true,
|
||||
bundleMcpMode: "claude-config-file",
|
||||
config: {
|
||||
command: "native-cli",
|
||||
args: ["--print"],
|
||||
systemPromptArg: "--system-prompt",
|
||||
systemPromptWhen: "first",
|
||||
output: "text",
|
||||
input: "arg",
|
||||
sessionMode: "existing",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const cliSessionBindingFacts = {
|
||||
extraSystemPromptStatic: "group:telegram:group:message_tool_only",
|
||||
sourceReplyDeliveryMode: "message_tool_only" as const,
|
||||
};
|
||||
const first = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:main:telegram:group:chat123",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
prompt: "first ask",
|
||||
provider: "native-cli",
|
||||
model: "test-model",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-test-owner-tool-scope-a",
|
||||
extraSystemPrompt: "volatile owner turn",
|
||||
currentMessageId: "owner-message",
|
||||
senderIsOwner: true,
|
||||
cliSessionBindingFacts,
|
||||
config: createCliBackendConfig({ bundleMcp: true }),
|
||||
});
|
||||
const second = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:main:telegram:group:chat123",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
prompt: "second ask",
|
||||
provider: "native-cli",
|
||||
model: "test-model",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-test-owner-tool-scope-b",
|
||||
extraSystemPrompt: "volatile non-owner turn",
|
||||
currentMessageId: "non-owner-message",
|
||||
senderIsOwner: false,
|
||||
cliSessionBindingFacts,
|
||||
cliSessionBinding: {
|
||||
sessionId: "cli-session",
|
||||
extraSystemPromptHash: first.extraSystemPromptHash,
|
||||
messageToolPolicyHash: first.messageToolPolicyHash,
|
||||
promptToolNamesHash: first.promptToolNamesHash,
|
||||
cwdHash: hashCliSessionText(dir),
|
||||
mcpConfigHash: first.preparedBackend.mcpConfigHash,
|
||||
mcpResumeHash: first.preparedBackend.mcpResumeHash,
|
||||
},
|
||||
config: createCliBackendConfig({ bundleMcp: true }),
|
||||
});
|
||||
|
||||
expect(resolveMcpLoopbackScopedTools).toHaveBeenCalledTimes(2);
|
||||
expect(resolveMcpLoopbackScopedTools).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
senderIsOwner: undefined,
|
||||
currentMessageId: undefined,
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
}),
|
||||
);
|
||||
expect(resolveMcpLoopbackScopedTools).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
senderIsOwner: undefined,
|
||||
currentMessageId: undefined,
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
}),
|
||||
);
|
||||
expect(second.promptToolNamesHash).toBe(first.promptToolNamesHash);
|
||||
expect(second.reusableCliSession).toEqual({ sessionId: "cli-session" });
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("prepares raw-tail history for safe invalidations only when the backend opts in", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
appendTranscriptEntry(sessionFile, {
|
||||
|
||||
@@ -601,14 +601,16 @@ export async function prepareCliRunContext(
|
||||
sessionKey: params.sessionKey ?? "",
|
||||
messageProvider: params.messageChannel ?? params.messageProvider,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
currentMessageId: params.currentMessageId,
|
||||
currentInboundAudio: params.currentInboundAudio,
|
||||
// CLI binding hashes must use session-stable prompt facts. Per-sender
|
||||
// and per-message scope stays in the runtime MCP env/list-call path.
|
||||
currentThreadTs: undefined,
|
||||
currentMessageId: undefined,
|
||||
currentInboundAudio: undefined,
|
||||
accountId: params.agentAccountId,
|
||||
inboundEventKind: params.currentInboundEventKind,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
requireExplicitMessageTarget,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
inboundEventKind: undefined,
|
||||
sourceReplyDeliveryMode: bindingSourceReplyDeliveryMode,
|
||||
requireExplicitMessageTarget: bindingRequireExplicitMessageTarget,
|
||||
senderIsOwner: undefined,
|
||||
}).tools
|
||||
: [];
|
||||
const promptToolNamesHash =
|
||||
|
||||
@@ -42,12 +42,12 @@ describe("resolveGatewayScopedTools excludeToolNames", () => {
|
||||
hoisted.createOpenClawToolsMock.mockClear();
|
||||
});
|
||||
|
||||
function readCreateToolsArgs(): {
|
||||
function readCreateToolsArgs(index = 0): {
|
||||
cronCreatorToolAllowlist?: Array<string | { name: string; pluginId?: string }>;
|
||||
inheritedToolDenylist?: string[];
|
||||
pluginToolDenylist?: string[];
|
||||
} {
|
||||
const args = hoisted.createOpenClawToolsMock.mock.calls[0]?.[0];
|
||||
const args = hoisted.createOpenClawToolsMock.mock.calls[index]?.[0];
|
||||
if (!args || typeof args !== "object") {
|
||||
throw new Error("expected createOpenClawTools args");
|
||||
}
|
||||
@@ -77,8 +77,16 @@ describe("resolveGatewayScopedTools excludeToolNames", () => {
|
||||
expect(args.inheritedToolDenylist).toEqual([]);
|
||||
});
|
||||
|
||||
it("filters owner-only core tools from non-owner loopback callers", () => {
|
||||
const result = resolveGatewayScopedTools({
|
||||
it("keeps owner-only core tools visible only for owner loopback callers", () => {
|
||||
const ownerResult = resolveGatewayScopedTools({
|
||||
cfg: {
|
||||
gateway: { tools: { allow: ["gateway"] } },
|
||||
} as OpenClawConfig,
|
||||
sessionKey: "agent:main:direct:test",
|
||||
surface: "loopback",
|
||||
senderIsOwner: true,
|
||||
});
|
||||
const nonOwnerResult = resolveGatewayScopedTools({
|
||||
cfg: {
|
||||
gateway: { tools: { allow: ["gateway"] } },
|
||||
} as OpenClawConfig,
|
||||
@@ -87,8 +95,15 @@ describe("resolveGatewayScopedTools excludeToolNames", () => {
|
||||
senderIsOwner: false,
|
||||
});
|
||||
|
||||
expect(result.tools.map((tool) => tool.name)).toEqual(["read", "sessions_spawn"]);
|
||||
const args = readCreateToolsArgs();
|
||||
expect(ownerResult.tools.map((tool) => tool.name)).toEqual([
|
||||
"read",
|
||||
"sessions_spawn",
|
||||
"cron",
|
||||
"gateway",
|
||||
"nodes",
|
||||
]);
|
||||
expect(nonOwnerResult.tools.map((tool) => tool.name)).toEqual(["read", "sessions_spawn"]);
|
||||
const args = readCreateToolsArgs(1);
|
||||
expect(args.pluginToolDenylist).toEqual(["cron", "gateway", "nodes"]);
|
||||
expect(args.inheritedToolDenylist).toEqual(["cron", "gateway", "nodes"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user