Skip to content

Commit e21d650

Browse files
committed
fix: separate sandbox policy from completion owner in sessions_spawn
PR #80242 passed runSessionKey as agentSessionKey to createSessionsSpawnTool, which caused spawnSubagentDirect to use the run session key for sandbox policy checks (resolveSandboxRuntimeStatus). This could make a sandboxed channel run appear unsandboxed. Introduce completionOwnerKey as a separate field that is only used for registerSubagentRun routing (requesterSessionKey), keeping agentSessionKey for sandbox enforcement, callerDepth, activeChildren, and all other policy checks.
1 parent e950b02 commit e21d650

4 files changed

Lines changed: 76 additions & 6 deletions

File tree

src/agents/openclaw-tools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,8 @@ export function createOpenClawTools(
427427
...(includeSubagentSpawnTool
428428
? [
429429
createSessionsSpawnTool({
430-
agentSessionKey: options?.runSessionKey ?? options?.agentSessionKey,
430+
agentSessionKey: options?.agentSessionKey,
431+
completionOwnerKey: options?.runSessionKey,
431432
agentChannel: options?.agentChannel,
432433
agentAccountId: options?.agentAccountId,
433434
agentTo: options?.agentTo,

src/agents/subagent-spawn.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ export type SpawnSubagentParams = {
147147

148148
export type SpawnSubagentContext = {
149149
agentSessionKey?: string;
150+
/** Separate key used only for completion routing, not sandbox policy. */
151+
completionOwnerKey?: string;
150152
agentChannel?: string;
151153
agentAccountId?: string;
152154
agentTo?: string;
@@ -774,6 +776,20 @@ export async function spawnSubagentDirect(
774776
alias,
775777
mainKey,
776778
});
779+
const completionOwnerInternalKey = ctx.completionOwnerKey
780+
? resolveInternalSessionKey({
781+
key: ctx.completionOwnerKey,
782+
alias,
783+
mainKey,
784+
})
785+
: undefined;
786+
const completionOwnerDisplayKey = completionOwnerInternalKey
787+
? resolveDisplaySessionKey({
788+
key: completionOwnerInternalKey,
789+
alias,
790+
mainKey,
791+
})
792+
: undefined;
777793

778794
const callerDepth = getSubagentDepthFromSessionStore(requesterInternalKey, { cfg });
779795
const maxSpawnDepth =
@@ -980,7 +996,7 @@ export async function spawnSubagentDirect(
980996
agentId: targetAgentId,
981997
label: label || undefined,
982998
mode: spawnMode,
983-
requesterSessionKey: requesterInternalKey,
999+
requesterSessionKey: completionOwnerInternalKey ?? requesterInternalKey,
9841000
requester: {
9851001
channel: childSessionOrigin?.channel,
9861002
accountId: childSessionOrigin?.accountId,
@@ -1246,9 +1262,9 @@ export async function spawnSubagentDirect(
12461262
runId: childRunId,
12471263
childSessionKey,
12481264
controllerSessionKey: requesterInternalKey,
1249-
requesterSessionKey: requesterInternalKey,
1265+
requesterSessionKey: completionOwnerInternalKey ?? requesterInternalKey,
12501266
requesterOrigin,
1251-
requesterDisplayKey,
1267+
requesterDisplayKey: completionOwnerDisplayKey ?? requesterDisplayKey,
12521268
task,
12531269
taskName,
12541270
cleanup,

src/agents/tools/sessions-spawn-tool.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,4 +947,40 @@ describe("sessions_spawn tool", () => {
947947
expect(peerContext.agentSessionKey).toBe(telegramPeerKey);
948948
expect(runContext.agentSessionKey).toBe(runSessionKey);
949949
});
950+
951+
it("passes completionOwnerKey through to spawnSubagentDirect separately from agentSessionKey", async () => {
952+
const tool = createSessionsSpawnTool({
953+
agentSessionKey: "agent:main:telegram:default:direct:456",
954+
completionOwnerKey: "agent:main:main",
955+
agentChannel: "telegram",
956+
agentAccountId: "default",
957+
agentTo: "telegram:direct:456",
958+
});
959+
960+
await tool.execute("call-completion-owner", { task: "background work" });
961+
962+
const spawnContext = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 1, "spawnSubagentDirect");
963+
expect(spawnContext.agentSessionKey).toBe("agent:main:telegram:default:direct:456");
964+
expect(spawnContext.completionOwnerKey).toBe("agent:main:main");
965+
});
966+
967+
it("uses completionOwnerKey for ACP registerSubagentRun requesterSessionKey", async () => {
968+
registerAcpBackendForTest();
969+
const tool = createSessionsSpawnTool({
970+
agentSessionKey: "agent:main:telegram:default:direct:456",
971+
completionOwnerKey: "agent:main:main",
972+
agentChannel: "telegram",
973+
agentAccountId: "default",
974+
agentTo: "telegram:direct:456",
975+
});
976+
977+
await tool.execute("call-acp-completion-owner", {
978+
runtime: "acp",
979+
task: "investigate",
980+
agentId: "codex",
981+
});
982+
983+
const registration = mockCallArg(hoisted.registerSubagentRunMock, 0, 0, "registerSubagentRun");
984+
expect(registration.requesterSessionKey).toBe("agent:main:main");
985+
});
950986
});

src/agents/tools/sessions-spawn-tool.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ function resolveAcpUnavailableMessage(opts?: { sandboxed?: boolean; config?: Ope
250250
export function createSessionsSpawnTool(
251251
opts?: {
252252
agentSessionKey?: string;
253+
/** Separate key used only for completion routing (registerSubagentRun requesterSessionKey). */
254+
completionOwnerKey?: string;
253255
agentChannel?: GatewayMessageChannel;
254256
agentAccountId?: string;
255257
agentTo?: string;
@@ -427,6 +429,20 @@ export function createSessionsSpawnTool(
427429
mainKey,
428430
})
429431
: alias;
432+
const completionOwnerInternalKey = opts?.completionOwnerKey
433+
? resolveInternalSessionKey({
434+
key: opts.completionOwnerKey,
435+
alias,
436+
mainKey,
437+
})
438+
: undefined;
439+
const completionOwnerDisplayKey = completionOwnerInternalKey
440+
? resolveDisplaySessionKey({
441+
key: completionOwnerInternalKey,
442+
alias,
443+
mainKey,
444+
})
445+
: undefined;
430446
const requesterDisplayKey = resolveDisplaySessionKey({
431447
key: requesterInternalKey,
432448
alias,
@@ -445,9 +461,9 @@ export function createSessionsSpawnTool(
445461
registerSubagentRun({
446462
runId: childRunId,
447463
childSessionKey,
448-
requesterSessionKey: requesterInternalKey,
464+
requesterSessionKey: completionOwnerInternalKey ?? requesterInternalKey,
449465
requesterOrigin,
450-
requesterDisplayKey,
466+
requesterDisplayKey: completionOwnerDisplayKey ?? requesterDisplayKey,
451467
task,
452468
taskName,
453469
cleanup: trackedCleanup,
@@ -497,6 +513,7 @@ export function createSessionsSpawnTool(
497513
},
498514
{
499515
agentSessionKey: opts?.agentSessionKey,
516+
completionOwnerKey: opts?.completionOwnerKey,
500517
agentChannel: opts?.agentChannel,
501518
agentAccountId: opts?.agentAccountId,
502519
agentTo: opts?.agentTo,

0 commit comments

Comments
 (0)