Skip to content
This repository was archived by the owner on May 5, 2026. It is now read-only.

Commit 4808361

Browse files
authored
fix: gate startup context for sandboxed spawned sessions (openclaw#73611)
* fix: gate startup context for sandboxed spawned sessions * docs: add startup sandbox changelog entry * fix: address startup sandbox review feedback * test: format startup sandbox coverage
1 parent 3abc90a commit 4808361

3 files changed

Lines changed: 135 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ Docs: https://docs.openclaw.ai
226226
- Configure/GitHub Copilot: reuse existing Copilot auth during configure and show the provider's manifest model catalog in the model picker. (#74276) Thanks @obviyus.
227227
- Configure/models: keep the model picker scoped to the selected manifest provider and enable its bundled plugin before catalog lookup, so choosing GitHub Copilot no longer falls back to Ollama or skips the catalog. (#74322) Thanks @obviyus.
228228
- Auto-reply/subagents: reject `/focus` from leaf subagents and scope fallback target resolution to the requesting subagent's children, so subagents cannot bind conversations outside their control boundary. (#73613) Thanks @drobison00.
229+
- Gateway/startup: skip inherited workspace startup memory for sandboxed spawned sessions without real-workspace write access, so `/new` no longer preloads host workspace memory into isolated child runs. (#73611) Thanks @drobison00.
229230

230231
## 2026.4.27
231232

src/gateway/server-methods/agent.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2288,6 +2288,93 @@ describe("gateway agent handler", () => {
22882288
});
22892289
});
22902290

2291+
it.each(["all", "non-main"] as const)(
2292+
"does not preload startup memory from inherited workspaces for spawned sandboxed sessions in %s mode",
2293+
async (sandboxMode) => {
2294+
vi.useFakeTimers();
2295+
vi.setSystemTime(new Date("2026-04-27T12:00:00.000Z"));
2296+
try {
2297+
await withTempDir(
2298+
{ prefix: "openclaw-gateway-startup-canonical-" },
2299+
async (canonicalWorkspaceDir) => {
2300+
await withTempDir(
2301+
{ prefix: "openclaw-gateway-startup-inherited-" },
2302+
async (inheritedWorkspaceDir) => {
2303+
await fs.mkdir(`${inheritedWorkspaceDir}/memory`, { recursive: true });
2304+
const inheritedMarker = "OC_INHERITED_WORKSPACE_MEMORY_MARKER";
2305+
await fs.writeFile(
2306+
`${inheritedWorkspaceDir}/memory/2026-04-27.md`,
2307+
inheritedMarker,
2308+
"utf-8",
2309+
);
2310+
mocks.loadConfigReturn = {
2311+
agents: {
2312+
defaults: {
2313+
workspace: canonicalWorkspaceDir,
2314+
userTimezone: "UTC",
2315+
startupContext: {
2316+
enabled: true,
2317+
applyOn: ["new"],
2318+
dailyMemoryDays: 1,
2319+
},
2320+
sandbox: {
2321+
mode: sandboxMode,
2322+
scope: "session",
2323+
workspaceAccess: "none",
2324+
},
2325+
},
2326+
},
2327+
};
2328+
mockSessionResetSuccess({
2329+
reason: "new",
2330+
key: "agent:main:subagent:sandbox-child",
2331+
});
2332+
mocks.loadSessionEntry.mockReturnValue({
2333+
cfg: mocks.loadConfigReturn,
2334+
storePath: "/tmp/sessions.json",
2335+
entry: {
2336+
sessionId: "existing-child-session",
2337+
updatedAt: Date.now(),
2338+
spawnedBy: "agent:main:main",
2339+
spawnedWorkspaceDir: inheritedWorkspaceDir,
2340+
},
2341+
canonicalKey: "agent:main:subagent:sandbox-child",
2342+
});
2343+
mocks.updateSessionStore.mockResolvedValue(undefined);
2344+
mocks.agentCommand.mockResolvedValue({
2345+
payloads: [{ text: "ok" }],
2346+
meta: { durationMs: 100 },
2347+
});
2348+
2349+
await invokeAgent(
2350+
{
2351+
message: "/new",
2352+
sessionKey: "agent:main:subagent:sandbox-child",
2353+
idempotencyKey: `test-idem-new-spawned-sandbox-memory-${sandboxMode}`,
2354+
},
2355+
{
2356+
reqId: `4-startup-spawned-sandbox-memory-${sandboxMode}`,
2357+
client: {
2358+
connect: { scopes: ["operator.admin"] },
2359+
} as AgentHandlerArgs["client"],
2360+
},
2361+
);
2362+
2363+
await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled());
2364+
const call = readLastAgentCommandCall();
2365+
expect(call?.message).toContain("Execute your Session Startup sequence now");
2366+
expect(call?.message).not.toContain("[Startup context loaded by runtime]");
2367+
expect(call?.message).not.toContain(inheritedMarker);
2368+
},
2369+
);
2370+
},
2371+
);
2372+
} finally {
2373+
vi.useRealTimers();
2374+
}
2375+
},
2376+
);
2377+
22912378
it("uses /reset suffix as the post-reset message and still injects timestamp", async () => {
22922379
setupNewYorkTimeConfig("2026-01-29T01:30:00.000Z");
22932380
mockSessionResetSuccess({ reason: "reset" });

src/gateway/server-methods/agent.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
resolvePublicAgentAvatarSource,
1111
} from "../../agents/identity-avatar.js";
1212
import type { AgentInternalEvent } from "../../agents/internal-events.js";
13+
import { resolveSandboxConfigForAgent } from "../../agents/sandbox/config.js";
1314
import {
1415
normalizeSpawnedRunMetadata,
1516
resolveIngressWorkspaceOverrideForSpawnedRun,
@@ -181,6 +182,31 @@ function resolveSessionRuntimeWorkspace(params: {
181182
};
182183
}
183184

185+
function shouldSkipStartupContextForSpawnedSandbox(params: {
186+
cfg: OpenClawConfig;
187+
sessionKey: string;
188+
spawnedBy?: string;
189+
}): boolean {
190+
if (!params.spawnedBy) {
191+
return false;
192+
}
193+
const agentId = resolveAgentIdFromSessionKey(params.sessionKey);
194+
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId);
195+
if (sandboxCfg.mode === "off") {
196+
return false;
197+
}
198+
if (sandboxCfg.mode === "non-main") {
199+
const mainSessionKey = resolveAgentMainSessionKey({
200+
cfg: params.cfg,
201+
agentId,
202+
});
203+
if (params.sessionKey.trim() === mainSessionKey.trim()) {
204+
return false;
205+
}
206+
}
207+
return sandboxCfg.workspaceAccess !== "rw";
208+
}
209+
184210
function emitSessionsChanged(
185211
context: Pick<
186212
GatewayRequestHandlerOptions["context"],
@@ -1152,18 +1178,27 @@ export const agentHandlers: GatewayRequestHandlers = {
11521178
}
11531179

11541180
if (shouldPrependStartupContext && resolvedSessionKey) {
1155-
const { runtimeWorkspaceDir } = resolveSessionRuntimeWorkspace({
1156-
cfg: cfgForAgent ?? cfg,
1157-
sessionKey: resolvedSessionKey,
1158-
sessionEntry,
1159-
spawnedBy: spawnedByValue,
1160-
});
1161-
const startupContextPrelude = await buildSessionStartupContextPrelude({
1162-
workspaceDir: runtimeWorkspaceDir,
1163-
cfg: cfgForAgent ?? cfg,
1164-
});
1165-
if (startupContextPrelude) {
1166-
message = `${startupContextPrelude}\n\n${message}`;
1181+
const startupCfg = cfgForAgent ?? cfg;
1182+
if (
1183+
!shouldSkipStartupContextForSpawnedSandbox({
1184+
cfg: startupCfg,
1185+
sessionKey: resolvedSessionKey,
1186+
spawnedBy: spawnedByValue,
1187+
})
1188+
) {
1189+
const { runtimeWorkspaceDir } = resolveSessionRuntimeWorkspace({
1190+
cfg: startupCfg,
1191+
sessionKey: resolvedSessionKey,
1192+
sessionEntry,
1193+
spawnedBy: spawnedByValue,
1194+
});
1195+
const startupContextPrelude = await buildSessionStartupContextPrelude({
1196+
workspaceDir: runtimeWorkspaceDir,
1197+
cfg: startupCfg,
1198+
});
1199+
if (startupContextPrelude) {
1200+
message = `${startupContextPrelude}\n\n${message}`;
1201+
}
11671202
}
11681203
}
11691204
if (!isRawModelRun) {

0 commit comments

Comments
 (0)