feat: remove OVH cloud and make featured_cloud an array (#1474)

- Remove OVH as a cloud provider: delete ovh/ directory (lib + 11 agent
  scripts), remove from manifest.json clouds and all ovh/* matrix entries,
  update README matrix table, remove OVH destroy case in CLI commands,
  and clean up all test harness references (mock.sh, mock-curl-script.sh,
  record.sh, e2e.sh, cloud-lib-api-surface.test.ts, test-infra-sync.test.ts)

- Make featured_cloud an array (string[]) so agents can recommend multiple
  clouds; update manifest.ts type, all 10 manifest.json values, and the
  prioritizeCloudsByCredentials() comparison in commands.ts

- Sandbox OAuth in subprocess tests: add OPENROUTER_API_KEY=sk-or-test-fake
  to the default env in cli-entry-edge-cases.test.ts and
  cmdrun-resolution.test.ts so get_or_prompt_api_key() never triggers the
  real OAuth browser flow during test runs

- Fix upload-file-security.test.ts SSH cloud count (5→4) after OVH removal

- Bump CLI version 0.5.6 → 0.5.7

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
L 2026-02-19 14:06:27 -05:00 committed by GitHub
parent 5612cda40b
commit 32522882c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 42 additions and 920 deletions

View file

@ -37,6 +37,9 @@ function runCli(
HOME: process.env.HOME,
SHELL: process.env.SHELL,
TERM: process.env.TERM || "xterm",
// Prevent OAuth browser from opening during tests — if OPENROUTER_API_KEY
// is set, get_or_prompt_api_key() skips the entire OAuth flow.
OPENROUTER_API_KEY: "sk-or-test-fake",
...env,
SPAWN_NO_UPDATE_CHECK: "1",
NODE_ENV: "",

View file

@ -74,12 +74,12 @@ function isSandboxOrContainer(cloud: string): boolean {
}
/** Check if a function name matches a pattern, allowing cloud-prefixed variants.
* e.g. hasFunctionOrVariant(fns, "run_server", "ovh") matches "run_server" or "run_ovh" */
* e.g. hasFunctionOrVariant(fns, "run_server", "sprite") matches "run_server" or "run_sprite" */
function hasFunctionOrVariant(functions: string[], baseName: string, cloud: string): boolean {
if (functions.includes(baseName)) return true;
// Check for cloud-prefixed variant (e.g. run_ovh, upload_file_sprite)
// Check for cloud-prefixed variant (e.g. run_sprite, upload_file_sprite)
const prefix = baseName.replace(/_server$/, "").replace(/_file$/, "");
const variant1 = `${prefix}_${cloud}`; // run_ovh, upload_file_ovh
const variant1 = `${prefix}_${cloud}`; // run_sprite, upload_file_sprite
const variant2 = `${baseName}_${cloud}`; // upload_file_sprite
return functions.includes(variant1) || functions.includes(variant2);
}
@ -161,11 +161,11 @@ describe("Cloud lib/common.sh API surface contracts", () => {
for (const fn of SSH_REQUIRED_FUNCTIONS) {
it(`${cloud}/lib/common.sh defines ${fn}() or cloud-prefixed variant`, () => {
// Some clouds (OVH, Sprite) use cloud-prefixed function names
// e.g. run_ovh instead of run_server, create_ovh_instance instead of create_server
// Some clouds use cloud-prefixed function names
// e.g. run_sprite instead of run_server, create_sprite_instance instead of create_server
const hasStandard = functions.includes(fn);
const hasVariant = hasFunctionOrVariant(functions, fn, cloud);
// Also check for <action>_<cloud>_<noun> patterns (create_ovh_instance)
// Also check for <action>_<cloud>_<noun> patterns (create_sprite_instance)
const hasExtendedVariant = functions.some((f) => {
const prefix = fn.split("_")[0]; // "create", "run", "upload", etc.
return f.startsWith(`${prefix}_${cloud}`);
@ -526,25 +526,6 @@ describe("Cloud lib/common.sh API surface contracts", () => {
}
});
// ── OVH special case (uses function prefixing) ─────────────────────
describe("OVH cloud special API pattern", () => {
const content = readCloudLib("ovh");
if (!content) return;
const functions = extractFunctionNames(content);
it("OVH lib defines signature-based auth functions", () => {
// OVH uses a custom auth pattern with signatures
const hasSigAuth = functions.some(
(fn) =>
fn.includes("sign") ||
fn.includes("ovh_api") ||
fn.includes("_signature")
);
expect(hasSigAuth).toBe(true);
});
});
// ── Sprite special case (CLI-based, no standard SSH) ───────────────
describe("Sprite cloud special CLI pattern", () => {

View file

@ -34,6 +34,9 @@ function runCli(
HOME: process.env.HOME,
SHELL: process.env.SHELL,
TERM: process.env.TERM || "xterm",
// Prevent OAuth browser from opening during tests — if OPENROUTER_API_KEY
// is set, get_or_prompt_api_key() skips the entire OAuth flow.
OPENROUTER_API_KEY: "sk-or-test-fake",
...env,
SPAWN_NO_UPDATE_CHECK: "1",
NODE_ENV: "",

View file

@ -120,7 +120,6 @@ function getCloudsInStripApiBase(): string[] {
"console.kamatera.com": "kamatera",
"api.latitude.sh": "latitude",
"infrahub-api.nexgencloud.com": "hyperstack",
"eu.api.ovh.com": "ovh",
"cloudapi.atlantic.net": "atlanticnet",
"invapi.hostkey.com": "hostkey",
"cloudsigma.com": "cloudsigma",

View file

@ -210,7 +210,7 @@ describe("upload_file() Security Patterns", () => {
.filter(([, info]) => info.type === "ssh");
it("should have multiple SSH-based clouds", () => {
expect(sshClouds.length).toBeGreaterThanOrEqual(5);
expect(sshClouds.length).toBeGreaterThanOrEqual(4);
});
for (const [cloud, info] of sshClouds) {

View file

@ -353,7 +353,7 @@ export function hasCloudCli(cloud: string): boolean {
export function prioritizeCloudsByCredentials(
clouds: string[],
manifest: Manifest,
featuredCloud?: string
featuredCloud?: string[]
): { sortedClouds: string[]; hintOverrides: Record<string, string>; credCount: number; cliCount: number } {
const withCreds: string[] = [];
const featured: string[] = [];
@ -362,7 +362,7 @@ export function prioritizeCloudsByCredentials(
for (const c of clouds) {
if (hasCloudCredentials(manifest.clouds[c].auth)) {
withCreds.push(c);
} else if (featuredCloud && c === featuredCloud) {
} else if (featuredCloud && featuredCloud.includes(c)) {
featured.push(c);
} else if (hasCloudCli(c)) {
withCli.push(c);
@ -1817,8 +1817,6 @@ function buildDeleteScript(cloud: string, connection: VMConnection): string {
case "aws":
return `${sourceLib}\nensure_aws_cli\ndestroy_server "${id}"`;
case "ovh":
return `${sourceLib}\nensure_ovh_authenticated\ndestroy_ovh_instance "${id}"`;
case "daytona":
return `${sourceLib}\nensure_daytona_cli\nensure_daytona_token\ndestroy_server "${id}"`;
case "sprite":
@ -2568,7 +2566,7 @@ function getHelpExamplesSection(): string {
spawn claude sprite --prompt "Fix all linter errors"
${pc.dim("# Execute Claude with prompt and exit")}
spawn codex sprite -p "Add tests" ${pc.dim("# Short form of --prompt")}
spawn openclaw ovh -f instructions.txt
spawn openclaw fly -f instructions.txt
${pc.dim("# Read prompt from file (short for --prompt-file)")}
spawn gptme gcp --dry-run ${pc.dim("# Preview without provisioning")}
spawn claude hetzner --headless ${pc.dim("# Provision, print connection info, exit")}

View file

@ -17,7 +17,7 @@ export interface AgentDef {
interactive_prompts?: Record<string, { prompt: string; default: string }>;
dotenv?: { path: string; values: Record<string, string> };
notes?: string;
featured_cloud?: string;
featured_cloud?: string[];
}
export interface CloudDef {