import type { OpenClawConfig } from "../config/types.openclaw.js"; import { callGateway as defaultCallGateway } from "../gateway/call.js"; import { resolveAgentIdFromSessionKey } from "../routing/session-key.js"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "../shared/string-coerce.js"; type GatewayCaller = typeof defaultCallGateway; let callGatewayForListSpawned: GatewayCaller = defaultCallGateway; /** Test hook: must stay aligned with `sessions-resolution` `__testing.setDepsForTest`. */ export const sessionVisibilityGatewayTesting = { setCallGatewayForListSpawned(overrides?: GatewayCaller) { callGatewayForListSpawned = overrides ?? defaultCallGateway; }, }; export type SessionToolsVisibility = "self" | "tree" | "agent" | "all"; export type AgentToAgentPolicy = { enabled: boolean; matchesAllow: (agentId: string) => boolean; isAllowed: (requesterAgentId: string, targetAgentId: string) => boolean; }; export type SessionAccessAction = "history" | "send" | "list" | "status"; export type SessionAccessResult = | { allowed: true } | { allowed: false; error: string; status: "forbidden" }; export async function listSpawnedSessionKeys(params: { requesterSessionKey: string; limit?: number; }): Promise> { const limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? Math.max(1, Math.floor(params.limit)) : undefined; try { const list = await callGatewayForListSpawned<{ sessions: Array<{ key?: unknown }> }>({ method: "sessions.list", params: { includeGlobal: false, includeUnknown: false, ...(limit !== undefined ? { limit } : {}), spawnedBy: params.requesterSessionKey, }, }); const sessions = Array.isArray(list?.sessions) ? list.sessions : []; const keys = sessions.map((entry) => normalizeOptionalString(entry?.key) ?? "").filter(Boolean); return new Set(keys); } catch { return new Set(); } } export function resolveSessionToolsVisibility(cfg: OpenClawConfig): SessionToolsVisibility { const raw = (cfg.tools as { sessions?: { visibility?: unknown } } | undefined)?.sessions ?.visibility; const value = normalizeLowercaseStringOrEmpty(raw); if (value === "self" || value === "tree" || value === "agent" || value === "all") { return value; } return "tree"; } export function resolveEffectiveSessionToolsVisibility(params: { cfg: OpenClawConfig; sandboxed: boolean; }): SessionToolsVisibility { const visibility = resolveSessionToolsVisibility(params.cfg); if (!params.sandboxed) { return visibility; } const sandboxClamp = params.cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned"; if (sandboxClamp === "spawned" && visibility !== "tree") { return "tree"; } return visibility; } export function resolveSandboxSessionToolsVisibility(cfg: OpenClawConfig): "spawned" | "all" { return cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned"; } export function createAgentToAgentPolicy(cfg: OpenClawConfig): AgentToAgentPolicy { const routingA2A = cfg.tools?.agentToAgent; const enabled = routingA2A?.enabled === true; const allowPatterns = Array.isArray(routingA2A?.allow) ? routingA2A.allow : []; const matchesAllow = (agentId: string) => { if (allowPatterns.length === 0) { return true; } return allowPatterns.some((pattern) => { const raw = normalizeOptionalString(typeof pattern === "string" ? pattern : String(pattern ?? "")) ?? ""; if (!raw) { return false; } if (raw === "*") { return true; } if (!raw.includes("*")) { return raw === agentId; } const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`, "i"); return re.test(agentId); }); }; const isAllowed = (requesterAgentId: string, targetAgentId: string) => { if (requesterAgentId === targetAgentId) { return true; } if (!enabled) { return false; } return matchesAllow(requesterAgentId) && matchesAllow(targetAgentId); }; return { enabled, matchesAllow, isAllowed }; } function actionPrefix(action: SessionAccessAction): string { if (action === "history") { return "Session history"; } if (action === "send") { return "Session send"; } if (action === "status") { return "Session status"; } return "Session list"; } function a2aDisabledMessage(action: SessionAccessAction): string { if (action === "history") { return "Agent-to-agent history is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access."; } if (action === "send") { return "Agent-to-agent messaging is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent sends."; } if (action === "status") { return "Agent-to-agent status is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent access."; } return "Agent-to-agent listing is disabled. Set tools.agentToAgent.enabled=true to allow cross-agent visibility."; } function a2aDeniedMessage(action: SessionAccessAction): string { if (action === "history") { return "Agent-to-agent history denied by tools.agentToAgent.allow."; } if (action === "send") { return "Agent-to-agent messaging denied by tools.agentToAgent.allow."; } if (action === "status") { return "Agent-to-agent status denied by tools.agentToAgent.allow."; } return "Agent-to-agent listing denied by tools.agentToAgent.allow."; } function crossVisibilityMessage(action: SessionAccessAction): string { if (action === "history") { return "Session history visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access."; } if (action === "send") { return "Session send visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access."; } if (action === "status") { return "Session status visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access."; } return "Session list visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access."; } function selfVisibilityMessage(action: SessionAccessAction): string { return `${actionPrefix(action)} visibility is restricted to the current session (tools.sessions.visibility=self).`; } function treeVisibilityMessage(action: SessionAccessAction): string { return `${actionPrefix(action)} visibility is restricted to the current session tree (tools.sessions.visibility=tree).`; } export function createSessionVisibilityChecker(params: { action: SessionAccessAction; requesterSessionKey: string; visibility: SessionToolsVisibility; a2aPolicy: AgentToAgentPolicy; spawnedKeys: Set | null; }): { check: (targetSessionKey: string) => SessionAccessResult } { const requesterAgentId = resolveAgentIdFromSessionKey(params.requesterSessionKey); const spawnedKeys = params.spawnedKeys; const check = (targetSessionKey: string): SessionAccessResult => { const targetAgentId = resolveAgentIdFromSessionKey(targetSessionKey); const isCrossAgent = targetAgentId !== requesterAgentId; if (isCrossAgent) { if (params.visibility !== "all") { return { allowed: false, status: "forbidden", error: crossVisibilityMessage(params.action), }; } if (!params.a2aPolicy.enabled) { return { allowed: false, status: "forbidden", error: a2aDisabledMessage(params.action), }; } if (!params.a2aPolicy.isAllowed(requesterAgentId, targetAgentId)) { return { allowed: false, status: "forbidden", error: a2aDeniedMessage(params.action), }; } return { allowed: true }; } if (params.visibility === "self" && targetSessionKey !== params.requesterSessionKey) { return { allowed: false, status: "forbidden", error: selfVisibilityMessage(params.action), }; } if ( params.visibility === "tree" && targetSessionKey !== params.requesterSessionKey && !spawnedKeys?.has(targetSessionKey) ) { return { allowed: false, status: "forbidden", error: treeVisibilityMessage(params.action), }; } return { allowed: true }; }; return { check }; } export async function createSessionVisibilityGuard(params: { action: SessionAccessAction; requesterSessionKey: string; visibility: SessionToolsVisibility; a2aPolicy: AgentToAgentPolicy; }): Promise<{ check: (targetSessionKey: string) => SessionAccessResult; }> { const spawnedKeys = params.visibility === "tree" ? await listSpawnedSessionKeys({ requesterSessionKey: params.requesterSessionKey }) : null; return createSessionVisibilityChecker({ action: params.action, requesterSessionKey: params.requesterSessionKey, visibility: params.visibility, a2aPolicy: params.a2aPolicy, spawnedKeys, }); }