fix(agents): enforce session_status guard after sessionId resolution (#55105)

* fix(agents): enforce visibility guard after sessionId resolution in session_status

When a sessionId (rather than an explicit agent key) is passed to the
session_status tool, the sessionId resolution block rewrites
requestedKeyRaw to an explicit "agent:..." key.  The subsequent
visibility guard check at line 375 tested
`!requestedKeyRaw.startsWith("agent:")`, which was now always false
after resolution — skipping the visibility check entirely.

This meant a sandboxed agent could bypass visibility restrictions by
providing a sessionId instead of an explicit session key.

Fix: use the original `isExplicitAgentKey` flag (captured before
resolution) instead of re-checking the dynamic requestedKeyRaw.
This ensures the visibility guard runs for sessionId inputs while
still skipping the redundant check for inputs that were already
validated at the earlier explicit-key check (lines 281-286).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: cover session status sessionId guard

* test: align parent sessionId guard coverage

---------

Co-authored-by: Kevin Sheng <shenghuikevin@github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jacob Tomlinson
2026-03-26 04:34:22 -07:00
committed by GitHub
parent 5e8cb22176
commit d9810811b6
2 changed files with 52 additions and 1 deletions

View File

@@ -587,6 +587,57 @@ describe("session_status tool", () => {
]);
});
it("blocks sandboxed child session_status parent sessionId access outside its tree", async () => {
resetSessionStore({
"agent:main:subagent:child": {
sessionId: "s-child",
updatedAt: 20,
},
"agent:main:main": {
sessionId: "s-parent",
updatedAt: 10,
},
});
installSandboxedSessionStatusConfig();
mockSpawnedSessionList(() => []);
const tool = getSessionStatusTool("agent:main:subagent:child", {
sandboxed: true,
});
await expect(
tool.execute("call7-parent-session-id", {
sessionKey: "s-parent",
}),
).rejects.toThrow("Session status visibility is restricted to the current session tree");
expect(loadSessionStoreMock).toHaveBeenCalledTimes(1);
expect(loadSessionStoreMock).toHaveBeenCalledWith("/tmp/main/sessions.json");
expect(updateSessionStoreMock).not.toHaveBeenCalled();
expect(callGatewayMock).toHaveBeenCalledTimes(3);
expect(callGatewayMock.mock.calls).toContainEqual([
{
method: "sessions.resolve",
params: {
sessionId: "s-parent",
spawnedBy: "agent:main:subagent:child",
includeGlobal: false,
includeUnknown: false,
},
},
]);
expect(callGatewayMock.mock.calls).toContainEqual([
{
method: "sessions.list",
params: {
includeGlobal: false,
includeUnknown: false,
spawnedBy: "agent:main:subagent:child",
},
},
]);
});
it("keeps legacy main requester keys for sandboxed session tree checks", async () => {
resetSessionStore({
"agent:main:main": {

View File

@@ -367,7 +367,7 @@ export function createSessionStatusTool(opts?: {
throw new Error(`Unknown ${kind}: ${requestedKeyRaw}`);
}
if (visibilityGuard && !requestedKeyRaw.startsWith("agent:")) {
if (visibilityGuard && !isExplicitAgentKey) {
const access = visibilityGuard.check(
normalizeVisibilityTargetSessionKey(resolved.key, agentId),
);