From 4d369a3400dc9b737fbe8daa63f09d909ce7beb8 Mon Sep 17 00:00:00 2001 From: Robin Waslander Date: Mon, 30 Mar 2026 16:48:10 +0200 Subject: [PATCH] harden session-status tool visibility guard for all callers --- .../openclaw-tools.session-status.test.ts | 41 +++++++++++++++++++ src/agents/tools/session-status-tool.ts | 23 ++++++----- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/agents/openclaw-tools.session-status.test.ts b/src/agents/openclaw-tools.session-status.test.ts index e652fcd3219..e41ae65a5c3 100644 --- a/src/agents/openclaw-tools.session-status.test.ts +++ b/src/agents/openclaw-tools.session-status.test.ts @@ -731,6 +731,47 @@ describe("session_status tool", () => { expect(updateSessionStoreMock).not.toHaveBeenCalled(); }); + it("blocks unsandboxed same-agent bare main session_status outside self visibility", async () => { + resetSessionStore({ + "agent:main:main": { + sessionId: "s-parent", + updatedAt: 10, + providerOverride: "anthropic", + modelOverride: "claude-sonnet-4-6", + }, + "agent:main:subagent:child": { + sessionId: "s-child", + updatedAt: 20, + }, + }); + mockConfig = { + session: { mainKey: "main", scope: "per-sender" }, + tools: { + sessions: { visibility: "self" }, + agentToAgent: { enabled: true, allow: ["*"] }, + }, + agents: { + defaults: { + model: { primary: "openai/gpt-5.4" }, + models: {}, + }, + }, + }; + + const tool = getSessionStatusTool("agent:main:subagent:child"); + + await expect( + tool.execute("call-self-visibility-bare-main", { + sessionKey: "main", + model: "default", + }), + ).rejects.toThrow( + "Session status visibility is restricted to the current session (tools.sessions.visibility=self).", + ); + + expect(updateSessionStoreMock).not.toHaveBeenCalled(); + }); + it("blocks unsandboxed same-agent session_status outside tree visibility before mutation", async () => { resetSessionStore({ "agent:main:main": { diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts index 732ad21b6a1..d5338de4e3e 100644 --- a/src/agents/tools/session-status-tool.ts +++ b/src/agents/tools/session-status-tool.ts @@ -233,7 +233,7 @@ export function createSessionStatusTool(opts?: { const requesterAgentId = resolveAgentIdFromSessionKey( opts?.agentSessionKey ?? effectiveRequesterKey, ); - const visibilityRequesterKey = effectiveRequesterKey.trim(); + const visibilityRequesterKey = (opts?.agentSessionKey ?? effectiveRequesterKey).trim(); const usesLegacyMainAlias = alias === mainKey; const isLegacyMainVisibilityKey = (sessionKey: string) => { const trimmed = sessionKey.trim(); @@ -282,7 +282,8 @@ export function createSessionStatusTool(opts?: { const requestedKeyParam = readStringParam(params, "sessionKey"); let requestedKeyRaw = requestedKeyParam ?? opts?.agentSessionKey; - let resolvedTargetViaSessionId = false; + const requestedKeyInput = requestedKeyRaw?.trim() ?? ""; + let resolvedViaSessionId = false; if (!requestedKeyRaw?.trim()) { throw new Error("sessionKey required"); } @@ -357,7 +358,7 @@ export function createSessionStatusTool(opts?: { } // If resolution points at another agent, enforce A2A policy before switching stores. ensureAgentAccess(resolveAgentIdFromSessionKey(visibleSession.key)); - resolvedTargetViaSessionId = true; + resolvedViaSessionId = true; requestedKeyRaw = visibleSession.key; agentId = resolveAgentIdFromSessionKey(visibleSession.key); storePath = resolveStorePath(cfg.session?.store, { agentId }); @@ -395,13 +396,15 @@ export function createSessionStatusTool(opts?: { throw new Error(`Unknown ${kind}: ${requestedKeyRaw}`); } - if (resolvedTargetViaSessionId || (opts?.sandboxed === true && !isExplicitAgentKey)) { - const access = visibilityGuard.check( - normalizeVisibilityTargetSessionKey(resolved.key, agentId), - ); - if (!access.allowed) { - throw new Error(access.error); - } + // Preserve caller-scoped raw-key/current lookups as "self" for visibility checks. + const visibilityTargetKey = + !resolvedViaSessionId && + (requestedKeyInput === "current" || resolved.key === requestedKeyInput) + ? visibilityRequesterKey + : normalizeVisibilityTargetSessionKey(resolved.key, agentId); + const access = visibilityGuard.check(visibilityTargetKey); + if (!access.allowed) { + throw new Error(access.error); } const configured = resolveDefaultModelForAgent({ cfg, agentId });