harden session-status tool visibility guard for all callers

This commit is contained in:
Robin Waslander 2026-03-30 16:48:10 +02:00
parent 5cca380840
commit 4d369a3400
No known key found for this signature in database
GPG Key ID: 712657D6EA17B7E5
2 changed files with 54 additions and 10 deletions

View File

@ -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": {

View File

@ -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 });