diff --git a/.agents/skills/crabbox/SKILL.md b/.agents/skills/crabbox/SKILL.md index 5e586a14c8b0..718dd2295238 100644 --- a/.agents/skills/crabbox/SKILL.md +++ b/.agents/skills/crabbox/SKILL.md @@ -630,7 +630,7 @@ Install/auth for owned Crabbox if needed: ```sh brew install openclaw/tap/crabbox -crabbox login --url https://crabbox.openclaw.ai --provider aws +crabbox login --provider aws ``` New users should self-resolve broker auth before anyone asks for AWS keys: @@ -641,7 +641,7 @@ crabbox doctor crabbox whoami ``` -- If broker auth is missing, run `crabbox login --url https://crabbox.openclaw.ai --provider aws`. +- If broker auth is missing, run `crabbox login --provider aws`. - If the CLI asks for `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, or AWS profile setup during normal OpenClaw validation, assume the agent selected the wrong path. Use brokered `crabbox login` or an existing brokered lease @@ -649,7 +649,7 @@ crabbox whoami - Ask for AWS keys only for explicit direct-provider/account administration, not for normal brokered OpenClaw proof. - Trusted automation may still use - `printf '%s' "$CRABBOX_COORDINATOR_TOKEN" | crabbox login --url https://crabbox.openclaw.ai --provider aws --token-stdin`. + `printf '%s' "$CRABBOX_COORDINATOR_TOKEN" | crabbox login --provider aws --token-stdin`. macOS config lives at: diff --git a/scripts/crabbox-wrapper.mjs b/scripts/crabbox-wrapper.mjs index 8ec56e9b3843..54ee1df3729e 100755 --- a/scripts/crabbox-wrapper.mjs +++ b/scripts/crabbox-wrapper.mjs @@ -175,6 +175,7 @@ function checkedOutput(command, commandArgs) { return { status: result.status ?? 1, text: `${result.stdout ?? ""}${result.stderr ?? ""}`.trim(), + stdout: (result.stdout ?? "").trim(), }; } @@ -392,6 +393,48 @@ function selectedProvider(commandArgs) { return commandProvider(commandArgs) || configuredProvider(); } +function shouldRequireBrokeredAws(commandArgs, providerName) { + if (process.env.OPENCLAW_CRABBOX_ALLOW_DIRECT_AWS === "1") { + return false; + } + const canonicalProvider = providerAliases.get(providerName) ?? providerName; + if (canonicalProvider !== "aws") { + return false; + } + if (commandArgs[0] === "run" || commandArgs[0] === "warmup") { + return true; + } + return commandArgs[0] === "actions" && commandArgs[1] === "hydrate"; +} + +function brokerAuthConfigured() { + const config = checkedOutput(binary, ["config", "show", "--json"]); + if (config.status !== 0) { + return false; + } + let parsed; + try { + parsed = JSON.parse(config.stdout || config.text); + } catch { + return false; + } + return Boolean(parsed?.coordinator && parsed?.brokerAuth === "configured"); +} + +function enforceBrokeredAws(commandArgs, providerName) { + if (!shouldRequireBrokeredAws(commandArgs, providerName) || brokerAuthConfigured()) { + return; + } + console.error( + [ + "[crabbox] provider=aws requires a configured Crabbox broker for OpenClaw proof.", + "[crabbox] run `crabbox login --provider aws`, then retry.", + "[crabbox] for intentional direct AWS provider debugging, set OPENCLAW_CRABBOX_ALLOW_DIRECT_AWS=1.", + ].join("\n"), + ); + process.exit(2); +} + function optionValue(commandArgs, name) { commandArgs = crabboxOptionArgs(commandArgs); for (let index = 0; index < commandArgs.length; index += 1) { @@ -1722,6 +1765,8 @@ if (provider && !isProviderAdvertised(provider, providers)) { process.exit(2); } +enforceBrokeredAws(normalizedArgs, provider); + if (provider === "blacksmith-testbox") { const envProvider = process.env.CRABBOX_PROVIDER?.trim(); const source = commandProviderValue diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index 9ad2c7d5975d..43a929a24bad 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -28,6 +28,15 @@ function writeFakeCrabbox(binDir: string, helpText: string): string { ` process.stdout.write(${JSON.stringify(helpText)});`, " process.exit(0);", "}", + 'if (args[0] === "config" && args[1] === "show" && args.includes("--json")) {', + " const status = Number.parseInt(process.env.OPENCLAW_FAKE_CRABBOX_CONFIG_STATUS || '0', 10);", + " if (status !== 0) {", + " process.stderr.write('config unavailable\\n');", + " process.exit(status);", + " }", + " process.stdout.write(process.env.OPENCLAW_FAKE_CRABBOX_CONFIG_JSON || '{\"coordinator\":\"configured-broker\",\"brokerAuth\":\"configured\"}');", + " process.exit(0);", + "}", "const scriptIndex = args.findIndex((arg) => arg === '--script' || arg === '-script');", "const scriptPath = scriptIndex >= 0 ? args[scriptIndex + 1] : '';", "const scriptContent = scriptPath ? require('node:fs').readFileSync(scriptPath, 'utf8') : '';", @@ -75,6 +84,9 @@ function runWrapper( helpText: string, args: string[], options: { + configJson?: Record; + configStatus?: number; + env?: Record; extraPathEntries?: string[]; gitResponses?: Record; input?: string; @@ -92,6 +104,13 @@ function runWrapper( .filter(Boolean) .join(path.delimiter), OPENCLAW_CRABBOX_WRAPPER_IGNORE_REPO_BINARY: "1", + ...(options.configJson + ? { OPENCLAW_FAKE_CRABBOX_CONFIG_JSON: JSON.stringify(options.configJson) } + : {}), + ...(options.configStatus + ? { OPENCLAW_FAKE_CRABBOX_CONFIG_STATUS: String(options.configStatus) } + : {}), + ...(options.env ?? {}), ...(options.gitResponses ? { OPENCLAW_FAKE_GIT_RESPONSES: JSON.stringify(options.gitResponses) } : {}), @@ -189,6 +208,40 @@ describe("scripts/crabbox-wrapper", () => { ]); }); + it("fails closed for AWS proof when broker auth is missing", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "aws", "--", "echo ok"], + { configJson: { coordinator: "", brokerAuth: "missing" } }, + ); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("provider=aws requires a configured Crabbox broker"); + expect(result.stderr).toContain("crabbox login --provider aws"); + expect(result.stderr).not.toContain("crabbox.openclaw.ai"); + }); + + it("allows explicit direct AWS debugging without broker auth", () => { + const result = runWrapper( + "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n", + ["run", "--provider", "aws", "--", "echo ok"], + { + configJson: { coordinator: "", brokerAuth: "missing" }, + env: { OPENCLAW_CRABBOX_ALLOW_DIRECT_AWS: "1" }, + }, + ); + + expect(result.status).toBe(0); + expect(parseFakeCrabboxOutput(result).args).toEqual([ + "run", + "--provider", + "aws", + "--", + "echo ok", + ]); + }); + it("defaults AWS macOS warmups to on-demand capacity", () => { const result = runWrapper( "provider: hetzner, aws, local-container, blacksmith-testbox, or cloudflare\n",