mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
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
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user