From 4808361fcaa3e5b969ffa4b5d8e8ffd0d5d5c582 Mon Sep 17 00:00:00 2001 From: Devin Robison Date: Wed, 29 Apr 2026 13:35:55 -0600 Subject: [PATCH] fix: gate startup context for sandboxed spawned sessions (#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 --- CHANGELOG.md | 1 + src/gateway/server-methods/agent.test.ts | 87 ++++++++++++++++++++++++ src/gateway/server-methods/agent.ts | 59 ++++++++++++---- 3 files changed, 135 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba2925d6ad..a8d66bae11c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -226,6 +226,7 @@ Docs: https://docs.openclaw.ai - Configure/GitHub Copilot: reuse existing Copilot auth during configure and show the provider's manifest model catalog in the model picker. (#74276) Thanks @obviyus. - 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. - 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. +- 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. ## 2026.4.27 diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 9184173fec2..51bdf82bcdc 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -2288,6 +2288,93 @@ describe("gateway agent handler", () => { }); }); + it.each(["all", "non-main"] as const)( + "does not preload startup memory from inherited workspaces for spawned sandboxed sessions in %s mode", + async (sandboxMode) => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-04-27T12:00:00.000Z")); + try { + await withTempDir( + { prefix: "openclaw-gateway-startup-canonical-" }, + async (canonicalWorkspaceDir) => { + await withTempDir( + { prefix: "openclaw-gateway-startup-inherited-" }, + async (inheritedWorkspaceDir) => { + await fs.mkdir(`${inheritedWorkspaceDir}/memory`, { recursive: true }); + const inheritedMarker = "OC_INHERITED_WORKSPACE_MEMORY_MARKER"; + await fs.writeFile( + `${inheritedWorkspaceDir}/memory/2026-04-27.md`, + inheritedMarker, + "utf-8", + ); + mocks.loadConfigReturn = { + agents: { + defaults: { + workspace: canonicalWorkspaceDir, + userTimezone: "UTC", + startupContext: { + enabled: true, + applyOn: ["new"], + dailyMemoryDays: 1, + }, + sandbox: { + mode: sandboxMode, + scope: "session", + workspaceAccess: "none", + }, + }, + }, + }; + mockSessionResetSuccess({ + reason: "new", + key: "agent:main:subagent:sandbox-child", + }); + mocks.loadSessionEntry.mockReturnValue({ + cfg: mocks.loadConfigReturn, + storePath: "/tmp/sessions.json", + entry: { + sessionId: "existing-child-session", + updatedAt: Date.now(), + spawnedBy: "agent:main:main", + spawnedWorkspaceDir: inheritedWorkspaceDir, + }, + canonicalKey: "agent:main:subagent:sandbox-child", + }); + mocks.updateSessionStore.mockResolvedValue(undefined); + mocks.agentCommand.mockResolvedValue({ + payloads: [{ text: "ok" }], + meta: { durationMs: 100 }, + }); + + await invokeAgent( + { + message: "/new", + sessionKey: "agent:main:subagent:sandbox-child", + idempotencyKey: `test-idem-new-spawned-sandbox-memory-${sandboxMode}`, + }, + { + reqId: `4-startup-spawned-sandbox-memory-${sandboxMode}`, + client: { + connect: { scopes: ["operator.admin"] }, + } as AgentHandlerArgs["client"], + }, + ); + + await waitForAssertion(() => expect(mocks.agentCommand).toHaveBeenCalled()); + const call = readLastAgentCommandCall(); + expect(call?.message).toContain("Execute your Session Startup sequence now"); + expect(call?.message).not.toContain("[Startup context loaded by runtime]"); + expect(call?.message).not.toContain(inheritedMarker); + }, + ); + }, + ); + } finally { + vi.useRealTimers(); + } + }, + ); + it("uses /reset suffix as the post-reset message and still injects timestamp", async () => { setupNewYorkTimeConfig("2026-01-29T01:30:00.000Z"); mockSessionResetSuccess({ reason: "reset" }); diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 01ca26f224c..c908bf9cd10 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -10,6 +10,7 @@ import { resolvePublicAgentAvatarSource, } from "../../agents/identity-avatar.js"; import type { AgentInternalEvent } from "../../agents/internal-events.js"; +import { resolveSandboxConfigForAgent } from "../../agents/sandbox/config.js"; import { normalizeSpawnedRunMetadata, resolveIngressWorkspaceOverrideForSpawnedRun, @@ -181,6 +182,31 @@ function resolveSessionRuntimeWorkspace(params: { }; } +function shouldSkipStartupContextForSpawnedSandbox(params: { + cfg: OpenClawConfig; + sessionKey: string; + spawnedBy?: string; +}): boolean { + if (!params.spawnedBy) { + return false; + } + const agentId = resolveAgentIdFromSessionKey(params.sessionKey); + const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, agentId); + if (sandboxCfg.mode === "off") { + return false; + } + if (sandboxCfg.mode === "non-main") { + const mainSessionKey = resolveAgentMainSessionKey({ + cfg: params.cfg, + agentId, + }); + if (params.sessionKey.trim() === mainSessionKey.trim()) { + return false; + } + } + return sandboxCfg.workspaceAccess !== "rw"; +} + function emitSessionsChanged( context: Pick< GatewayRequestHandlerOptions["context"], @@ -1152,18 +1178,27 @@ export const agentHandlers: GatewayRequestHandlers = { } if (shouldPrependStartupContext && resolvedSessionKey) { - const { runtimeWorkspaceDir } = resolveSessionRuntimeWorkspace({ - cfg: cfgForAgent ?? cfg, - sessionKey: resolvedSessionKey, - sessionEntry, - spawnedBy: spawnedByValue, - }); - const startupContextPrelude = await buildSessionStartupContextPrelude({ - workspaceDir: runtimeWorkspaceDir, - cfg: cfgForAgent ?? cfg, - }); - if (startupContextPrelude) { - message = `${startupContextPrelude}\n\n${message}`; + const startupCfg = cfgForAgent ?? cfg; + if ( + !shouldSkipStartupContextForSpawnedSandbox({ + cfg: startupCfg, + sessionKey: resolvedSessionKey, + spawnedBy: spawnedByValue, + }) + ) { + const { runtimeWorkspaceDir } = resolveSessionRuntimeWorkspace({ + cfg: startupCfg, + sessionKey: resolvedSessionKey, + sessionEntry, + spawnedBy: spawnedByValue, + }); + const startupContextPrelude = await buildSessionStartupContextPrelude({ + workspaceDir: runtimeWorkspaceDir, + cfg: startupCfg, + }); + if (startupContextPrelude) { + message = `${startupContextPrelude}\n\n${message}`; + } } } if (!isRawModelRun) {