Improve error messages in shared utilities and cloud providers that
previously showed bare "Failed to..." messages without telling users
how to fix the problem.
Shared (shared/common.sh):
- generate_ssh_key_if_missing: handle ssh-keygen/mkdir failures with
disk space and permission guidance
- get_ssh_fingerprint: detect missing/corrupt public key files with
regeneration instructions
- generic_ssh_wait: structured "How to fix" with manual SSH test command
and firewall check
- _report_api_failure: add DNS/firewall/proxy guidance for network errors
- ensure_jq: platform-specific install commands for unknown package
managers, hash rehash hint after install
- get_openrouter_api_key_manual: structured guidance after 3 failed
attempts
Cloud providers:
- Contabo: actionable guidance for OAuth token failures
- Exoscale: guidance for credential validation and CLI download failures
- Netcup: network connectivity hint for API connection failure
- Scaleway: structured guidance for project ID lookup failure
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add log_install_failed helper to shared/common.sh that provides
structured troubleshooting for agent install failures: possible causes,
SSH debug command (when server IP available), manual install command,
and re-run suggestion. Also improve SSH key registration error message.
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When server destruction fails, users are left with a bare error message and
no indication that they may still be billed for a running server. This adds
dashboard URLs and clear warnings to destroy_server errors across 9 clouds
(Hetzner, UpCloud, Contabo, Netcup, RamNode, Hostinger, HOSTKEY, OVH,
Latitude). Also improves error messages for Koyeb (app creation, service
deployment, deployment timeout, instance ID), GitHub Codespaces (creation
failure, readiness timeout), and E2B (sandbox creation failure).
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SSH key registration in 11 cloud providers used unescaped key_name
directly in JSON request bodies. If the hostname (used to generate
key names) contained JSON-special characters like double-quotes, it
could break out of the JSON string and inject arbitrary JSON fields.
Fix: use json_escape for key_name in all providers, matching the
pattern already used by Scaleway.
Also fix GCP create_server which embedded the startup script inline
in --metadata with comma delimiters. Commas in the script could break
metadata parsing or inject additional metadata keys. Fix: use
--metadata-from-file for the startup script.
Affected providers: Hetzner, DigitalOcean, Vultr, BinaryLane,
Hostinger, Contabo, Cherry, HOSTKEY, Civo, Linode, Genesis Cloud, GCP.
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 10 inline `python3 -c "import json,sys; d=json.loads(...)..."` one-liners
across vultr, hetzner, digitalocean, and contabo with calls to a new shared
`extract_api_error_message` helper in shared/common.sh. The helper tries common
JSON error field patterns (message, error, error.message, error.error_message,
reason) and falls back to a caller-specified default.
This pattern appears 35+ times across cloud libs; this PR converts the first 4
clouds as a proof of concept. Remaining clouds can adopt incrementally.
Net reduction: 10 lines per converted cloud (~3 lines saved per call site).
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In-progress actions (installing, starting, connecting...) should use
log_step (cyan) to visually distinguish them from completion messages
which use log_info (green). This makes it easier for users to see at a
glance what is happening vs what has finished.
Changes:
- cli/install.sh: add log_step function, use it for install progress
- shared/common.sh: OAuth flow and non-interactive exec messages
- Cloud libs: interactive_session, auth, and cleanup messages
- Agent scripts: gateway startup and session opening messages
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consistently use log_step for progress/status messages ("Waiting for...",
"Fetching...", "Creating...") and reserve log_info for success/completion
messages. This gives users a clear visual distinction between operations
that are still running (cyan) vs operations that have completed (green).
Also adds periodic progress updates to silent polling loops in ramnode,
cherry, and netcup IP wait functions so users see activity during long waits.
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 cloud providers had identical 5-line check_ssh_key functions that
fetch SSH keys from the provider API and grep for the fingerprint.
Extract this pattern into a shared check_ssh_key_by_fingerprint helper
in shared/common.sh, reducing each cloud's function to a single line.
Affected clouds: BinaryLane, Cherry, Civo, Contabo, DigitalOcean,
Genesis Cloud, Hetzner, Hostinger, Latitude, Linode, OVH, Scaleway,
Vultr.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The get_model_id_interactive function returned MODEL_ID from env vars
without calling validate_model_id, bypassing the allowlist check. Also
migrated 13 legacy scripts from raw safe_read to get_model_id_interactive
which includes validation.
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
18 cloud lib/common.sh files had identical 7-line get_server_name()
functions (get_resource_name + validate_server_name + echo). Added a
shared get_validated_server_name helper to shared/common.sh and replaced
all duplicates with one-line delegations. Net -110 lines.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
~1500 progress messages across 481 files were using log_warn (yellow)
for normal status updates like "Installing...", "Setting up...",
"Creating server...", etc. This made users think something was wrong
when everything was proceeding normally.
Changes:
- Replace log_warn with log_step for all progress/status messages
- Keep log_warn only for actual warnings (errors, remediation hints)
- Remove emoji from 3 sprite completion messages
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Add a generic ensure_multi_credentials() helper to shared/common.sh that
handles the env-var/config-file/prompt/test/save flow for providers needing
multiple credentials. This eliminates ~270 lines of duplicated logic across
contabo, netcup, ramnode, ionos, and upcloud, replacing it with single
function calls.
Each provider's ensure_*_credentials() function is now 3-8 lines instead
of 30-65 lines.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline Python config loading (`_load_*_config`) and custom JSON
save functions (`_save_*_config`) in OVH, UpCloud, and Contabo with the
shared `_load_json_config_fields` and `_save_json_config` helpers.
Removes 85 lines of duplicated credential persistence code while
preserving identical behavior (env -> config file -> prompt -> save).
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ssh_run_server, ssh_upload_file, ssh_interactive_session, and
ssh_verify_connectivity to shared/common.sh. These four functions
were copy-pasted identically across 21 cloud provider lib files,
differing only in SSH username (root vs ubuntu).
Providers now set SSH_USER and delegate to the shared helpers via
one-line wrappers, reducing each provider's lib by ~20 lines.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Seven cloud providers had nearly identical instance status polling loops
(20-36 lines each). Extract the shared pattern into generic_wait_for_instance()
in shared/common.sh and replace the duplicated loops with one-liner calls.
Clouds refactored: Civo, Contabo, DigitalOcean, GenesisCloud, Linode, UpCloud, Vultr
Net reduction: ~99 lines (-185/+86)
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Split the two longest functions in the codebase into focused sub-functions:
Contabo create_server (108 -> 52 lines):
- _contabo_get_ssh_secret_ids: fetch SSH secret IDs from API
- _contabo_build_instance_body: construct API request JSON
- _contabo_wait_for_instance: poll until instance is running
Genesis Cloud create_server (91 -> 42 lines):
- _genesis_get_ssh_key_ids: fetch SSH key IDs from API
- _genesis_build_instance_body: construct API request JSON
- _genesis_wait_for_instance: poll until instance is active
No behavioral changes - pure restructuring for readability.
Agent: complexity-hunter
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Implement contabo/nanoclaw.sh with WhatsApp-based AI agent support.
- Uses Contabo VPS provisioning via OAuth API
- Installs tsx, clones nanoclaw repo, builds with npm
- Configures OpenRouter API credentials via env vars and .env file
- Launches interactive session with WhatsApp QR code auth
Agent: gap-filler-contabo-1
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Implements Kilo Code deployment on Contabo VPS:
- Uses Contabo OAuth API for server provisioning
- Installs @kilocode/cli via npm
- Injects OpenRouter API key as KILO_OPEN_ROUTER_API_KEY
- Sets KILO_PROVIDER_TYPE=openrouter for native OpenRouter support
- Launches interactive Kilo Code CLI session
Agent: gap-filler-remaining
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Hostinger: HOSTINGER_OS_TEMPLATE was interpolated into Python code
without validation, allowing Python code injection via env var.
Added validate_resource_name check.
Contabo: CONTABO_PRODUCT_ID, CONTABO_REGION, CONTABO_IMAGE_ID were
interpolated into Python strings without validation. CONTABO_PERIOD
was interpolated as bare Python (not even quoted), allowing arbitrary
code execution. Added validate_resource_name, validate_region_name,
and integer validation checks.
Agent: security-auditor
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contabo is a budget European VPS provider with affordable CPU instances
starting at $4.95/month. Ideal for AI agents using remote API inference.
Key features:
- Budget-friendly pricing ($4.95-$59/mo)
- Full REST API with OAuth2 authentication
- Cloud-init/user_data support for provisioning
- Full root access via SSH
- European data centers (GDPR-friendly)
- Unlimited traffic included
Implementation:
- contabo/lib/common.sh: OAuth2 token flow, instance provisioning, SSH access
- contabo/claude.sh: Claude Code deployment (implemented)
- contabo/aider.sh: Aider deployment (implemented)
- contabo/openclaw.sh: OpenClaw deployment (implemented)
- manifest.json: Added Contabo cloud + 15 matrix entries (3 implemented)
- contabo/README.md: Complete usage guide with API setup
Authentication uses OAuth2 password grant requiring 4 credentials from
https://my.contabo.com/api/details: Client ID, Client Secret, API User,
API Password.
Agent: cloud-scout-1
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>