From 10b0f418e211229d5d3f74123c351e3e94ed920d Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Thu, 26 Mar 2026 20:13:03 -0600 Subject: [PATCH] refactor: update DigitalOcean authentication variable names - Changed all instances of `DO_API_TOKEN` to `DIGITALOCEAN_ACCESS_TOKEN` in the codebase for consistency and clarity. - Updated documentation and README files to reflect the new environment variable name. - Ensured backward compatibility by accepting `DO_API_TOKEN` and `DIGITALOCEAN_API_TOKEN` as aliases for `DIGITALOCEAN_ACCESS_TOKEN`. - Adjusted tests to use the new variable name and verify legacy aliases. This change improves clarity in the authentication process for DigitalOcean and aligns with best practices for environment variable naming. --- .../setup-agent-team/qa-fixtures-prompt.md | 10 +++--- .github/workflows/packer-snapshots.yml | 22 ++++++------ README.md | 4 +-- manifest.json | 2 +- .../__tests__/commands-exported-utils.test.ts | 4 +-- packages/cli/src/__tests__/do-cov.test.ts | 2 +- .../src/__tests__/do-payment-warning.test.ts | 32 ++++++++++++++--- .../run-path-credential-display.test.ts | 36 ++++++++++++++++--- .../__tests__/script-failure-guidance.test.ts | 12 +++---- packages/cli/src/commands/index.ts | 1 + packages/cli/src/commands/shared.ts | 25 ++++++++++--- packages/cli/src/digitalocean/digitalocean.ts | 21 +++++++---- packer/digitalocean.pkr.hcl | 4 +-- sh/digitalocean/README.md | 6 ++-- sh/e2e/interactive-harness.ts | 6 ++-- sh/e2e/lib/clouds/digitalocean.sh | 20 +++++++---- 16 files changed, 147 insertions(+), 60 deletions(-) diff --git a/.claude/skills/setup-agent-team/qa-fixtures-prompt.md b/.claude/skills/setup-agent-team/qa-fixtures-prompt.md index 52cfcec06..fc036454e 100644 --- a/.claude/skills/setup-agent-team/qa-fixtures-prompt.md +++ b/.claude/skills/setup-agent-team/qa-fixtures-prompt.md @@ -31,7 +31,7 @@ Cloud credentials are stored in `~/.config/spawn/{cloud}.json` (loaded by `sh/sh For each cloud with a fixture directory, check if its required env vars are set: - **hetzner**: `HCLOUD_TOKEN` -- **digitalocean**: `DO_API_TOKEN` +- **digitalocean**: `DIGITALOCEAN_ACCESS_TOKEN` - **aws**: `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` Skip clouds where credentials are missing (log which ones). @@ -53,11 +53,11 @@ curl -s -H "Authorization: Bearer ${HCLOUD_TOKEN}" "https://api.hetzner.cloud/v1 curl -s -H "Authorization: Bearer ${HCLOUD_TOKEN}" "https://api.hetzner.cloud/v1/locations" ``` -### DigitalOcean (needs DO_API_TOKEN) +### DigitalOcean (needs DIGITALOCEAN_ACCESS_TOKEN) ```bash -curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" "https://api.digitalocean.com/v2/account/keys" -curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" "https://api.digitalocean.com/v2/sizes" -curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" "https://api.digitalocean.com/v2/regions" +curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" "https://api.digitalocean.com/v2/account/keys" +curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" "https://api.digitalocean.com/v2/sizes" +curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" "https://api.digitalocean.com/v2/regions" ``` For any other cloud directories found, read their TypeScript module in `packages/cli/src/{cloud}/` to discover the API base URL and auth pattern, then call equivalent GET-only endpoints. diff --git a/.github/workflows/packer-snapshots.yml b/.github/workflows/packer-snapshots.yml index e5ca67882..5064e4a60 100644 --- a/.github/workflows/packer-snapshots.yml +++ b/.github/workflows/packer-snapshots.yml @@ -71,18 +71,18 @@ jobs: - name: Generate variables file run: | jq -n \ - --arg token "$DO_API_TOKEN" \ + --arg token "$DIGITALOCEAN_ACCESS_TOKEN" \ --arg agent "$AGENT_NAME" \ --arg tier "$TIER" \ --argjson install "$INSTALL_COMMANDS" \ '{ - do_api_token: $token, + digitalocean_access_token: $token, agent_name: $agent, cloud_init_tier: $tier, install_commands: $install }' > packer/auto.pkrvars.json env: - DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }} + DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }} AGENT_NAME: ${{ matrix.agent }} TIER: ${{ steps.config.outputs.tier }} INSTALL_COMMANDS: ${{ steps.config.outputs.install }} @@ -96,7 +96,7 @@ jobs: if: cancelled() run: | # Filter by spawn-packer tag to avoid destroying builder droplets from other workflows - DROPLET_IDS=$(curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" \ + DROPLET_IDS=$(curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \ "https://api.digitalocean.com/v2/droplets?per_page=200&tag_name=spawn-packer" \ | jq -r '.droplets[].id') @@ -107,28 +107,28 @@ jobs: for ID in $DROPLET_IDS; do echo "Destroying orphaned builder droplet: ${ID}" - curl -s -X DELETE -H "Authorization: Bearer ${DO_API_TOKEN}" \ + curl -s -X DELETE -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \ "https://api.digitalocean.com/v2/droplets/${ID}" || true done env: - DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }} + DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }} - name: Cleanup old snapshots if: success() run: | PREFIX="spawn-${AGENT_NAME}-" - SNAPSHOTS=$(curl -s -H "Authorization: Bearer ${DO_API_TOKEN}" \ + SNAPSHOTS=$(curl -s -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \ "https://api.digitalocean.com/v2/images?private=true&per_page=100" \ | jq -r --arg prefix "$PREFIX" \ '[.images[] | select(.name | startswith($prefix))] | sort_by(.created_at) | reverse | .[1:] | .[].id') for ID in $SNAPSHOTS; do echo "Deleting old snapshot: ${ID}" - curl -s -X DELETE -H "Authorization: Bearer ${DO_API_TOKEN}" \ + curl -s -X DELETE -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \ "https://api.digitalocean.com/v2/images/${ID}" || true done env: - DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }} + DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }} AGENT_NAME: ${{ matrix.agent }} - name: Submit to DO Marketplace @@ -162,7 +162,7 @@ jobs: HTTP_CODE=$(curl -s -o /tmp/mp-response.json -w "%{http_code}" \ -X PATCH \ -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${DO_API_TOKEN}" \ + -H "Authorization: Bearer ${DIGITALOCEAN_ACCESS_TOKEN}" \ -d "$(jq -n \ --arg reason "Nightly rebuild — $(date -u '+%Y-%m-%d')" \ --argjson imageId "$IMG_ID" \ @@ -177,6 +177,6 @@ jobs: exit 1 ;; esac env: - DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }} + DIGITALOCEAN_ACCESS_TOKEN: ${{ secrets.DO_API_TOKEN }} AGENT_NAME: ${{ matrix.agent }} MARKETPLACE_APP_IDS: ${{ secrets.MARKETPLACE_APP_IDS }} diff --git a/README.md b/README.md index 2bbc2d2c8..6cbb12854 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ export OPENROUTER_API_KEY=sk-or-v1-xxxxx # Cloud-specific credentials (varies by provider) # Note: Sprite uses `sprite login` for authentication export HCLOUD_TOKEN=... # For Hetzner -export DO_API_TOKEN=... # For DigitalOcean +export DIGITALOCEAN_ACCESS_TOKEN=... # For DigitalOcean # Run non-interactively spawn claude hetzner @@ -223,7 +223,7 @@ If spawn fails to install, try these steps: 2. **Set credentials via environment variables** before launching: ```powershell $env:OPENROUTER_API_KEY = "sk-or-v1-xxxxx" - $env:DO_API_TOKEN = "dop_v1_xxxxx" # For DigitalOcean + $env:DIGITALOCEAN_ACCESS_TOKEN = "dop_v1_xxxxx" # For DigitalOcean $env:HCLOUD_TOKEN = "xxxxx" # For Hetzner spawn openclaw digitalocean ``` diff --git a/manifest.json b/manifest.json index 304eac21c..b08816b5d 100644 --- a/manifest.json +++ b/manifest.json @@ -360,7 +360,7 @@ "description": "Cloud servers (account + payment method required)", "url": "https://www.digitalocean.com/", "type": "api", - "auth": "DO_API_TOKEN", + "auth": "DIGITALOCEAN_ACCESS_TOKEN", "provision_method": "POST /v2/droplets with user_data", "exec_method": "ssh root@IP", "interactive_method": "ssh -t root@IP", diff --git a/packages/cli/src/__tests__/commands-exported-utils.test.ts b/packages/cli/src/__tests__/commands-exported-utils.test.ts index 0d50cb273..9a500bd01 100644 --- a/packages/cli/src/__tests__/commands-exported-utils.test.ts +++ b/packages/cli/src/__tests__/commands-exported-utils.test.ts @@ -47,8 +47,8 @@ describe("parseAuthEnvVars", () => { }); it("should extract env var starting with letter followed by digits", () => { - expect(parseAuthEnvVars("DO_API_TOKEN")).toEqual([ - "DO_API_TOKEN", + expect(parseAuthEnvVars("DIGITALOCEAN_ACCESS_TOKEN")).toEqual([ + "DIGITALOCEAN_ACCESS_TOKEN", ]); }); }); diff --git a/packages/cli/src/__tests__/do-cov.test.ts b/packages/cli/src/__tests__/do-cov.test.ts index e9697ecfb..5895f8457 100644 --- a/packages/cli/src/__tests__/do-cov.test.ts +++ b/packages/cli/src/__tests__/do-cov.test.ts @@ -270,7 +270,7 @@ describe("digitalocean/getServerIp", () => { ); const { getServerIp } = await import("../digitalocean/digitalocean"); // Need to set the token state - process.env.DO_API_TOKEN = "test-token"; + process.env.DIGITALOCEAN_ACCESS_TOKEN = "test-token"; // getServerIp calls doApi which uses internal state token - need to set via ensureDoToken // But doApi will use _state.token. Since we can't easily set _state, we test the 404 path // by mocking fetch to always return 404 diff --git a/packages/cli/src/__tests__/do-payment-warning.test.ts b/packages/cli/src/__tests__/do-payment-warning.test.ts index e02fc5020..456c1c6bd 100644 --- a/packages/cli/src/__tests__/do-payment-warning.test.ts +++ b/packages/cli/src/__tests__/do-payment-warning.test.ts @@ -25,9 +25,15 @@ describe("ensureDoToken — payment method warning for first-time users", () => let warnSpy: ReturnType; beforeEach(() => { - // Save and clear DO_API_TOKEN - savedEnv["DO_API_TOKEN"] = process.env.DO_API_TOKEN; - delete process.env.DO_API_TOKEN; + // Save and clear all accepted DigitalOcean token env vars + for (const v of [ + "DIGITALOCEAN_ACCESS_TOKEN", + "DIGITALOCEAN_API_TOKEN", + "DO_API_TOKEN", + ]) { + savedEnv[v] = process.env[v]; + delete process.env[v]; + } // Fail OAuth connectivity check → tryDoOAuth returns null immediately globalThis.fetch = mock(() => Promise.reject(new Error("Network unreachable"))); @@ -73,7 +79,25 @@ describe("ensureDoToken — payment method warning for first-time users", () => expect(warnMessages.some((msg: string) => msg.includes("payment method"))).toBe(false); }); - it("does NOT show payment warning when DO_API_TOKEN env var is set", async () => { + it("does NOT show payment warning when DIGITALOCEAN_ACCESS_TOKEN env var is set", async () => { + process.env.DIGITALOCEAN_ACCESS_TOKEN = "dop_v1_invalid_env_token"; + + await expect(ensureDoToken()).rejects.toThrow(); + + const warnMessages = warnSpy.mock.calls.map((c: unknown[]) => String(c[0])); + expect(warnMessages.some((msg: string) => msg.includes("payment method"))).toBe(false); + }); + + it("does NOT show payment warning when DIGITALOCEAN_API_TOKEN env var is set", async () => { + process.env.DIGITALOCEAN_API_TOKEN = "dop_v1_invalid_env_token"; + + await expect(ensureDoToken()).rejects.toThrow(); + + const warnMessages = warnSpy.mock.calls.map((c: unknown[]) => String(c[0])); + expect(warnMessages.some((msg: string) => msg.includes("payment method"))).toBe(false); + }); + + it("does NOT show payment warning when legacy DO_API_TOKEN env var is set", async () => { process.env.DO_API_TOKEN = "dop_v1_invalid_env_token"; await expect(ensureDoToken()).rejects.toThrow(); diff --git a/packages/cli/src/__tests__/run-path-credential-display.test.ts b/packages/cli/src/__tests__/run-path-credential-display.test.ts index e361c298e..5f1eceec9 100644 --- a/packages/cli/src/__tests__/run-path-credential-display.test.ts +++ b/packages/cli/src/__tests__/run-path-credential-display.test.ts @@ -68,7 +68,7 @@ function makeManifest(overrides?: Partial): Manifest { price: "test", url: "https://digitalocean.com", type: "api", - auth: "DO_API_TOKEN", + auth: "DIGITALOCEAN_ACCESS_TOKEN", provision_method: "api", exec_method: "ssh root@IP", interactive_method: "ssh -t root@IP", @@ -138,6 +138,8 @@ describe("prioritizeCloudsByCredentials", () => { // Save and clear credential env vars for (const v of [ "HCLOUD_TOKEN", + "DIGITALOCEAN_ACCESS_TOKEN", + "DIGITALOCEAN_API_TOKEN", "DO_API_TOKEN", "UPCLOUD_USERNAME", "UPCLOUD_PASSWORD", @@ -191,7 +193,7 @@ describe("prioritizeCloudsByCredentials", () => { it("should move multiple credential clouds to front", () => { process.env.HCLOUD_TOKEN = "test-token"; - process.env.DO_API_TOKEN = "test-do-token"; + process.env.DIGITALOCEAN_ACCESS_TOKEN = "test-do-token"; const manifest = makeManifest(); const clouds = [ "upcloud", @@ -290,7 +292,7 @@ describe("prioritizeCloudsByCredentials", () => { it("should preserve relative order within each group", () => { process.env.HCLOUD_TOKEN = "token"; - process.env.DO_API_TOKEN = "token"; + process.env.DIGITALOCEAN_ACCESS_TOKEN = "token"; const manifest = makeManifest(); // Input order: digitalocean before hetzner (both have creds) const clouds = [ @@ -331,7 +333,7 @@ describe("prioritizeCloudsByCredentials", () => { it("should count all credential clouds correctly with all set", () => { process.env.HCLOUD_TOKEN = "t1"; - process.env.DO_API_TOKEN = "t2"; + process.env.DIGITALOCEAN_ACCESS_TOKEN = "t2"; process.env.UPCLOUD_USERNAME = "u"; process.env.UPCLOUD_PASSWORD = "p"; const manifest = makeManifest(); @@ -350,4 +352,30 @@ describe("prioritizeCloudsByCredentials", () => { expect(result.sortedClouds.slice(3)).toContain("sprite"); expect(result.sortedClouds.slice(3)).toContain("localcloud"); }); + + it("should recognize legacy DO_API_TOKEN as alias for DIGITALOCEAN_ACCESS_TOKEN", () => { + process.env.DO_API_TOKEN = "legacy-token"; + const manifest = makeManifest(); + const clouds = [ + "digitalocean", + "hetzner", + ]; + const result = prioritizeCloudsByCredentials(clouds, manifest); + + expect(result.credCount).toBe(1); + expect(result.sortedClouds[0]).toBe("digitalocean"); + }); + + it("should recognize DIGITALOCEAN_API_TOKEN as alias for DIGITALOCEAN_ACCESS_TOKEN", () => { + process.env.DIGITALOCEAN_API_TOKEN = "alt-token"; + const manifest = makeManifest(); + const clouds = [ + "digitalocean", + "hetzner", + ]; + const result = prioritizeCloudsByCredentials(clouds, manifest); + + expect(result.credCount).toBe(1); + expect(result.sortedClouds[0]).toBe("digitalocean"); + }); }); diff --git a/packages/cli/src/__tests__/script-failure-guidance.test.ts b/packages/cli/src/__tests__/script-failure-guidance.test.ts index fab99e864..bb74c153e 100644 --- a/packages/cli/src/__tests__/script-failure-guidance.test.ts +++ b/packages/cli/src/__tests__/script-failure-guidance.test.ts @@ -213,12 +213,12 @@ describe("getScriptFailureGuidance", () => { it("should show specific env var name and setup hint for default case when authHint is provided", () => { const savedOR = process.env.OPENROUTER_API_KEY; - const savedDO = process.env.DO_API_TOKEN; + const savedDO = process.env.DIGITALOCEAN_ACCESS_TOKEN; delete process.env.OPENROUTER_API_KEY; - delete process.env.DO_API_TOKEN; - const lines = getScriptFailureGuidance(42, "digitalocean", "DO_API_TOKEN"); + delete process.env.DIGITALOCEAN_ACCESS_TOKEN; + const lines = getScriptFailureGuidance(42, "digitalocean", "DIGITALOCEAN_ACCESS_TOKEN"); const joined = lines.join("\n"); - expect(joined).toContain("DO_API_TOKEN"); + expect(joined).toContain("DIGITALOCEAN_ACCESS_TOKEN"); expect(joined).toContain("OPENROUTER_API_KEY"); expect(joined).toContain("spawn digitalocean"); expect(joined).toContain("setup"); @@ -226,7 +226,7 @@ describe("getScriptFailureGuidance", () => { process.env.OPENROUTER_API_KEY = savedOR; } if (savedDO !== undefined) { - process.env.DO_API_TOKEN = savedDO; + process.env.DIGITALOCEAN_ACCESS_TOKEN = savedDO; } }); @@ -234,7 +234,7 @@ describe("getScriptFailureGuidance", () => { const lines = getScriptFailureGuidance(42, "digitalocean"); const joined = lines.join("\n"); expect(joined).toContain("spawn digitalocean"); - expect(joined).not.toContain("DO_API_TOKEN"); + expect(joined).not.toContain("DIGITALOCEAN_ACCESS_TOKEN"); }); it("should handle multi-credential auth hint", () => { diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 00af41cf1..6c83f9d7d 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -55,6 +55,7 @@ export { getImplementedClouds, hasCloudCli, hasCloudCredentials, + isAuthEnvVarSet, isInteractiveTTY, levenshtein, loadManifestWithSpinner, diff --git a/packages/cli/src/commands/shared.ts b/packages/cli/src/commands/shared.ts index 329899aff..69ba15a63 100644 --- a/packages/cli/src/commands/shared.ts +++ b/packages/cli/src/commands/shared.ts @@ -477,9 +477,26 @@ export function parseAuthEnvVars(auth: string): string[] { .filter((s) => /^[A-Z][A-Z0-9_]{3,}$/.test(s)); } +/** Legacy env var names accepted as aliases for the canonical names in the manifest */ +const AUTH_VAR_ALIASES: Record = { + DIGITALOCEAN_ACCESS_TOKEN: [ + "DIGITALOCEAN_API_TOKEN", + "DO_API_TOKEN", + ], +}; + +/** Check if an auth env var (or one of its legacy aliases) is set */ +export function isAuthEnvVarSet(varName: string): boolean { + if (process.env[varName]) { + return true; + } + const aliases = AUTH_VAR_ALIASES[varName]; + return !!aliases?.some((a) => !!process.env[a]); +} + /** Format an auth env var line showing whether it's already set or needs to be exported */ function formatAuthVarLine(varName: string, urlHint?: string): string { - if (process.env[varName]) { + if (isAuthEnvVarSet(varName)) { return ` ${pc.green(varName)} ${pc.dim("-- set")}`; } const hint = urlHint ? ` ${pc.dim(`# ${urlHint}`)}` : ""; @@ -492,12 +509,12 @@ export function hasCloudCredentials(auth: string): boolean { if (vars.length === 0) { return false; } - return vars.every((v) => !!process.env[v]); + return vars.every((v) => isAuthEnvVarSet(v)); } /** Format a single credential env var as a status line (green if set, red if missing) */ export function formatCredStatusLine(varName: string, urlHint?: string): string { - if (process.env[varName]) { + if (isAuthEnvVarSet(varName)) { return ` ${pc.green(varName)} ${pc.dim("-- set")}`; } const suffix = urlHint ? ` ${pc.dim(urlHint)}` : ""; @@ -530,7 +547,7 @@ export function collectMissingCredentials(authVars: string[], cloud?: string): s missing.push("OPENROUTER_API_KEY"); } for (const v of authVars) { - if (!process.env[v]) { + if (!isAuthEnvVarSet(v)) { missing.push(v); } } diff --git a/packages/cli/src/digitalocean/digitalocean.ts b/packages/cli/src/digitalocean/digitalocean.ts index b80312901..0532da1df 100644 --- a/packages/cli/src/digitalocean/digitalocean.ts +++ b/packages/cli/src/digitalocean/digitalocean.ts @@ -664,14 +664,14 @@ async function tryDoOAuth(): Promise { if (oauthDenied) { logError("OAuth authorization was denied by the user"); logError("Alternative: Use a manual API token instead"); - logError(" export DO_API_TOKEN=dop_v1_..."); + logError(" export DIGITALOCEAN_ACCESS_TOKEN=dop_v1_..."); return null; } if (!oauthCode) { logError("OAuth authentication timed out after 120 seconds"); logError("Alternative: Use a manual API token instead"); - logError(" export DO_API_TOKEN=dop_v1_..."); + logError(" export DIGITALOCEAN_ACCESS_TOKEN=dop_v1_..."); return null; } @@ -727,15 +727,22 @@ async function tryDoOAuth(): Promise { /** Returns true if browser OAuth was triggered (so caller can delay before next OAuth). */ export async function ensureDoToken(): Promise { - // 1. Env var - if (process.env.DO_API_TOKEN) { - _state.token = process.env.DO_API_TOKEN.trim(); + // 1. Env var (DIGITALOCEAN_ACCESS_TOKEN > DIGITALOCEAN_API_TOKEN > DO_API_TOKEN) + const envToken = + process.env.DIGITALOCEAN_ACCESS_TOKEN ?? process.env.DIGITALOCEAN_API_TOKEN ?? process.env.DO_API_TOKEN; + if (envToken) { + const envVarName = process.env.DIGITALOCEAN_ACCESS_TOKEN + ? "DIGITALOCEAN_ACCESS_TOKEN" + : process.env.DIGITALOCEAN_API_TOKEN + ? "DIGITALOCEAN_API_TOKEN" + : "DO_API_TOKEN"; + _state.token = envToken.trim(); if (await testDoToken()) { logInfo("Using DigitalOcean API token from environment"); await saveTokenToConfig(_state.token); return false; } - logWarn("DO_API_TOKEN from environment is invalid"); + logWarn(`${envVarName} from environment is invalid`); _state.token = ""; } @@ -774,7 +781,7 @@ export async function ensureDoToken(): Promise { // 3. Try OAuth browser flow // Show payment method reminder for first-time users (no saved config, no env token) - if (!saved && !process.env.DO_API_TOKEN) { + if (!saved && !envToken) { process.stderr.write("\n"); logWarn("DigitalOcean requires a payment method before you can create servers."); logWarn("If you haven't added one yet, visit: https://cloud.digitalocean.com/account/billing"); diff --git a/packer/digitalocean.pkr.hcl b/packer/digitalocean.pkr.hcl index 2329be016..84ac88831 100644 --- a/packer/digitalocean.pkr.hcl +++ b/packer/digitalocean.pkr.hcl @@ -7,7 +7,7 @@ packer { } } -variable "do_api_token" { +variable "digitalocean_access_token" { type = string sensitive = true } @@ -32,7 +32,7 @@ locals { } source "digitalocean" "spawn" { - api_token = var.do_api_token + api_token = var.digitalocean_access_token image = "ubuntu-24-04-x64" region = "sfo3" # 2 GB RAM needed — Claude's native installer and zeroclaw's Rust build diff --git a/sh/digitalocean/README.md b/sh/digitalocean/README.md index 8ef181d9c..82a361cd9 100644 --- a/sh/digitalocean/README.md +++ b/sh/digitalocean/README.md @@ -56,7 +56,7 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/junie.sh) | Variable | Description | Default | |---|---|---| -| `DO_API_TOKEN` | DigitalOcean API token | — (OAuth if unset) | +| `DIGITALOCEAN_ACCESS_TOKEN` | DigitalOcean API token (also accepts `DIGITALOCEAN_API_TOKEN` or `DO_API_TOKEN`) | — (OAuth if unset) | | `DO_DROPLET_NAME` | Name for the created droplet | auto-generated | | `DO_REGION` | Datacenter region (see regions below) | `nyc3` | | `DO_DROPLET_SIZE` | Droplet size slug (see sizes below) | `s-2vcpu-2gb` | @@ -91,7 +91,7 @@ bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/junie.sh) ```bash DO_DROPLET_NAME=dev-mk1 \ -DO_API_TOKEN=your-token \ +DIGITALOCEAN_ACCESS_TOKEN=your-token \ OPENROUTER_API_KEY=sk-or-v1-xxxxx \ bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/claude.sh) ``` @@ -101,7 +101,7 @@ Override region and droplet size: ```bash DO_REGION=fra1 \ DO_DROPLET_SIZE=s-1vcpu-2gb \ -DO_API_TOKEN=your-token \ +DIGITALOCEAN_ACCESS_TOKEN=your-token \ OPENROUTER_API_KEY=sk-or-v1-xxxxx \ bash <(curl -fsSL https://openrouter.ai/labs/spawn/digitalocean/claude.sh) ``` diff --git a/sh/e2e/interactive-harness.ts b/sh/e2e/interactive-harness.ts index 8833cccd2..a89d12f36 100644 --- a/sh/e2e/interactive-harness.ts +++ b/sh/e2e/interactive-harness.ts @@ -9,7 +9,7 @@ // Required env: // ANTHROPIC_API_KEY — For the AI driver (Claude Haiku) // OPENROUTER_API_KEY — Injected into spawn for the agent -// Cloud credentials — HCLOUD_TOKEN, DO_API_TOKEN, AWS_ACCESS_KEY_ID, etc. +// Cloud credentials — HCLOUD_TOKEN, DIGITALOCEAN_ACCESS_TOKEN, AWS_ACCESS_KEY_ID, etc. // // Outputs JSON to stdout: { success: boolean, duration: number, transcript: string, uxIssues?: UxIssue[] } @@ -47,7 +47,7 @@ function buildCredentialHints(): string { const hetzner = process.env.HCLOUD_TOKEN ?? ""; if (hetzner) creds.push(`Hetzner token: ${hetzner}`); - const doToken = process.env.DO_API_TOKEN ?? ""; + const doToken = process.env.DIGITALOCEAN_ACCESS_TOKEN ?? process.env.DIGITALOCEAN_API_TOKEN ?? process.env.DO_API_TOKEN ?? ""; if (doToken) creds.push(`DigitalOcean token: ${doToken}`); const awsKey = process.env.AWS_ACCESS_KEY_ID ?? ""; @@ -79,6 +79,8 @@ function redactSecrets(text: string): string { const secrets = [ process.env.OPENROUTER_API_KEY, process.env.HCLOUD_TOKEN, + process.env.DIGITALOCEAN_ACCESS_TOKEN, + process.env.DIGITALOCEAN_API_TOKEN, process.env.DO_API_TOKEN, process.env.AWS_ACCESS_KEY_ID, process.env.AWS_SECRET_ACCESS_KEY, diff --git a/sh/e2e/lib/clouds/digitalocean.sh b/sh/e2e/lib/clouds/digitalocean.sh index 0867c57da..c4777fde8 100644 --- a/sh/e2e/lib/clouds/digitalocean.sh +++ b/sh/e2e/lib/clouds/digitalocean.sh @@ -4,11 +4,19 @@ # Implements the standard cloud driver interface (_digitalocean_*) for # provisioning and managing DigitalOcean droplets in the E2E test suite. # -# Requires: DO_API_TOKEN, jq, ssh +# Accepts: DIGITALOCEAN_ACCESS_TOKEN, DIGITALOCEAN_API_TOKEN, or DO_API_TOKEN # API: https://api.digitalocean.com/v2 # SSH user: root set -eo pipefail +# ── Resolve DigitalOcean token (canonical > alternate > legacy) ─────────── +if [ -n "${DIGITALOCEAN_ACCESS_TOKEN:-}" ]; then + DO_API_TOKEN="${DIGITALOCEAN_ACCESS_TOKEN}" +elif [ -n "${DIGITALOCEAN_API_TOKEN:-}" ]; then + DO_API_TOKEN="${DIGITALOCEAN_API_TOKEN}" +fi +export DO_API_TOKEN + # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- @@ -19,7 +27,7 @@ _DO_DEFAULT_REGION="nyc3" # --------------------------------------------------------------------------- # _do_curl_auth [curl-args...] # -# Wrapper around curl that passes the DO_API_TOKEN via a temp config file +# Wrapper around curl that passes the token via a temp config file # instead of a command-line -H flag. This keeps the token out of `ps` output. # All arguments are forwarded to curl. # --------------------------------------------------------------------------- @@ -37,19 +45,19 @@ _do_curl_auth() { # --------------------------------------------------------------------------- # _digitalocean_validate_env # -# Validates that DO_API_TOKEN is set and the DigitalOcean API is reachable -# with valid credentials. +# Validates that a DigitalOcean token is set and the API is reachable. +# Accepts DIGITALOCEAN_ACCESS_TOKEN, DIGITALOCEAN_API_TOKEN, or DO_API_TOKEN. # Returns 0 on success, 1 on failure. # --------------------------------------------------------------------------- _digitalocean_validate_env() { if [ -z "${DO_API_TOKEN:-}" ]; then - log_err "DO_API_TOKEN is not set" + log_err "DigitalOcean token is not set (set DIGITALOCEAN_ACCESS_TOKEN, DIGITALOCEAN_API_TOKEN, or DO_API_TOKEN)" return 1 fi if ! _do_curl_auth -sf \ "${_DO_API}/account" >/dev/null 2>&1; then - log_err "DigitalOcean API authentication failed — check DO_API_TOKEN" + log_err "DigitalOcean API authentication failed — check your token" return 1 fi