fix: fail crabbox lease-claim refusals (#104789)

This commit is contained in:
Peter Steinberger
2026-07-11 16:44:47 -07:00
committed by GitHub
parent d1f614c9e6
commit 5c7e1341ab
2 changed files with 67 additions and 10 deletions
+31 -10
View File
@@ -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() {
+36
View File
@@ -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);