mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:11:26 +00:00
fix(security): scope session tools and webhook secret fallback
This commit is contained in:
@@ -44,6 +44,62 @@ export type SessionListRow = {
|
||||
messages?: unknown[];
|
||||
};
|
||||
|
||||
export type SessionToolsVisibility = "self" | "tree" | "agent" | "all";
|
||||
|
||||
export function resolveSessionToolsVisibility(cfg: OpenClawConfig): SessionToolsVisibility {
|
||||
const raw = (cfg.tools as { sessions?: { visibility?: unknown } } | undefined)?.sessions
|
||||
?.visibility;
|
||||
const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||
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 async function listSpawnedSessionKeys(params: {
|
||||
requesterSessionKey: string;
|
||||
limit?: number;
|
||||
}): Promise<Set<string>> {
|
||||
const limit =
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
? Math.max(1, Math.floor(params.limit))
|
||||
: 500;
|
||||
try {
|
||||
const list = await callGateway<{ sessions: Array<{ key?: unknown }> }>({
|
||||
method: "sessions.list",
|
||||
params: {
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
limit,
|
||||
spawnedBy: params.requesterSessionKey,
|
||||
},
|
||||
});
|
||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||
const keys = sessions
|
||||
.map((entry) => (typeof entry?.key === "string" ? entry.key : ""))
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
return new Set(keys);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKey(value?: string) {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
|
||||
@@ -8,6 +8,8 @@ import { truncateUtf16Safe } from "../../utils.js";
|
||||
import { jsonResult, readStringParam } from "./common.js";
|
||||
import {
|
||||
createAgentToAgentPolicy,
|
||||
listSpawnedSessionKeys,
|
||||
resolveEffectiveSessionToolsVisibility,
|
||||
resolveSessionReference,
|
||||
SessionListRow,
|
||||
resolveSandboxedSessionToolContext,
|
||||
@@ -167,7 +169,6 @@ async function isSpawnedSessionAllowed(params: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createSessionsHistoryTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
sandboxed?: boolean;
|
||||
@@ -189,11 +190,12 @@ export function createSessionsHistoryTool(opts?: {
|
||||
agentSessionKey: opts?.agentSessionKey,
|
||||
sandboxed: opts?.sandboxed,
|
||||
});
|
||||
const effectiveRequesterKey = requesterInternalKey ?? alias;
|
||||
const resolvedSession = await resolveSessionReference({
|
||||
sessionKey: sessionKeyParam,
|
||||
alias,
|
||||
mainKey,
|
||||
requesterInternalKey,
|
||||
requesterInternalKey: effectiveRequesterKey,
|
||||
restrictToSpawned,
|
||||
});
|
||||
if (!resolvedSession.ok) {
|
||||
@@ -203,9 +205,9 @@ export function createSessionsHistoryTool(opts?: {
|
||||
const resolvedKey = resolvedSession.key;
|
||||
const displayKey = resolvedSession.displayKey;
|
||||
const resolvedViaSessionId = resolvedSession.resolvedViaSessionId;
|
||||
if (restrictToSpawned && requesterInternalKey && !resolvedViaSessionId) {
|
||||
if (restrictToSpawned && !resolvedViaSessionId && resolvedKey !== effectiveRequesterKey) {
|
||||
const ok = await isSpawnedSessionAllowed({
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
requesterSessionKey: effectiveRequesterKey,
|
||||
targetSessionKey: resolvedKey,
|
||||
});
|
||||
if (!ok) {
|
||||
@@ -215,11 +217,22 @@ export function createSessionsHistoryTool(opts?: {
|
||||
});
|
||||
}
|
||||
}
|
||||
const visibility = resolveEffectiveSessionToolsVisibility({
|
||||
cfg,
|
||||
sandboxed: opts?.sandboxed === true,
|
||||
});
|
||||
|
||||
const a2aPolicy = createAgentToAgentPolicy(cfg);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(requesterInternalKey);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(effectiveRequesterKey);
|
||||
const targetAgentId = resolveAgentIdFromSessionKey(resolvedKey);
|
||||
const isCrossAgent = requesterAgentId !== targetAgentId;
|
||||
if (isCrossAgent && visibility !== "all") {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session history visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access.",
|
||||
});
|
||||
}
|
||||
if (isCrossAgent) {
|
||||
if (!a2aPolicy.enabled) {
|
||||
return jsonResult({
|
||||
@@ -236,6 +249,28 @@ export function createSessionsHistoryTool(opts?: {
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCrossAgent) {
|
||||
if (visibility === "self" && resolvedKey !== effectiveRequesterKey) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session history visibility is restricted to the current session (tools.sessions.visibility=self).",
|
||||
});
|
||||
}
|
||||
if (visibility === "tree" && resolvedKey !== effectiveRequesterKey) {
|
||||
const spawned = await listSpawnedSessionKeys({
|
||||
requesterSessionKey: effectiveRequesterKey,
|
||||
});
|
||||
if (!spawned.has(resolvedKey)) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session history visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const limit =
|
||||
typeof params.limit === "number" && Number.isFinite(params.limit)
|
||||
? Math.max(1, Math.floor(params.limit))
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
createAgentToAgentPolicy,
|
||||
classifySessionKind,
|
||||
deriveChannel,
|
||||
listSpawnedSessionKeys,
|
||||
resolveDisplaySessionKey,
|
||||
resolveEffectiveSessionToolsVisibility,
|
||||
resolveInternalSessionKey,
|
||||
resolveSandboxedSessionToolContext,
|
||||
type SessionListRow,
|
||||
@@ -42,6 +44,11 @@ export function createSessionsListTool(opts?: {
|
||||
agentSessionKey: opts?.agentSessionKey,
|
||||
sandboxed: opts?.sandboxed,
|
||||
});
|
||||
const effectiveRequesterKey = requesterInternalKey ?? alias;
|
||||
const visibility = resolveEffectiveSessionToolsVisibility({
|
||||
cfg,
|
||||
sandboxed: opts?.sandboxed === true,
|
||||
});
|
||||
|
||||
const kindsRaw = readStringArrayParam(params, "kinds")?.map((value) =>
|
||||
value.trim().toLowerCase(),
|
||||
@@ -72,15 +79,19 @@ export function createSessionsListTool(opts?: {
|
||||
activeMinutes,
|
||||
includeGlobal: !restrictToSpawned,
|
||||
includeUnknown: !restrictToSpawned,
|
||||
spawnedBy: restrictToSpawned ? requesterInternalKey : undefined,
|
||||
spawnedBy: restrictToSpawned ? effectiveRequesterKey : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const sessions = Array.isArray(list?.sessions) ? list.sessions : [];
|
||||
const storePath = typeof list?.path === "string" ? list.path : undefined;
|
||||
const a2aPolicy = createAgentToAgentPolicy(cfg);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(requesterInternalKey);
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(effectiveRequesterKey);
|
||||
const rows: SessionListRow[] = [];
|
||||
const spawnedKeys =
|
||||
visibility === "tree"
|
||||
? await listSpawnedSessionKeys({ requesterSessionKey: effectiveRequesterKey })
|
||||
: null;
|
||||
|
||||
for (const entry of sessions) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
@@ -93,8 +104,20 @@ export function createSessionsListTool(opts?: {
|
||||
|
||||
const entryAgentId = resolveAgentIdFromSessionKey(key);
|
||||
const crossAgent = entryAgentId !== requesterAgentId;
|
||||
if (crossAgent && !a2aPolicy.isAllowed(requesterAgentId, entryAgentId)) {
|
||||
continue;
|
||||
if (crossAgent) {
|
||||
if (visibility !== "all") {
|
||||
continue;
|
||||
}
|
||||
if (!a2aPolicy.isAllowed(requesterAgentId, entryAgentId)) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (visibility === "self" && key !== effectiveRequesterKey) {
|
||||
continue;
|
||||
}
|
||||
if (visibility === "tree" && key !== effectiveRequesterKey && !spawnedKeys?.has(key)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (key === "unknown") {
|
||||
|
||||
@@ -18,6 +18,8 @@ import { jsonResult, readStringParam } from "./common.js";
|
||||
import {
|
||||
createAgentToAgentPolicy,
|
||||
extractAssistantText,
|
||||
listSpawnedSessionKeys,
|
||||
resolveEffectiveSessionToolsVisibility,
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
resolveSessionReference,
|
||||
@@ -51,21 +53,25 @@ export function createSessionsSendTool(opts?: {
|
||||
const cfg = loadConfig();
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const visibility = cfg.agents?.defaults?.sandbox?.sessionToolsVisibility ?? "spawned";
|
||||
const requesterInternalKey =
|
||||
const requesterKeyInput =
|
||||
typeof opts?.agentSessionKey === "string" && opts.agentSessionKey.trim()
|
||||
? resolveInternalSessionKey({
|
||||
key: opts.agentSessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
})
|
||||
: undefined;
|
||||
? opts.agentSessionKey
|
||||
: "main";
|
||||
const requesterInternalKey = resolveInternalSessionKey({
|
||||
key: requesterKeyInput,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
const restrictToSpawned =
|
||||
opts?.sandboxed === true &&
|
||||
visibility === "spawned" &&
|
||||
!!requesterInternalKey &&
|
||||
!isSubagentSessionKey(requesterInternalKey);
|
||||
|
||||
const a2aPolicy = createAgentToAgentPolicy(cfg);
|
||||
const sessionVisibility = resolveEffectiveSessionToolsVisibility({
|
||||
cfg,
|
||||
sandboxed: opts?.sandboxed === true,
|
||||
});
|
||||
|
||||
const sessionKeyParam = readStringParam(params, "sessionKey");
|
||||
const labelParam = readStringParam(params, "label")?.trim() || undefined;
|
||||
@@ -199,7 +205,7 @@ export function createSessionsSendTool(opts?: {
|
||||
const displayKey = resolvedSession.displayKey;
|
||||
const resolvedViaSessionId = resolvedSession.resolvedViaSessionId;
|
||||
|
||||
if (restrictToSpawned && !resolvedViaSessionId) {
|
||||
if (restrictToSpawned && !resolvedViaSessionId && resolvedKey !== requesterInternalKey) {
|
||||
const sessions = await listSessions({
|
||||
includeGlobal: false,
|
||||
includeUnknown: false,
|
||||
@@ -227,6 +233,15 @@ export function createSessionsSendTool(opts?: {
|
||||
const requesterAgentId = resolveAgentIdFromSessionKey(requesterInternalKey);
|
||||
const targetAgentId = resolveAgentIdFromSessionKey(resolvedKey);
|
||||
const isCrossAgent = requesterAgentId !== targetAgentId;
|
||||
if (isCrossAgent && sessionVisibility !== "all") {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session send visibility is restricted. Set tools.sessions.visibility=all to allow cross-agent access.",
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
if (isCrossAgent) {
|
||||
if (!a2aPolicy.enabled) {
|
||||
return jsonResult({
|
||||
@@ -245,6 +260,30 @@ export function createSessionsSendTool(opts?: {
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (sessionVisibility === "self" && resolvedKey !== requesterInternalKey) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session send visibility is restricted to the current session (tools.sessions.visibility=self).",
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
if (sessionVisibility === "tree" && resolvedKey !== requesterInternalKey) {
|
||||
const spawned = await listSpawnedSessionKeys({
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
});
|
||||
if (!spawned.has(resolvedKey)) {
|
||||
return jsonResult({
|
||||
runId: crypto.randomUUID(),
|
||||
status: "forbidden",
|
||||
error:
|
||||
"Session send visibility is restricted to the current session tree (tools.sessions.visibility=tree).",
|
||||
sessionKey: displayKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const agentMessageContext = buildAgentToAgentMessageContext({
|
||||
|
||||
Reference in New Issue
Block a user