From ca69917153a8745675d8492f57e504bab86cd29d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 3 May 2026 12:49:09 +0100 Subject: [PATCH] test(sandbox): cover registry migration --- CHANGELOG.md | 1 + docs/cli/doctor.md | 1 + docs/cli/sandbox.md | 9 ++++ src/agents/sandbox/registry.test.ts | 70 +++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b1d8805f34..9559ee35910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/cli/doctor.md b/docs/cli/doctor.md index 095c66f64d1..fd63975e3e4 100644 --- a/docs/cli/doctor.md +++ b/docs/cli/doctor.md @@ -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..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. diff --git a/docs/cli/sandbox.md b/docs/cli/sandbox.md index 8c7e6c25379..b16f1576b3a 100644 --- a/docs/cli/sandbox.md +++ b/docs/cli/sandbox.md @@ -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. +## 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`): diff --git a/src/agents/sandbox/registry.test.ts b/src/agents/sandbox/registry.test.ts index 87a29798765..a30aff5bfc9 100644 --- a/src/agents/sandbox/registry.test.ts +++ b/src/agents/sandbox/registry.test.ts @@ -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({