From abe204f6e018c76f1265a3314d4571340c06daa2 Mon Sep 17 00:00:00 2001 From: Alex Knight Date: Mon, 4 May 2026 09:43:09 +1000 Subject: [PATCH] fix: preserve session visibility semantics for runSessionKey (#76708) - Track isSemanticCurrentRequest before key rewrite - Skip early explicit-agent-key visibility check for current requests - Add isSemanticCurrentRequest to shouldTreatVisibilityTargetAsSelf - Fix test to use default/tree visibility instead of 'all' - Add negative guard test for explicit cross-session key rejection --- .../openclaw-tools.session-status.test.ts | 38 +++++++++++++++---- src/agents/tools/session-status-tool.ts | 21 ++++++---- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/agents/openclaw-tools.session-status.test.ts b/src/agents/openclaw-tools.session-status.test.ts index 9396354f3be..5c90b5d6c35 100644 --- a/src/agents/openclaw-tools.session-status.test.ts +++ b/src/agents/openclaw-tools.session-status.test.ts @@ -492,7 +492,7 @@ describe("session_status tool", () => { expect(details.sessionKey).toBe("main"); }); - it("resolves sessionKey=current to runSessionKey when sandbox key differs from live session (#76708)", async () => { + it("resolves sessionKey=current to runSessionKey under default tree visibility (#76708)", async () => { resetSessionStore({ "agent:main:telegram:default:direct:1234": { sessionId: "s-tg-direct", @@ -506,15 +506,13 @@ describe("session_status tool", () => { }, }); - // The tool is constructed with the Telegram sandbox key as agentSessionKey - // but the actual live run session key as runSessionKey. + // Default visibility is "tree". The tool is constructed with the Telegram + // sandbox key as agentSessionKey but the live run session key as runSessionKey. + // semantic-current must be treated as self for visibility purposes. const tool = createSessionStatusTool({ agentSessionKey: "agent:main:telegram:default:direct:1234", runSessionKey: "agent:main:main", - config: { - ...mockConfig, - tools: { ...mockConfig.tools, sessions: { visibility: "all" } }, - } as never, + config: mockConfig as never, }); const result = await tool.execute("call-current-run-session", { sessionKey: "current" }); @@ -523,6 +521,32 @@ describe("session_status tool", () => { expect(details.sessionKey).toBe("agent:main:main"); }); + it("rejects explicit cross-session key under tree visibility even when it equals runSessionKey (#76708)", async () => { + resetSessionStore({ + "agent:main:telegram:default:direct:1234": { + sessionId: "s-tg-direct", + updatedAt: 5, + status: "done", + }, + "agent:main:main": { + sessionId: "s-main", + updatedAt: 10, + status: "running", + }, + }); + + // Same setup but with an explicit key — should NOT bypass visibility. + const tool = createSessionStatusTool({ + agentSessionKey: "agent:main:telegram:default:direct:1234", + runSessionKey: "agent:main:main", + config: mockConfig as never, + }); + + await expect( + tool.execute("call-explicit-key", { sessionKey: "agent:main:main" }), + ).rejects.toThrow(/visibility is restricted/); + }); + it("treats the TUI client label as the current requester session", async () => { resetSessionStore({ "agent:main:main": { diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts index 22bdf8f7bf4..56e07e43e21 100644 --- a/src/agents/tools/session-status-tool.ts +++ b/src/agents/tools/session-status-tool.ts @@ -353,9 +353,18 @@ export function createSessionStatusTool(opts?: { const requestedKeyParam = readStringParam(params, "sessionKey"); let requestedKeyRaw = requestedKeyParam ?? opts?.agentSessionKey; - // When sessionKey is literally "current" and a runSessionKey is provided, - // resolve directly to the live run session instead of falling through to - // stale sandbox/policy key resolution (#76708). + // Track whether this is a semantic-current request (literal "current" or a + // current-client alias) BEFORE any rewrite, so visibility treats it as self. + const isSemanticCurrentRequest = + requestedKeyRaw === "current" || + Boolean( + resolveCurrentSessionClientAlias({ + key: requestedKeyRaw ?? "", + requesterInternalKey: effectiveRequesterKey, + }), + ); + + // Resolve "current" to the live run session key for lookup purposes (#76708). if (requestedKeyRaw === "current" && opts?.runSessionKey) { requestedKeyRaw = opts.runSessionKey; } @@ -365,9 +374,6 @@ export function createSessionStatusTool(opts?: { requesterInternalKey: effectiveRequesterKey, }); if (currentSessionAlias) { - // When a runSessionKey is provided (e.g. the live run session key), prefer it - // over the sandbox/policy key so "current" resolves to the active run session - // instead of a stale sandbox key (e.g. a Telegram direct peer key). requestedKeyRaw = opts?.runSessionKey ?? currentSessionAlias; } const requestedKeyInput = requestedKeyRaw?.trim() ?? ""; @@ -391,7 +397,7 @@ export function createSessionStatusTool(opts?: { } }; - if (requestedKeyRaw.startsWith("agent:")) { + if (requestedKeyRaw.startsWith("agent:") && !isSemanticCurrentRequest) { const requestedAgentId = resolveAgentIdFromSessionKey(requestedKeyRaw); ensureAgentAccess(requestedAgentId); const access = visibilityGuard.check( @@ -518,6 +524,7 @@ export function createSessionStatusTool(opts?: { // Preserve caller-scoped raw-key/current lookups as "self" for visibility checks. const shouldTreatVisibilityTargetAsSelf = + isSemanticCurrentRequest || resolvedViaImplicitCurrentFallback || (!resolvedViaSessionId && (requestedKeyInput === "current" || resolved.key === requestedKeyInput));