From 5c7e1341ab35d7465f01295fe7cdc9147eaa96cf Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 11 Jul 2026 16:44:47 -0700 Subject: [PATCH] fix: fail crabbox lease-claim refusals (#104789) --- scripts/crabbox-wrapper.mjs | 41 +++++++++++++++++++++------- test/scripts/crabbox-wrapper.test.ts | 36 ++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index b639aaa6ecdf..43e70e8bbc52 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -936,6 +936,21 @@ function blacksmithTestboxPrivateKeyPath(id) { return resolve(crabboxConfigDir(), "testboxes", id, "id_ed25519"); } +// Crabbox claims bind raw Testbox ids to one repo before remote execution. +// Check the same sidecar so a dependency exit bug cannot make refusal green. +function blacksmithTestboxClaimRepoRoot(id) { + const configuredStateRoot = process.env.XDG_STATE_HOME?.trim(); + const stateDir = configuredStateRoot + ? resolve(configuredStateRoot, "crabbox") + : resolve(crabboxConfigDir(), "state"); + const claimPath = resolve(stateDir, "claims", `${id}.json`); + if (!pathExists(claimPath)) { + return ""; + } + const claim = JSON.parse(readFileSync(claimPath, "utf8")); + return typeof claim.repoRoot === "string" ? claim.repoRoot : ""; +} + function enforceCrabboxOwnedBlacksmithLease(commandArgs) { if (commandArgs[0] !== "run") { return; @@ -949,18 +964,24 @@ function enforceCrabboxOwnedBlacksmithLease(commandArgs) { } const keyPath = blacksmithTestboxPrivateKeyPath(id); - if (pathExists(keyPath)) { - return; + if (!pathExists(keyPath)) { + console.error( + [ + `[crabbox] provider=blacksmith-testbox --id ${id} has no Crabbox SSH key at ${userDisplayPath(keyPath)}.`, + "[crabbox] create reusable Testboxes through Crabbox before reusing them: node scripts/crabbox-wrapper.mjs warmup --provider blacksmith-testbox --idle-timeout 90m", + "[crabbox] direct `blacksmith testbox warmup` leases can be used with `blacksmith testbox run`, but Crabbox cannot sync or run them by id.", + ].join("\n"), + ); + process.exit(2); } - console.error( - [ - `[crabbox] provider=blacksmith-testbox --id ${id} has no Crabbox SSH key at ${userDisplayPath(keyPath)}.`, - "[crabbox] create reusable Testboxes through Crabbox before reusing them: node scripts/crabbox-wrapper.mjs warmup --provider blacksmith-testbox --idle-timeout 90m", - "[crabbox] direct `blacksmith testbox warmup` leases can be used with `blacksmith testbox run`, but Crabbox cannot sync or run them by id.", - ].join("\n"), - ); - process.exit(2); + const claimRepoRoot = blacksmithTestboxClaimRepoRoot(id); + if (claimRepoRoot && claimRepoRoot !== repoRoot && !hasOption(commandArgs, "--reclaim")) { + console.error( + `[crabbox] lease ${id} is claimed by repo ${claimRepoRoot}; use --reclaim to claim it for ${repoRoot}`, + ); + process.exit(2); + } } function preserveTemporaryCrabboxRuns() { diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index ac835bfaf843..88903cc38b81 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -798,6 +798,42 @@ describe("scripts/crabbox-wrapper", () => { ]); }); + it("fails before reuse when a Blacksmith Testbox is claimed by another repo", () => { + const home = mkdtempSync(path.join(tmpdir(), "openclaw-crabbox-home-")); + tempDirs.push(home); + const id = "tbx_claimed"; + const keyPath = path.join(testCrabboxConfigDir(home), "testboxes", id, "id_ed25519"); + mkdirSync(path.dirname(keyPath), { recursive: true }); + writeFileSync(keyPath, "fake test key\n", "utf8"); + const stateRoot = path.join(home, ".local", "state"); + const claimPath = path.join(stateRoot, "crabbox", "claims", `${id}.json`); + mkdirSync(path.dirname(claimPath), { recursive: true }); + writeFileSync( + claimPath, + `${JSON.stringify({ leaseID: id, repoRoot: "/tmp/other-repo" })}\n`, + "utf8", + ); + + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "blacksmith-testbox", "--id", id, "--", "echo ok"], + { env: { ...testHomeEnv(home), XDG_STATE_HOME: stateRoot } }, + ); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain(`lease ${id} is claimed by repo /tmp/other-repo`); + expect(result.stderr).toContain(`use --reclaim to claim it for ${repoRoot}`); + + const reclaimed = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "blacksmith-testbox", "--id", id, "--reclaim", "--", "echo ok"], + { env: { ...testHomeEnv(home), XDG_STATE_HOME: stateRoot } }, + ); + expect(reclaimed.status).toBe(0); + expect(parseFakeCrabboxOutput(reclaimed).args).toContain("--reclaim"); + }); + it("lets Crabbox resolve reusable Testbox slugs", () => { const home = mkdtempSync(path.join(tmpdir(), "openclaw-crabbox-home-")); tempDirs.push(home);