test(sandbox): cover registry migration

This commit is contained in:
Peter Steinberger
2026-05-03 12:49:09 +01:00
parent 1402997489
commit ca69917153
4 changed files with 81 additions and 0 deletions

View File

@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
- Plugins/update: on the beta OpenClaw update channel, default-line npm and ClawHub plugin updates try `@beta` first and fall back to default/latest when no plugin beta release exists.
- Channels/WhatsApp: support explicit WhatsApp Channel/Newsletter `@newsletter` outbound message targets with channel session metadata instead of DM routing. Fixes #13417; carries forward the narrow outbound target idea from #13424. Thanks @vincentkoc and @agentz-manfred.
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
### Fixes

View File

@@ -57,6 +57,7 @@ Notes:
- Doctor warns when Codex-mode agents are configured and personal Codex CLI assets exist in the operator's Codex home. Local Codex app-server launches use isolated per-agent homes, so use `openclaw migrate codex --dry-run` to inventory assets that should be promoted deliberately.
- Doctor warns when skills allowed for the default agent are unavailable in the current runtime environment because bins, env vars, config, or OS requirements are missing. `doctor --fix` can disable those unavailable skills with `skills.entries.<skill>.enabled=false`; install/configure the missing requirement instead when you want to keep the skill active.
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
- If legacy sandbox registry files (`~/.openclaw/sandbox/containers.json` or `~/.openclaw/sandbox/browsers.json`) are present, doctor reports them; `openclaw doctor --fix` migrates valid entries into sharded registry directories and quarantines invalid legacy files.
- If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials.
- If channel SecretRef inspection fails in a fix path, doctor continues and reports a warning instead of exiting early.
- After state-directory migrations, doctor warns when enabled default Telegram or Discord accounts depend on env fallback and `TELEGRAM_BOT_TOKEN` or `DISCORD_BOT_TOKEN` is unavailable to the doctor process.

View File

@@ -164,6 +164,15 @@ Use `openclaw sandbox recreate` to force removal of old runtimes. They are recre
Prefer `openclaw sandbox recreate` over manual backend-specific cleanup. It uses the Gateway's runtime registry and avoids mismatches when scope or session keys change.
</Tip>
## Registry migration
OpenClaw stores sandbox runtime metadata as one JSON shard per container/browser entry under the sandbox state directory. Older installs may still have monolithic legacy files:
- `~/.openclaw/sandbox/containers.json`
- `~/.openclaw/sandbox/browsers.json`
Regular sandbox runtime reads do not rewrite those files. Run `openclaw doctor --fix` to migrate valid legacy entries into the sharded registry directories. Invalid legacy files are quarantined so one bad old registry cannot hide current runtime entries.
## Configuration
Sandbox settings live in `~/.openclaw/openclaw.json` under `agents.defaults.sandbox` (per-agent overrides go in `agents.list[].sandbox`):

View File

@@ -172,6 +172,22 @@ async function seedContainerRegistry(entries: SandboxRegistryEntry[]) {
await fs.writeFile(SANDBOX_REGISTRY_PATH, `${JSON.stringify({ entries }, null, 2)}\n`, "utf-8");
}
async function seedBrowserRegistry(entries: SandboxBrowserRegistryEntry[]) {
await fs.writeFile(
SANDBOX_BROWSER_REGISTRY_PATH,
`${JSON.stringify({ entries }, null, 2)}\n`,
"utf-8",
);
}
async function seedStaleLock(lockPath: string) {
await fs.writeFile(
lockPath,
`${JSON.stringify({ pid: 999_999_999, createdAt: "2000-01-01T00:00:00.000Z" })}\n`,
"utf-8",
);
}
describe("registry race safety", () => {
it("does not migrate legacy registry files from runtime reads", async () => {
await seedContainerRegistry([containerEntry({ containerName: "legacy-container" })]);
@@ -204,6 +220,60 @@ describe("registry race safety", () => {
]);
});
it("migrates legacy container and browser registry files after explicit repair", async () => {
await seedContainerRegistry([
containerEntry({
containerName: "legacy-container",
sessionKey: "agent:legacy",
lastUsedAtMs: 7,
configHash: "legacy-container-hash",
}),
]);
await seedBrowserRegistry([
browserEntry({
containerName: "legacy-browser",
sessionKey: "agent:legacy",
cdpPort: 9333,
noVncPort: 6081,
configHash: "legacy-browser-hash",
}),
]);
await seedStaleLock(`${SANDBOX_REGISTRY_PATH}.lock`);
await seedStaleLock(`${SANDBOX_BROWSER_REGISTRY_PATH}.lock`);
await expect(migrateLegacySandboxRegistryFiles()).resolves.toEqual([
expect.objectContaining({ kind: "containers", status: "migrated", entries: 1 }),
expect.objectContaining({ kind: "browsers", status: "migrated", entries: 1 }),
]);
await expect(fs.access(SANDBOX_REGISTRY_PATH)).rejects.toThrow();
await expect(fs.access(SANDBOX_BROWSER_REGISTRY_PATH)).rejects.toThrow();
await expect(fs.access(`${SANDBOX_REGISTRY_PATH}.lock`)).rejects.toThrow();
await expect(fs.access(`${SANDBOX_BROWSER_REGISTRY_PATH}.lock`)).rejects.toThrow();
await expect(readRegistry()).resolves.toEqual({
entries: [
expect.objectContaining({
containerName: "legacy-container",
backendId: "docker",
runtimeLabel: "legacy-container",
sessionKey: "agent:legacy",
configHash: "legacy-container-hash",
}),
],
});
await expect(readBrowserRegistry()).resolves.toEqual({
entries: [
expect.objectContaining({
containerName: "legacy-browser",
sessionKey: "agent:legacy",
cdpPort: 9333,
noVncPort: 6081,
configHash: "legacy-browser-hash",
}),
],
});
});
it("does not overwrite newer sharded entries during legacy migration", async () => {
await updateRegistry(
containerEntry({