mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-25 19:33:34 +00:00
fix(security): restrict leaf subagent control scope
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
sortSubagentRuns,
|
||||
type SubagentTargetResolution,
|
||||
} from "../../auto-reply/reply/subagents-utils.js";
|
||||
import { DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH } from "../../config/agent-limits.js";
|
||||
import { loadConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { loadSessionStore, resolveStorePath, updateSessionStore } from "../../config/sessions.js";
|
||||
@@ -28,7 +27,6 @@ import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
||||
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
|
||||
import { abortEmbeddedPiRun } from "../pi-embedded.js";
|
||||
import { optionalStringEnum } from "../schema/typebox.js";
|
||||
import { getSubagentDepthFromSessionStore } from "../subagent-depth.js";
|
||||
import {
|
||||
clearSubagentRunSteerRestart,
|
||||
countPendingDescendantRuns,
|
||||
@@ -204,36 +202,28 @@ function resolveRequesterKey(params: {
|
||||
};
|
||||
}
|
||||
|
||||
// Check if this sub-agent can spawn children (orchestrator).
|
||||
// If so, it should see its own children, not its parent's children.
|
||||
const callerDepth = getSubagentDepthFromSessionStore(callerSessionKey, { cfg: params.cfg });
|
||||
const maxSpawnDepth =
|
||||
params.cfg.agents?.defaults?.subagents?.maxSpawnDepth ?? DEFAULT_SUBAGENT_MAX_SPAWN_DEPTH;
|
||||
if (callerDepth < maxSpawnDepth) {
|
||||
// Orchestrator sub-agent: use its own session key as requester
|
||||
// so it sees children it spawned.
|
||||
return {
|
||||
requesterSessionKey: callerSessionKey,
|
||||
callerSessionKey,
|
||||
callerIsSubagent: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Leaf sub-agent: walk up to its parent so it can see sibling runs.
|
||||
const cache = new Map<string, Record<string, SessionEntry>>();
|
||||
const callerEntry = resolveSessionEntryForKey({
|
||||
cfg: params.cfg,
|
||||
key: callerSessionKey,
|
||||
cache,
|
||||
}).entry;
|
||||
const spawnedBy = typeof callerEntry?.spawnedBy === "string" ? callerEntry.spawnedBy.trim() : "";
|
||||
return {
|
||||
requesterSessionKey: spawnedBy || callerSessionKey,
|
||||
// Subagents can only control runs spawned from their own session key.
|
||||
// Announce routing still uses SubagentRunRecord.requesterSessionKey elsewhere.
|
||||
requesterSessionKey: callerSessionKey,
|
||||
callerSessionKey,
|
||||
callerIsSubagent: true,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureSubagentControlsOwnDescendants(params: {
|
||||
requester: ResolvedRequesterKey;
|
||||
entry: SubagentRunRecord;
|
||||
}) {
|
||||
if (!params.requester.callerIsSubagent) {
|
||||
return undefined;
|
||||
}
|
||||
if (params.entry.requesterSessionKey === params.requester.callerSessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
return "Subagents can only control runs spawned from their own session.";
|
||||
}
|
||||
|
||||
async function killSubagentRun(params: {
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
entry: SubagentRunRecord;
|
||||
@@ -499,6 +489,20 @@ export function createSubagentsTool(opts?: { agentSessionKey?: string }): AnyAge
|
||||
error: resolved.error ?? "Unknown subagent target.",
|
||||
});
|
||||
}
|
||||
const ownershipError = ensureSubagentControlsOwnDescendants({
|
||||
requester,
|
||||
entry: resolved.entry,
|
||||
});
|
||||
if (ownershipError) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
action: "kill",
|
||||
target,
|
||||
runId: resolved.entry.runId,
|
||||
sessionKey: resolved.entry.childSessionKey,
|
||||
error: ownershipError,
|
||||
});
|
||||
}
|
||||
const killCache = new Map<string, Record<string, SessionEntry>>();
|
||||
const stopResult = await killSubagentRun({
|
||||
cfg,
|
||||
@@ -568,6 +572,20 @@ export function createSubagentsTool(opts?: { agentSessionKey?: string }): AnyAge
|
||||
error: resolved.error ?? "Unknown subagent target.",
|
||||
});
|
||||
}
|
||||
const ownershipError = ensureSubagentControlsOwnDescendants({
|
||||
requester,
|
||||
entry: resolved.entry,
|
||||
});
|
||||
if (ownershipError) {
|
||||
return jsonResult({
|
||||
status: "forbidden",
|
||||
action: "steer",
|
||||
target,
|
||||
runId: resolved.entry.runId,
|
||||
sessionKey: resolved.entry.childSessionKey,
|
||||
error: ownershipError,
|
||||
});
|
||||
}
|
||||
if (resolved.entry.endedAt) {
|
||||
return jsonResult({
|
||||
status: "done",
|
||||
|
||||
Reference in New Issue
Block a user