Server definition
- Hash
- sha256:9c15c12eef96e0a5c687f75306a3b6479dff16bed5bf6a21749d3eded4eec602
- What it is
- What a remote MCP server returned when asked what it offers: 81 tools
The blob, as servednamed by its sha256
{
"instructions": "redu.cloud — an EU-hosted, agent-native cloud. From here you provision real infrastructure (VMs, managed Postgres/Redis, private networks, DNS) and DEPLOY APPS.\n\nSTART: call whoami to confirm your credential actually works (real round-trip).\n\nDEPLOY AN APP (the main flow — call deploy_overview for the full recipe):\nCODEX NOTE: for 'deploy/host this website/app/repo' requests, DO NOT jump straight to create_instance. Use check_deploy_prerequisites -> plan_deploy -> deploy_app/deploy_compose so the user sees the plan + cost first. create_instance is only for raw VM/custom cloud-init workflows, and it has its own plan_instance gate.\nDRIVE MODE — GUIDED (default: show the plan + COST and WAIT for the user's explicit 'go' before creating ANY billed resource) or YOLO (auto-proceed end to end, no stopping). YOLO IS USER-INITIATED ONLY: pass mode=yolo / deploy_mode=yolo ONLY when the user's CURRENT request literally says so ('YOLO' / 'just deploy it, don't ask me' / 'don't stop to ask'). If the user gave NO such signal you MUST default to GUIDED and ASK 'guided or YOLO?' — you may NOT assume YOLO. 'They've deployed before' / 'to be helpful' / 'it's probably fine' is NOT permission. Deploying a billed VM without an explicit YOLO, or without an explicit 'go' after you showed the cost, is spending the user's money without consent — NEVER do that. Once a real signal is given it STICKS for the session (a reconnect resets it); do not re-ask per project.\n0. Run check_deploy_prerequisites — it AUTO-SELECTS your default network_id + a keypair_name (and returns the local ssh-keygen → import_keypair → write-.env recipe if you have no keypair). Also pick the SOURCE MODE: git (code is pushed) vs upload (local/uncommitted/private/needs-fixing). mode=yolo auto-proceeds; guided asks.\nPICK THE RIGHT SURFACE: many repos have SEVERAL runnable parts. ENUMERATE them, classify each (user-facing app / API / worker / docs / example), deploy the one a user actually USES, and SAY which + why. THE TEST: open that URL — would the user USE the product there, or just READ ABOUT it / hit a JSON endpoint? DEMOTE (never the default pick, even though a user CAN open them): documentation & marketing/landing sites, an API/backend-only service with no UI, SDKs/libraries/CLIs, examples/ — user-openable but NOT the product. For a LIBRARY / SDK / FRAMEWORK / agent-toolkit repo (no single app), deploy the sandbox / playground / demo that lets someone TRY it in a browser — NOT its docs and NOT its headless server. A ROOT/top-level Dockerfile is NOT automatically the surface: if it builds a headless server/library while a SUBDIR ships a browser GUI/desktop, deploy the GUI — a noVNC/VNC/web-desktop is a PRIME visual target (e.g. cua: the ROOT Dockerfile builds the headless python computer-server — deploy libs/xfce's noVNC desktop on port 6901 instead, NOT the root Dockerfile / docs / computer-server). Unsure? Guided: ask which. YOLO: pick the most user-useful surface and SAY which + why. A URL that loads but only shows docs or raw JSON = wrong pick; reconsider before deploying. For a MULTI-PART repo, pick DETERMINISTICALLY: enumerate every surface (find all Dockerfiles/Containerfiles, OPEN each, classify by EXPOSE+CMD) and call select_surface — it ranks them by fixed rules; GUIDED: present its ranked list and let the USER pick; YOLO: deploy its TOP-ranked surface.\nSURFACE TYPES THAT GET MISSED (check these BEFORE settling on a pick): (a) a containerized GUI/DESKTOP streamed to the browser — noVNC / VNC / websockify / XFCE / KasmVNC / Xvfb — IS a deployable web surface and is often the BEST visual one; when enumerating, look for these too, NOT only web frameworks (gradio/fastapi/react/next). A Dockerfile that builds a desktop+noVNC is a runnable surface even though it has no 'web framework'. (b) SELF-CONTAINED beats needs-your-own-keys (HARD RULE, general to ANY repo): a standalone browser GUI/desktop/noVNC container ALWAYS outranks a playground/demo that is INERT until the visitor supplies their own API key or an external backend/sandbox — you MUST pick the standalone GUI and MUST NOT pick the keys-required one ('loads but does nothing' = a failed deploy). Concretely for cua: libs/xfce is a noVNC DESKTOP that works standalone (the visual hero, port 6901); the Gradio agent playground only acts if the visitor brings an LLM key + a cloud sandbox, so despite being a 'playground' it is the WORSE pick — choose the xfce desktop. THREE TRAPS that pick the WRONG surface (a real agent fell into ALL THREE on cua — avoid them): (1) NEVER classify a surface by its DIRECTORY NAME — run `find . -name Dockerfile -o -name Containerfile`, OPEN each, and judge by its EXPOSE + CMD/start script. A dir whose name sounds like low-level plumbing (xfce, kasm, vnc, sandbox, base, runtime, driver) but whose Dockerfile EXPOSEs a web port and runs noVNC/a desktop/a server is a TOP surface — opening libs/xfce/Dockerfile (EXPOSE 6901 + start-novnc.sh + supervisord) makes it obvious. (2) DON'T STOP at the first browsable surface — especially one the README or a scripts/playground.* brands as 'the playground' / 'try it here'; that branding sells the project's STORY, not what deploys SELF-CONTAINED on redu. Enumerate them ALL, THEN rank. (3) DON'T over-trust a Python/docs-centric layout — a browser-GUI subdir (often a non-Python dir treated as 'infra') outranks the SDK packages the docs center on.\nMULTIPLE URLS PER DEPLOYMENT: a deployment is NOT limited to one URL. An app whose parts need SEPARATE public domains (a frontend that calls its API on another domain — Lago, Supabase, …) is FULLY supported: deploy it with deploy_compose and pass expose[] — each service gets its OWN *.redu.cloud URL (up to 10), exactly like an instance with multiple DNS entries. NEVER reject such an app as a poor fit because 'redu gives one URL per deployment' — that limit does not exist; expose each surface and wire the cross-surface URL env (LAGO_API_URL, NEXT_PUBLIC_API_URL, …) to those URLs.\n1. plan_deploy — YOU classify the repo → a sized, costed, quota-checked plan. ALWAYS do this first, and ALWAYS write its redu-deploy-plan.md report to disk — in guided AND yolo (yolo skips the approval pause, never the plan file). Note the detected PORT.\n2. Show the plan + cost and get ONE approval (guided) — the trust moment and the ONLY gate. NEVER skip straight to deploy_app, but once approved, proceed with the plan's defaults WITHOUT asking more sub-questions (size, name, network, security group, DB mode are auto-selected). In yolo mode, proceed without stopping to ask at all.\n3. deploy_app — pass name, source, the detected PORT (it health-probes that exact port — a static nginx app is :80, not the 3000 default), network_id, keypair_name. Builds the container ON a VM and serves it at https://<name>.redu.cloud.\n • GIT source: pass `repo` (public; private repos need git_token).\n • UPLOAD source (no git, no PAT): call prepare_upload, run its tar+curl commands locally, pass the returned source_token — for uncommitted code, a fixed clone of a repo you don't own, or private code.\n • No Dockerfile in the source? plan_deploy can generate one → pass it as dockerfile_content (or include it in the upload).\n4. Poll list_deployments / get_deployment until status='ready'. On failure, get_deployment(id) returns build_log.\n\nFor a database: just pass deploy_app `database:'managed'` (a dedicated managed datastore VM, auto-provisioned + wired, data persists across redeploys) or `database:'single_vm'` (Postgres on the app VM, cheapest) — both auto-inject env, no standalone create_database call needed. Advanced standalone datastore creation still works, but run plan_managed_datastore before create_database/create_relational_database/create_redis/create_clickhouse. ANALYTICS APPS (Plausible, PostHog, Langfuse, Matomo, SigNoz, anything storing every event) need a SECOND datastore next to the relational one: pass plan_deploy needs_clickhouse:true so the events tier is sized, quota-checked and PRICED, then create_clickhouse it and pass CLICKHOUSE_* env to the deploy. Skipping it under-quotes the user by a whole datastore and nothing errors.\nWORDPRESS/WOOCOMMERCE CLUSTERS: if the user says 'deploy as cluster' and the repo is WordPress/WooCommerce (wp-config.php, wp-content, WooCommerce plugin, wordpress/woocommerce compose image), this is NOT generic stateless autoscaling. The default real path is managed MariaDB/MySQL PLUS a Redu media space mounted at wp-content/uploads. Run list_media_spaces; if none is suitable, include create_media_space:true in the plan-approved deploy. Deploy with database:'managed', db_engine:'mariadb' (or mysql), app_profile:'wordpress'/'woocommerce', cluster_target:true, cluster_media_mode:'media_space', and either media_space_id or create_media_space:true. Then upgrade_to_cluster with cluster_media_mode:'media_space' and the same media_space_id/create_media_space choice. Local uploads are single-VM only and redu-webservice will refuse the cluster.\n\nMULTI-VM: most apps fit on ONE VM (plus a managed DB when needed). You CAN split a heavy compose across VMs (app tier on one, data tier on another, every datastore DSN pointing at the data VM's PRIVATE IP), but NEVER split a SELF-CLUSTERING data tier (ClickHouse ON CLUSTER, Kafka, Elasticsearch, Cassandra) across VMs: those nodes coordinate by identity (a node must recognize ITSELF as a cluster member) and break across the private network even when the network itself is fine. Keep such a cluster on ONE VM, or use a managed datastore (create_clickhouse / create_redis / create_database). Full playbook in deploy_overview.\nVERIFY FUNCTIONALLY: after a deploy, confirm the app actually works (serves real traffic, data flows end to end, write a row and read it back), not just a /health 200. Health endpoints often report ok while the schema or data layer has silently failed.\n\nHIGH AVAILABILITY: if the user wants the app to survive a machine or host failure, use upgrade_to_cluster with high_availability:true on their EXISTING VM (no rebuild, no redeploy, same URL). That runs 3 members across 3 different physical hosts, rebuilds a destroyed member automatically in 1.5 to 5 minutes depending on how it failed (both ends measured), with the cluster serving on the survivors throughout, and rolls updates one member at a time, so the cluster never drops below 2 of 3 members and the URL never changes (expect a brief blip per member swap, measured at about 2% of requests, not a maintenance window). Say the cost plainly first: 3 members run continuously plus the source VM, versus one VM today. A single VM (or a 1-2 member group) is NOT highly available, so do not describe it as such. Cluster members must start their app on EVERY boot (systemd unit or container restart policy) or a replaced member comes back empty and silently leaves the load balancer - pass startup_command if the image does not already do it.\nHIGH AVAILABILITY FOR THE DATABASE is a SEPARATE decision from the app tier, and clustering the app does nothing for it: a clustered app in front of a one-machine database still goes down when that machine does. Managed Postgres and managed ClickHouse take ha:true (create_database / create_clickhouse, or plan it with plan_managed_datastore ha:true): three machines on three different physical hosts behind a load balancer, surviving the loss of a machine with no manual failover and one unchanged connection string. It runs THREE machines instead of one, so it costs roughly 3x the hourly rate: say that number to the user and get an explicit yes before you pass ha:true, exactly like any other spend. Default is a single machine and that is the right answer for dev, staging and most side projects. MySQL/MariaDB and Redis are single-machine only today - do not tell a user their MySQL is highly available. AN EXISTING DATABASE DOES NOT HAVE TO BE REBUILT: upgrade_to_ha turns a live single-machine Postgres or ClickHouse into a three-machine cluster in place, keeping the data and the same credentials, with the old machine left running and serving throughout. Never tell a user to create a second database and copy the data across by hand. Two things you must say before calling it, on top of the 3x cost: writes made to the old machine during the copy are NOT carried over and cannot be recovered (measured on a real upgrade), and the connection address CHANGES, so their app needs redeploying against the new one and the old machine keeps billing until they delete it.\nADD A REDU FEATURE TO A DEPLOYED APP (e.g. 'add backups to the Supabase I deployed'): call integrate_overview for the pattern. In short: mint a LEAST-PRIVILEGE scoped API key with create_api_key, inject it into the app as an env var, then call the redu REST API from the app. create_api_key REQUIRES the user's explicit approval (a two-step confirm) and reminds them the key's usage is billed to their account. NEVER create a key the user has not accepted.\n\nThis MCP is meant to run in Codex, Claude Code, Claude.ai, and ChatGPT. Codex/Claude Code can run local shell for SSH keygen + upload. Say 'EU-hosted / EU data residency' — never expose the underlying cloud internals.",
"tools": [
{
"description": "Read-only. Fetches EVERY resource on your redu.cloud account (VMs, volumes, private networks, managed databases: Postgres/MySQL/MariaDB/ClickHouse/Redis/Qdrant, load balancers, and DNS access points) and renders ONE Mermaid diagram of the whole account, grouped into per-private-network subgraphs with repetition collapsed into count nodes (e.g. \"App VM x12\"). Returns report_markdown to save as redu_architecture_diagram.md at the repo root, PLUS a redu_md_section that enriches the repo's redu.md with the current account topology, so the NEXT deploy is account-aware: it integrates with services that already exist and picks models/versions/sizes that fit the stack (better decisions, not just avoiding duplicate networks/databases). Nothing is created or changed.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"include_load_balancers": {
"description": "Include load balancers in the map (default true).",
"type": "boolean"
},
"include_managed": {
"description": "Include managed databases (Postgres/MySQL/MariaDB/ClickHouse/Redis/Qdrant) in the map (default true).",
"type": "boolean"
},
"include_volumes": {
"description": "Include block volumes in the map (default true).",
"type": "boolean"
}
},
"type": "object"
},
"name": "architecture_diagram",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"counts": {},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"redu_md_filename": {
"type": "string"
},
"redu_md_section": {
"type": "string"
},
"report_filename": {
"type": "string"
},
"report_markdown": {
"type": "string"
},
"resources": {}
},
"type": "object"
}
},
{
"description": "Attaches an EXISTING block volume to a running instance as an extra disk. This is how you finish a RESTORE after a VM is gone: restore_backup (volumeName) rebuilds the data into a new volume, then attach_volume puts that volume on a replacement instance — then SSH in (get_ssh_command) and mount it (`lsblk` to find it, usually /dev/vdb, then `mount /dev/vdb /mnt/...`). The volume must be 'available' (not already in-use — detach_volume it first) and in the same project. Attaching does NOT mount or boot from it: the instance keeps booting from its own root disk, the volume shows up as an extra block device. Poll list_volumes until status is 'in-use'.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"instance_id": {
"description": "Instance to attach it to / detach it from — from list_instances (id).",
"minLength": 1,
"type": "string"
},
"volume_id": {
"description": "Volume to attach/detach — from list_volumes (id). For a recovery, this is the volume restore_backup created (restore_backup with volumeName makes a NEW volume from a backup).",
"minLength": 1,
"type": "string"
}
},
"required": [
"volume_id",
"instance_id"
],
"type": "object"
},
"name": "attach_volume",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Checks if your account has everything needed to run autonomous coding agents. Returns a checklist: API key validity, private network, SSH keypair, and billing. Safe to call anytime — read-only. Call this first before create_controller.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"github_pat": {
"description": "GitHub Personal Access Token to validate (repo + workflow scopes required)",
"type": "string"
}
},
"type": "object"
},
"name": "check_agent_prerequisites",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"all_ok": {
"type": "boolean"
},
"checks": {}
},
"type": "object"
}
},
{
"description": "The deploy ENTRY GATE — run this first when a user wants to deploy. Verifies the account is ready and AUTO-SELECTS the network_id + keypair_name to pass to deploy_app (so you never hunt for them): (1) account reachable + quota, (2) a private network (picks your default — every account has one), (3) an SSH keypair (if you have none, returns ssh_key_recipe: the exact local ssh-keygen → import_keypair → write SSH_KEY_LOCATION-to-.env steps — run them, in yolo mode without asking). Returns { ready, network_id, keypair_name, missing, ssh_key_recipe }.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"description": "guided = confirm each choice with the user; yolo = auto-proceed with sensible defaults (auto-select the default network, auto-create a keypair). STICKY for the session once set — OMIT it on later re-checks to KEEP the chosen mode (do NOT re-pass 'guided' on a yolo session; passing it flips the session back to guided). Defaults to the session's sticky mode, or guided if never set.",
"enum": [
"guided",
"yolo"
],
"type": "string"
}
},
"type": "object"
},
"name": "check_deploy_prerequisites",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"account_ok": {
"type": "boolean"
},
"keypair_name": {
"type": [
"string",
"null"
]
},
"missing": {
"items": {
"type": "string"
},
"type": "array"
},
"mode": {
"type": "string"
},
"network_id": {
"type": [
"string",
"null"
]
},
"next": {
"type": "string"
},
"platform_facts": {
"items": {
"type": "string"
},
"type": "array"
},
"quota": {},
"ready": {
"type": "boolean"
},
"ssh_key_recipe": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Mints a NEW least-privilege redu API key for a deployed app or automation to use (e.g. to add a backup feature). GUARDRAIL: the first call does NOT create anything. It returns a confirmation the user must approve, because a key is a long-lived credential that acts on the account and ANY usage of it is BILLED to the account owner. Show the user the name, the exact scopes, and the billing notice, get their explicit approval, then call again with confirm:true. This is required even in yolo/auto mode. The secret is returned ONCE.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"confirm": {
"default": false,
"description": "Set true ONLY after the user has explicitly approved (see the confirmation the first call returns). Leave unset to get the approval prompt first. Do NOT set it yourself.",
"type": "boolean"
},
"expires_in_days": {
"description": "Optional: auto-expire the key after N days.",
"exclusiveMinimum": 0,
"maximum": 365,
"type": "integer"
},
"name": {
"description": "A recognizable name for the key, e.g. 'supabase-backups'.",
"maxLength": 100,
"minLength": 1,
"type": "string"
},
"scopes": {
"description": "LEAST-PRIVILEGE scopes the key may use, e.g. ['instance:read','backup:read','backup:create','backup:restore']. Grant only what the feature needs. Discover the catalog via GET /v1/capabilities (grammar '<resource>:<action>'). billing/subscription/payment/admin are human-only and cannot be granted.",
"items": {
"minLength": 1,
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"required": [
"name",
"scopes"
],
"type": "object"
},
"name": "create_api_key",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"billing_notice": {
"type": "string"
},
"error": {
"type": "boolean"
},
"expires_at": {},
"key": {
"type": "string"
},
"mode": {
"type": "string"
},
"name": {
"type": "string"
},
"needs_confirmation": {
"type": "boolean"
},
"next": {
"type": "string"
},
"scopes": {},
"validation": {},
"warning": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Creates a backup, by volumeId or by instanceId (the instance's volume is resolved for you). The backup is held independently of the instance, so it survives the VM. Poll list_backups until the new backup is available.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"backupName": {
"description": "A name for the backup.",
"minLength": 1,
"type": "string"
},
"instanceId": {
"description": "ID of the instance to back up — its bootable (else first) attached volume is resolved automatically, so no volume:list scope is needed. Provide this OR volumeId.",
"minLength": 1,
"type": "string"
},
"volumeId": {
"description": "ID of the volume to back up (see list_volumes). Provide this OR instanceId.",
"minLength": 1,
"type": "string"
}
},
"required": [
"backupName"
],
"type": "object"
},
"name": "create_backup",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Provisions a managed ClickHouse database (OLAP / columnar analytics engine, Apache-2.0) on a dedicated VM on your private network — its OWN resource, NOT a relational database. Requires a recent plan_managed_datastore. Use it for analytics / observability workloads that need a column store (PostHog, Langfuse, event analytics, time-series). It is PRIVATE — reachable only from another instance on the same private network, via the DB's internal/private IP on the ClickHouse HTTP port 8123 (CLICKHOUSE_HOST/PORT/USER/PASSWORD/DB env, http://host:8123). Get the ids from plan_managed_datastore/list_flavors (use m1.small+ — ClickHouse needs >=2GB RAM), list_private_networks, list_keypairs. Provisioning takes ~5 min; poll list_clickhouse_databases until status='ready'. HIGH AVAILABILITY: pass ha:true to get THREE machines on three different physical hosts behind a load balancer instead of one: all three take reads and writes, so losing a machine costs no failover and no write pause, and the replacement refills itself from the survivors before it serves again. It costs about 3x the hourly rate (three machines instead of one) and provisions more slowly. Default is a single machine; show the user the price difference and get an explicit yes before turning HA on.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"db_name": {
"default": "analytics",
"description": "The database to create (default 'analytics').",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"db_password": {
"description": "Password for the db user (letters/numbers/_/- only). Auto-generated and returned once if omitted.",
"maxLength": 128,
"minLength": 8,
"type": "string"
},
"db_user": {
"default": "appuser",
"description": "The database user (default 'appuser').",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"flavor_id": {
"description": "Instance size for the ClickHouse VM — from list_flavors. ClickHouse is RAM-hungry; pick m1.small (2GB) or larger.",
"minLength": 1,
"type": "string"
},
"ha": {
"default": false,
"description": "High availability. FALSE (the default) = ONE machine: right for dev, staging and most side projects, but if that machine dies the database is down until it is rebuilt. TRUE = THREE machines on three different physical hosts behind a load balancer, so losing a machine does not lose the database and nobody has to fail it over by hand. COST, say this to the user BEFORE you pass true: three machines instead of one at the size you picked, so roughly 3x the hourly rate of the same non-HA ClickHouse (plus a load balancer). Get an explicit yes on that number: a user who discovers HA from their bill was not asked. ClickHouse gets the easy version of this: all three machines accept reads AND writes, so there is no promotion step and no write pause. A machine dying is dropped from the load balancer, the other two carry on, and its replacement refills itself from them and is only allowed back once it has actually caught up. Queries that were running on the machine that died fail once and need a retry; new queries never see it. ⛔ ONE CONSTRAINT YOU MUST TELL THE USER BEFORE THEY CHOOSE THIS: on three machines every table has to use a REPLICATED engine. `ENGINE = MergeTree` is refused outright (ClickHouse error 56, UNKNOWN_STORAGE) - write `ENGINE = ReplicatedMergeTree` instead, with no arguments. It is refused rather than accepted because a plain MergeTree on three machines replicates its schema and NOT its rows, which would leave two of the three answering the user's queries with an empty table and no error. So anything that ships plain-MergeTree migrations - Plausible, PostHog and Langfuse all do - cannot run on the three-machine version without changing its migrations, and should use the single machine. HA takes longer to provision than a single machine, because the machines are built one at a time on purpose so they land on different hosts. HA is not a backup: it replicates your mistakes too, so keep backups on either setting.",
"type": "boolean"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"keypair_name": {
"description": "SSH keypair name — from list_keypairs.",
"minLength": 1,
"type": "string"
},
"name": {
"description": "Name for the managed ClickHouse database (lowercase letters, numbers, hyphens).",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Private network id — from list_private_networks. The DB is reachable only from this network.",
"minLength": 1,
"type": "string"
},
"security_group_names": {
"default": [
"default"
],
"items": {
"type": "string"
},
"type": "array"
},
"superuser": {
"description": "Grant the db user global rights (CURRENT GRANTS(ALL ON *.*)) on top of its own database - safe here, it is a dedicated single-tenant VM. SET IT for any app whose own MIGRATIONS create ClickHouse objects outside their database, above all USER-DEFINED FUNCTIONS. Measured 2026-08-08: a stock Plausible deploy crash-loops forever on `Code: 497 ... necessary to have the grant CREATE FUNCTION ON *.*` while it creates its 12 acquisition_channel functions, and no amount of retrying fixes it - the database has to be created with this flag. PostHog and other analytics apps that ship SQL UDFs are the same shape. Without it the app deploys, the container restarts, and the only symptom is a migration stack trace.",
"type": "boolean"
}
},
"required": [
"name",
"flavor_id",
"network_id",
"keypair_name"
],
"type": "object"
},
"name": "create_clickhouse",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Creates a self-configuring controller VM on redu.cloud. Auto-discovers your network, Ubuntu image, and flavor. Generates and uploads an SSH keypair if you don't have one. The VM bootstraps itself — no SSH or VPN needed. After ~7 minutes, authorize the fleet (run the authorize.sh one-liner it returns) then call trigger_agent_batch.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"github_pat": {
"description": "GitHub Personal Access Token (repo + workflow scopes)",
"minLength": 1,
"type": "string"
},
"repo": {
"description": "GitHub repo to run agents on, e.g. 'owner/repo'",
"minLength": 1,
"type": "string"
},
"worker_count": {
"default": 3,
"description": "Number of worker VMs to provision (default 3)",
"maximum": 5,
"minimum": 1,
"type": "integer"
}
},
"required": [
"github_pat",
"repo"
],
"type": "object"
},
"name": "create_controller",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"authorize_command": {
"type": "string"
},
"controller_host": {
"type": "string"
},
"dns": {
"type": "string"
},
"instance_id": {
"type": "string"
},
"log_token": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"items": {},
"type": "array"
},
"ssh_key_name": {
"type": "string"
},
"ssh_private_key": {
"type": "string"
},
"ssh_user": {
"type": "string"
},
"status": {
"type": "string"
},
"trigger_token": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Provisions a managed PostgreSQL database on a dedicated VM on your private network. Requires a recent plan_managed_datastore. For app deployments, prefer deploy_app database:'managed' so plan_deploy includes and wires the DB automatically. It is PRIVATE — reachable only from another instance on the same private network, via the DB's internal/private IP (not a public address). Get the ids from plan_managed_datastore/list_flavors/list_private_networks/list_keypairs. Provisioning takes ~5 min; poll list_databases until status='ready', then the connection details (private_ip, port 5432, db_name, db_user) are populated. HIGH AVAILABILITY: pass ha:true to get THREE machines on three different physical hosts behind a load balancer with automatic failover instead of one machine: the database survives losing a machine, and nobody has to fail it over by hand. It costs about 3x the hourly rate (three machines instead of one), it provisions more slowly, and one connection string still serves the whole thing. Default is a single machine; show the user the price difference and get an explicit yes before turning HA on.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"db_name": {
"default": "app",
"description": "The database to create (default 'app').",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"db_password": {
"description": "Password for the db user. Auto-generated and returned once if omitted.",
"maxLength": 128,
"minLength": 8,
"type": "string"
},
"db_user": {
"default": "appuser",
"description": "The database user (default 'appuser').",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"extensions": {
"description": "Postgres extensions to pre-install at provision time (allowlisted): 'pgvector'/'vector' (embeddings), 'postgis' (geo), 'pgaudit', 'pg_trgm', 'pgcrypto', 'hstore', 'uuid-ossp', 'citext', 'ltree', 'pg_stat_statements', 'btree_gin', 'btree_gist'. Saves SSHing in to run CREATE EXTENSION; needed by apps like langfuse/lantern (pgvector).",
"items": {
"enum": [
"vector",
"pgvector",
"postgis",
"pgaudit",
"pg_stat_statements",
"hstore",
"pg_trgm",
"uuid-ossp",
"citext",
"pgcrypto",
"ltree",
"btree_gin",
"btree_gist"
],
"type": "string"
},
"type": "array"
},
"flavor_id": {
"description": "Instance size for the DB VM — from list_flavors.",
"minLength": 1,
"type": "string"
},
"ha": {
"default": false,
"description": "High availability. FALSE (the default) = ONE machine: right for dev, staging and most side projects, but if that machine dies the database is down until it is rebuilt. TRUE = THREE machines on three different physical hosts behind a load balancer, so losing a machine does not lose the database and nobody has to fail it over by hand. COST, say this to the user BEFORE you pass true: three machines instead of one at the size you picked, so roughly 3x the hourly rate of the same non-HA Postgres (plus a load balancer). Get an explicit yes on that number: a user who discovers HA from their bill was not asked. What it does and does NOT do for Postgres, state this honestly: only one of the three machines takes writes, so losing either of the other two changes nothing at all, and reads keep working through any single loss. Losing the WRITE machine pauses writes for roughly 30 seconds while one of the others is promoted, and transactions that were open at that moment fail and have to be retried by the app. That is true of every Postgres HA setup, hosted or not, so never promise zero downtime. Say 'no lost writes, one pause of about half a minute'. HA takes longer to provision than a single machine, because the machines are built one at a time on purpose so they land on different hosts. HA is not a backup: it replicates your mistakes too, so keep backups on either setting.",
"type": "boolean"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"keypair_name": {
"description": "SSH keypair name — from list_keypairs (for operating the DB VM).",
"minLength": 1,
"type": "string"
},
"name": {
"description": "Name for the managed database (lowercase letters, numbers, hyphens).",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Private network id — from list_private_networks. The DB is reachable only from this network.",
"minLength": 1,
"type": "string"
},
"security_group_names": {
"default": [
"default"
],
"items": {
"type": "string"
},
"type": "array"
},
"superuser": {
"description": "Grant the db user SUPERUSER — safe here (a dedicated single-tenant DB VM). Use when the app's own migrations must CREATE EXTENSION or alter roles.",
"type": "boolean"
},
"version": {
"default": "16",
"description": "Postgres major version (default 16).",
"enum": [
"16",
"15",
"14"
],
"type": "string"
}
},
"required": [
"name",
"flavor_id",
"network_id",
"keypair_name"
],
"type": "object"
},
"name": "create_database",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Creates a raw compute instance for custom OS/cloud-init workflows. For app/source deployments, prefer check_deploy_prerequisites -> plan_deploy -> deploy_app/deploy_compose; do not use create_instance as a shortcut around the deployment plan. Requires a recent plan_instance. For reduOS, set cloud_init_template='reduos' and flavor m1.xlarge (16GB). SSH keypair must exist. For a public web app on a raw VM, pass dns_entries (gives a .redu.cloud URL through redu's automatic public proxy). The origin app should listen on dns_entries[].port; do not install nginx on the VM unless the app itself needs it, because redu.cloud already creates the public Nginx proxy host.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"boot_volume_id": {
"description": "DISASTER RECOVERY: boot this new VM from an EXISTING volume (one restore_backup just created) instead of a fresh image, so the machine comes back with the restored filesystem AS its root disk. The volume must be 'available' (detached) and bootable. image_id is ignored when this is set. Without it, a restored disk can only be attached as a SECOND device and the VM still boots off a blank root disk.",
"format": "uuid",
"type": "string"
},
"cloud_init_template": {
"enum": [
"reduos"
],
"type": "string"
},
"dname": {
"default": "",
"type": "string"
},
"dns_entries": {
"default": [],
"description": "Public DNS/proxy entries to create automatically. Use [{ dname:'', port:8080 }] to generate a redu.cloud URL pointing at an origin process on port 8080.",
"items": {
"additionalProperties": false,
"properties": {
"dname": {
"default": "",
"description": "Optional custom domain; leave empty to generate a redu.cloud DNS name.",
"type": "string"
},
"port": {
"description": "Origin HTTP port on the instance that the redu cloud proxy should forward to.",
"maximum": 65535,
"minimum": 1,
"type": "integer"
}
},
"type": "object"
},
"type": "array"
},
"flavor_id": {
"minLength": 1,
"type": "string"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"imageName": {
"default": "",
"type": "string"
},
"image_id": {
"minLength": 1,
"type": "string"
},
"keypair_name": {
"minLength": 1,
"type": "string"
},
"name": {
"minLength": 1,
"type": "string"
},
"network_id": {
"minLength": 1,
"type": "string"
},
"port": {
"default": 3000,
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"security_group_names": {
"default": [
"default"
],
"items": {
"type": "string"
},
"type": "array"
},
"user_data": {
"type": "string"
},
"volume": {
"additionalProperties": false,
"properties": {
"name": {
"type": "string"
},
"size": {
"exclusiveMinimum": 0,
"type": "number"
}
},
"required": [
"name",
"size"
],
"type": "object"
}
},
"required": [
"name",
"image_id",
"flavor_id",
"keypair_name",
"network_id"
],
"type": "object"
},
"name": "create_instance",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Creates a private Redu media space: a small NFS VM backed by a persistent volume, intended for WordPress/WooCommerce wp-content/uploads in autoscaling clusters. Prefer letting deploy_app/deploy_compose create this by passing create_media_space:true after plan_deploy approval; use this tool when manually preparing or reusing shared uploads storage.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"flavor_id": {
"default": "2",
"description": "Small VM flavor id for the NFS media server. m1.small is enough for most WordPress uploads.",
"minLength": 1,
"type": "string"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"keypair_name": {
"description": "Existing SSH keypair name.",
"minLength": 1,
"type": "string"
},
"mount_path": {
"default": "/mnt/redu-media/uploads",
"description": "Host mount path that app VMs will use before binding into /var/www/html/wp-content/uploads.",
"type": "string"
},
"name": {
"description": "Name for the media space, e.g. myshop-media.",
"maxLength": 63,
"minLength": 1,
"pattern": "^[a-z0-9-]+$",
"type": "string"
},
"network_id": {
"description": "Private network id. Use the same network as the WordPress app and managed DB.",
"minLength": 1,
"type": "string"
},
"security_group_names": {
"default": [
"default"
],
"description": "Security groups for the media VM. Redu opens NFS 2049 from private networks only.",
"items": {
"type": "string"
},
"type": "array"
},
"size_gb": {
"default": 20,
"description": "Persistent media volume size in GB.",
"maximum": 2048,
"minimum": 1,
"type": "integer"
}
},
"required": [
"name",
"keypair_name"
],
"type": "object"
},
"name": "create_media_space",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Provisions a managed Redis instance on a dedicated VM on your private network. Requires a recent plan_managed_datastore. It is PRIVATE — reachable only from another instance on the same private network, via its internal/private IP on port 6379 (not a public address). AUTH (requirepass) is always enabled. Get the ids from plan_managed_datastore/list_flavors, list_private_networks (or check_deploy_prerequisites), list_keypairs — use the SAME network_id as the app that will connect. Provisioning takes ~5 min; poll list_redis until status='ready', then the connection details (private_ip, port 6379) are populated. Wire an app with REDIS_URL=redis://:<password>@<private_ip>:6379 (pass it via deploy_app env).",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"flavor_id": {
"description": "Instance size for the Redis VM — from list_flavors. m1.small (2GB) is plenty for most caches/queues.",
"minLength": 1,
"type": "string"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"keypair_name": {
"description": "SSH keypair name — from list_keypairs (for operating the Redis VM).",
"minLength": 1,
"type": "string"
},
"name": {
"description": "Name for the managed Redis instance (lowercase letters, numbers, hyphens).",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Private network id — from list_private_networks (or check_deploy_prerequisites). Redis is reachable only from this network; use the SAME network as the app that will connect.",
"minLength": 1,
"type": "string"
},
"password": {
"description": "Redis AUTH password (requirepass). Auto-generated and returned once if omitted.",
"maxLength": 128,
"minLength": 8,
"type": "string"
},
"security_group_names": {
"default": [
"default"
],
"items": {
"type": "string"
},
"type": "array"
},
"version": {
"default": "7",
"description": "Redis major version (default '7').",
"enum": [
"7",
"6"
],
"type": "string"
}
},
"required": [
"name",
"flavor_id",
"network_id",
"keypair_name"
],
"type": "object"
},
"name": "create_redis",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Provisions a managed MySQL (or MariaDB) database on a dedicated VM on your private network — the relational-database resource (use this instead of create_database when the app needs MySQL/MariaDB, e.g. WordPress, NextCloud, Matomo, many PHP/LAMP apps). Requires a recent plan_managed_datastore. For app deployments, prefer deploy_app database:'managed' with db_engine mysql/mariadb so plan_deploy includes and wires the DB automatically. It is PRIVATE — reachable only from another instance on the same private network, via the DB's internal/private IP (port 3306), not a public address. Get the ids from plan_managed_datastore/list_flavors/list_private_networks/list_keypairs. Provisioning takes ~5 min; poll list_relational_databases until status='ready', then the connection details (private_ip, port 3306, db_name, db_user) are populated. MySQL is created with mysql_native_password auth so older clients/apps connect cleanly. (ClickHouse is a separate resource — use create_clickhouse / list_clickhouse_databases.)",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"db_name": {
"default": "app",
"description": "The database to create (default 'app').",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"db_password": {
"description": "Password for the db user (letters/numbers/_/- only). Auto-generated and returned once if omitted.",
"maxLength": 128,
"minLength": 8,
"type": "string"
},
"db_user": {
"default": "appuser",
"description": "The database user (default 'appuser').",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"engine": {
"default": "mysql",
"description": "Database engine: 'mysql' (default) or 'mariadb'. Both speak the MySQL protocol (mysql:// URL). Pick the one your app/stack uses. (For analytics/column-store needs use create_clickhouse instead — ClickHouse is not a relational DB.)",
"enum": [
"mysql",
"mariadb"
],
"type": "string"
},
"flavor_id": {
"description": "Instance size for the DB VM — from list_flavors.",
"minLength": 1,
"type": "string"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"keypair_name": {
"description": "SSH keypair name — from list_keypairs (for operating the DB VM).",
"minLength": 1,
"type": "string"
},
"name": {
"description": "Name for the managed database (lowercase letters, numbers, hyphens).",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Private network id — from list_private_networks. The DB is reachable only from this network.",
"minLength": 1,
"type": "string"
},
"security_group_names": {
"default": [
"default"
],
"items": {
"type": "string"
},
"type": "array"
},
"version": {
"description": "Engine version (e.g. mysql '8.0', mariadb '11.4'). Defaults to the engine's current major if omitted.",
"type": "string"
}
},
"required": [
"name",
"flavor_id",
"network_id",
"keypair_name"
],
"type": "object"
},
"name": "create_relational_database",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Creates a snapshot of a running instance.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"instanceId": {
"minLength": 1,
"type": "string"
},
"snapshotName": {
"minLength": 1,
"type": "string"
}
},
"required": [
"instanceId",
"snapshotName"
],
"type": "object"
},
"name": "create_snapshot",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Creates a block storage volume.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"name": {
"minLength": 1,
"type": "string"
},
"size": {
"exclusiveMinimum": 0,
"maximum": 10000,
"type": "integer"
}
},
"required": [
"name",
"size"
],
"type": "object"
},
"name": "create_volume",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Deletes a backup by ID.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_backup",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deletes a managed ClickHouse database and its underlying VM. Pass the numeric id from list_clickhouse_databases. This cannot be undone.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_clickhouse",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deletes an autoscaling cluster — tears down the WHOLE cluster: every autoscaled EXTRA member VM, the load balancer, the pool, the cluster security group, and the cluster's *.redu.cloud proxy host. Your SOURCE (hero) VM is NOT part of the stack and is NOT deleted — it was the always-on baseline member but it is your own VM; delete_instance it separately if you no longer need it. Pass stack_name + stack_id from list_clusters. Deleting extra members individually does NOT work (the autoscaling group re-creates them under load) — this deletes the group itself. The managed database / external store the cluster used is NOT touched (data safety); delete_database it separately when you are done with the data. Cannot be undone.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"stack_id": {
"description": "Cluster stack id, from list_clusters (id).",
"minLength": 1,
"type": "string"
},
"stack_name": {
"description": "Cluster stack name, from list_clusters (stack_name).",
"minLength": 1,
"type": "string"
}
},
"required": [
"stack_name",
"stack_id"
],
"type": "object"
},
"name": "delete_cluster",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"stack_id": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deletes a managed Postgres database and its underlying VM. Pass the numeric database id from list_databases. This cannot be undone.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_database",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deletes a deployment and its underlying app VM. Pass the numeric id from list_deployments. IMPORTANT: if the deployment used database:'managed', the managed Postgres VM is NOT deleted (data safety) — this tool returns its id so you can delete_database it when you're done with the data. Cannot be undone.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "Deployment id from list_deployments.",
"exclusiveMinimum": 0,
"type": "integer"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_deployment",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "number"
},
"managed_db": {},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Permanently deletes an instance. This cannot be undone.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_instance",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Removes an SSH keypair from your account by name.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"name": {
"description": "Name of the keypair to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"name": "delete_keypair",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"name": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deletes a managed Redis instance and its underlying VM. Pass the numeric id from list_redis. This cannot be undone.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_redis",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deletes a managed MySQL/MariaDB database and its underlying VM. Pass the numeric id from list_relational_databases. This cannot be undone.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_relational_database",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deletes a snapshot by ID.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_snapshot",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Permanently deletes a block storage volume and the data on it. This cannot be undone. The volume must be 'available' (detached) — detach_volume it first, and note you cannot detach a running instance's boot disk. A volume left behind after a restore keeps billing, so delete the ones you no longer need. Any BACKUP the volume was restored from is independent and survives this.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "ID of the resource to delete.",
"minLength": 1,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "delete_volume",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deleted": {
"type": "boolean"
},
"id": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Deploys an app to a VM and exposes it at a public https://<name>-<id>.redu.cloud URL. The container is built ON the VM. PREREQS — run check_deploy_prerequisites first for network_id + keypair_name, then plan_deploy for cost approval. Source can be git repo or prepare_upload source_token. PORT must be the real app listen port. To wire a DB, pass database:'managed' (dedicated managed datastore VM on the same private network, reused on same-name redeploy) or database:'single_vm' for Postgres on the app VM. Choose db_engine ('postgres' default; 'mysql'/'mariadb' for WordPress/Matomo/LAMP, managed only). For WordPress/WooCommerce cluster intent, do not use generic stateless deploy: pass app_profile, cluster_target:true, database:'managed', db_engine:'mariadb' or 'mysql', cluster_media_mode:'media_space', and either media_space_id or create_media_space:true. Redu mounts the media space into wp-content/uploads and refuses unsafe local uploads. Build+provision takes minutes; poll list_deployments/get_deployment.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"app_profile": {
"description": "Detected app profile from source inspection. For WordPress/WooCommerce pass wordpress/woocommerce so cluster deploys enforce managed DB + shared media instead of treating local uploads as stateless.",
"enum": [
"generic",
"wordpress",
"woocommerce"
],
"type": "string"
},
"cluster_media_mode": {
"description": "Record the WordPress media strategy for future clustering. For real WordPress clusters use media_space by default: Redu mounts a shared uploads filesystem into every member. local_uploads is single-VM only and upgrade_to_cluster will refuse it for WordPress.",
"enum": [
"local_uploads",
"central_media_origin",
"media_space"
],
"type": "string"
},
"cluster_target": {
"description": "Set TRUE when the user asked to deploy this app as an autoscaling cluster. For WordPress/WooCommerce this makes the backend require managed MySQL/MariaDB plus media_space/central_media_origin before deployment can proceed.",
"type": "boolean"
},
"context": {
"description": "Build-context dir within the source (default: repo root, or `subdir`). Set when the Dockerfile lives in a subfolder but builds from the repo root.",
"type": "string"
},
"create_media_space": {
"description": "For WordPress/WooCommerce cluster_target:true: set TRUE when no suitable media space exists. Redu creates an NFS media VM + persistent volume and mounts it into /var/www/html/wp-content/uploads.",
"type": "boolean"
},
"database": {
"description": "DB wiring (auto-injects the connection env + DATABASE_URL — zero setup): 'single_vm' = Postgres ON the app VM (cheapest, data dies when the VM is replaced; Postgres only); 'managed' = a SEPARATE managed-DB VM auto-provisioned + wired on the same private network (data PERSISTS across app redeploys; reused on a same-name redeploy). Omit (or 'none') for no DB. With 'managed' you do NOT call create_database/create_relational_database — it's done for you. Choose the engine with db_engine.",
"enum": [
"none",
"single_vm",
"managed"
],
"type": "string"
},
"db_engine": {
"description": "Managed-DB engine (default 'postgres'). 'mysql'/'mariadb' provision a managed MySQL/MariaDB VM and wire MYSQL_HOST/MYSQL_PORT/MYSQL_USER/MYSQL_PASSWORD/MYSQL_DATABASE + a mysql:// DATABASE_URL — use for WordPress/Matomo/NextCloud/most PHP-LAMP apps. Requires database:'managed' (single_vm is Postgres only).",
"enum": [
"postgres",
"mysql",
"mariadb"
],
"type": "string"
},
"db_extensions": {
"description": "managed/single_vm Postgres only: extensions to pre-install (pgvector, postgis, pgaudit, pg_trgm, …). Pass when the app needs one — e.g. langfuse/lantern need pgvector — so you don't have to CREATE EXTENSION by hand.",
"items": {
"enum": [
"vector",
"pgvector",
"postgis",
"pgaudit",
"pg_stat_statements",
"hstore",
"pg_trgm",
"uuid-ossp",
"citext",
"pgcrypto",
"ltree",
"btree_gin",
"btree_gist"
],
"type": "string"
},
"type": "array"
},
"db_flavor_id": {
"description": "managed only: VM size for the dedicated Postgres (from list_flavors). Defaults to the app flavor; m1.small is plenty for most. plan_deploy sizes this for you.",
"type": "string"
},
"db_id": {
"description": "Informational link to a managed Postgres (from create_database/list_databases).",
"exclusiveMinimum": 0,
"type": "integer"
},
"db_name": {
"description": "DB name for single_vm/managed (default 'app').",
"type": "string"
},
"db_superuser": {
"description": "managed/single_vm Postgres only: grant the app DB user SUPERUSER (dedicated single-tenant DB VM, so safe). Use when the app's migrations create extensions/roles themselves.",
"type": "boolean"
},
"db_user": {
"description": "DB user for single_vm/managed (default 'appuser').",
"type": "string"
},
"db_version": {
"description": "DB version for single_vm/managed. Postgres: '16'|'15'|'14' (default 16). MySQL: '8.0'. MariaDB: '11.4'|'10.11'. Defaults per engine if omitted.",
"type": "string"
},
"dname": {
"description": "Custom *.redu.cloud subdomain. For a STABLE, KNOWN-AHEAD URL (needed when the app must be told its OWN url — OAuth callbacks, cookie domain, a frontend that calls its API), generate the FULL auto-gen form yourself: `<label>-<8 lowercase letters/digits>.redu.cloud` (e.g. `myapp-7k2m9x4p.redu.cloud`) — that exact form is used VERBATIM — and wire the app's own URL env to that SAME value. A BARE `<label>.redu.cloud` is NOT used as-is: redu appends a random 8-char suffix for uniqueness, so anything wired to the bare name will NOT match the real URL. Omit to auto-generate (then read the real URL from get_deployment).",
"type": "string"
},
"dockerfile": {
"description": "Path within the source to the Dockerfile (e.g. 'scripts/Dockerfile') when it's NOT at the root. Pair with `context` when the Dockerfile is in a subfolder but COPYs from the repo root.",
"type": "string"
},
"dockerfile_content": {
"description": "A Dockerfile to write into the build dir before building — pass the Dockerfile that plan_deploy generated when the repo has none, or to override a broken one. (In upload mode you can instead just include the Dockerfile in the tarball.)",
"type": "string"
},
"env": {
"additionalProperties": {
"type": "string"
},
"description": "Env vars injected into the container at deploy time (e.g. PGHOST/PGPORT/PGUSER/PGPASSWORD/PGDATABASE from a managed Postgres). Never baked into the image.",
"type": "object"
},
"flavor_id": {
"default": "3",
"description": "Instance size — from list_flavors. Default m1.medium (enough RAM to build on the VM). For a RAM-heavy app whose state is in a managed DB (Next.js/cal.com, Rails, JVM), prefer m1.mem16/m1.mem32: full RAM on a lean 40 GB disk, so it's cheaper and snapshots/clusters far faster than the 160 GB m1.xlarge/xlarge-highmem. If you will CLUSTER this app (upgrade_to_cluster), prefer m1.mem16/m1.mem32 regardless of RAM needs — a lean disk makes clustering and every scale-out several times faster (plan_deploy sizes cluster_target onto a mem flavor for you).",
"minLength": 1,
"type": "string"
},
"git_ref": {
"description": "git mode only: branch/tag/commit to deploy (default: the repo's default branch).",
"type": "string"
},
"git_token": {
"description": "git mode only: token to clone a PRIVATE repo. Omit for public repos.",
"type": "string"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"keypair_name": {
"description": "REQUIRED. An EXISTING SSH keypair name — call list_keypairs and reuse one, or import_keypair first.",
"minLength": 1,
"type": "string"
},
"media_mount_path": {
"description": "Host mount path on the app VM/member. Redu mounts this into /var/www/html/wp-content/uploads.",
"type": "string"
},
"media_origin_url": {
"description": "Public base URL where WordPress wp-content/uploads is served when using central_media_origin. Do not put this on the DB VM.",
"format": "uri",
"type": "string"
},
"media_space_flavor_id": {
"description": "Flavor id for the media VM when create_media_space:true. Defaults to m1.small/backend default.",
"minLength": 1,
"type": "string"
},
"media_space_id": {
"description": "Existing Redu media space id to mount at WordPress wp-content/uploads. Get it from list_media_spaces.",
"exclusiveMinimum": 0,
"type": "integer"
},
"media_space_name": {
"description": "Optional media space name when create_media_space:true. Defaults to <deployment-name>-media.",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"media_space_size_gb": {
"description": "Media space data volume size in GB when create_media_space:true (default 20).",
"maximum": 2048,
"minimum": 1,
"type": "integer"
},
"name": {
"description": "Name for the deployment / VM (lowercase letters, numbers, hyphens).",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Existing private network id — from check_deploy_prerequisites (auto-selects your default) or list_private_networks. Optional: if omitted, redu auto-selects your default network.",
"minLength": 1,
"type": "string"
},
"port": {
"default": 3000,
"description": "REQUIRED in practice: the port the app actually listens on inside the container — pass the port plan_deploy detected / the Dockerfile EXPOSE / the framework default. redu probes THIS port for health, so a wrong value fails the deploy. Defaults to 3000 only if omitted (e.g. a static nginx app listens on 80 — pass 80). IGNORED when worker:true.",
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"redis": {
"description": "Redis wiring (auto-injects REDIS_URL/REDIS_HOST/REDIS_PORT/REDIS_PASSWORD — zero setup): 'managed' = a SEPARATE managed Redis VM auto-provisioned + wired on the same private network (data persists across app redeploys; reused on a same-name redeploy). Omit (or 'none') for no Redis. With 'managed' you do NOT call create_redis — it's done for you. Use when the app needs Redis (REDIS_URL / bullmq / celery / sidekiq / cache).",
"enum": [
"none",
"managed"
],
"type": "string"
},
"redis_flavor_id": {
"description": "managed Redis only: the dedicated Redis VM size (from list_flavors). Defaults to the app flavor; m1.small is plenty for a cache.",
"type": "string"
},
"redis_password": {
"description": "managed Redis only: a specific password to set (otherwise auto-generated).",
"type": "string"
},
"redis_version": {
"description": "managed Redis only: version (default '7').",
"enum": [
"7",
"6"
],
"type": "string"
},
"repo": {
"description": "GIT MODE: public git repo URL (https). For a PRIVATE repo also pass git_token. Omit when using source_token (upload mode).",
"format": "uri",
"type": "string"
},
"runtime": {
"description": "Informational: node/python/go/… (e.g. from plan_deploy).",
"type": "string"
},
"security_group_names": {
"default": [
"default"
],
"items": {
"type": "string"
},
"type": "array"
},
"source_token": {
"description": "UPLOAD MODE: token from prepare_upload's curl step — deploys an uploaded tarball of your LOCAL working dir (no git, no PAT). Use this to deploy uncommitted code, a fixed clone of a repo you don't own, or private code. Omit `repo` when set.",
"minLength": 16,
"type": "string"
},
"subdir": {
"description": "Build context within the source (e.g. 'demo-go') when the Containerfile/Dockerfile isn't at the root.",
"type": "string"
},
"worker": {
"description": "HEADLESS WORKER / daemon mode. Deploys a long-running container with NO HTTP server, NO public URL, and NO health probe — ready = the container stays running. The VM still gets a floating IP + SSH stream (so you can SSH in), but nothing is exposed on the web. Use for background workers, a chaos-monkey, an on-infra agent runner, cron/queue consumers. `port`, `dname`, and the injected APP_URL/PUBLIC_URL are ignored. Run plan_deploy with worker:true too.",
"type": "boolean"
}
},
"required": [
"name",
"keypair_name"
],
"type": "object"
},
"name": "deploy_app",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Deploys a MULTI-CONTAINER app — a repo that ships docker-compose.yml / compose.yaml — onto ONE VM via podman-compose, and exposes one or more services at redu.cloud URLs. Use this instead of deploy_app when the repo is a compose stack. Same prereqs + source modes as deploy_app; always run plan_deploy first. PORT is the HOST port for the exposed service. DB: 'compose' uses the stack's own db container; 'managed' provisions a separate managed Postgres/MySQL/MariaDB VM and appends connection env. For WordPress/WooCommerce cluster intent, do not leave the compose db service/local uploads as state: pass app_profile, cluster_target:true, database:'managed', db_engine:'mariadb' or 'mysql', cluster_media_mode:'media_space', and either media_space_id or create_media_space:true. Redu writes an override file that points the WordPress service at managed DB env and mounts the media space into /var/www/html/wp-content/uploads. Poll get_deployment until ready.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"app_profile": {
"description": "Detected app profile from source inspection. For WordPress/WooCommerce pass wordpress/woocommerce so cluster deploys enforce managed DB + shared media instead of treating local uploads as stateless.",
"enum": [
"generic",
"wordpress",
"woocommerce"
],
"type": "string"
},
"cluster_media_mode": {
"description": "Record the WordPress media strategy for future clustering. For real WordPress clusters use media_space by default: Redu mounts a shared uploads filesystem into every member. local_uploads is single-VM only and upgrade_to_cluster will refuse it for WordPress.",
"enum": [
"local_uploads",
"central_media_origin",
"media_space"
],
"type": "string"
},
"cluster_target": {
"description": "Set TRUE when the user asked to deploy this compose app as an autoscaling cluster. For WordPress/WooCommerce this requires managed MySQL/MariaDB plus media_space/central_media_origin.",
"type": "boolean"
},
"compose_engine": {
"description": "Container engine for the compose build (default: the server's choice, usually podman). Set 'docker' to force the docker engine.",
"enum": [
"auto",
"docker",
"podman"
],
"type": "string"
},
"compose_file": {
"description": "Path within the source to the compose file (e.g. 'deploy/docker-compose.yml'). Auto-detected (docker-compose.yml / compose.yaml / …) if omitted.",
"type": "string"
},
"create_media_space": {
"description": "For WordPress/WooCommerce cluster_target:true: set TRUE when no suitable media space exists. Redu creates an NFS media VM + persistent volume and mounts it into /var/www/html/wp-content/uploads.",
"type": "boolean"
},
"database": {
"description": "DB mode: 'compose' (default) = use the compose file's OWN db service (self-contained, nothing extra provisioned); 'single_vm' = Postgres ON the app VM; 'managed' = a SEPARATE managed-PG/MySQL VM. For single_vm/managed the conn env (DATABASE_URL/PG*/MYSQL_*) is APPENDED to the project .env — your compose service must REFERENCE it (e.g. environment: - DATABASE_URL=${DATABASE_URL}) to actually use it; we never modify your compose file. Choose engine with db_engine.",
"enum": [
"compose",
"single_vm",
"managed"
],
"type": "string"
},
"db_engine": {
"description": "managed only: 'postgres' (default) → PG* env; 'mysql'/'mariadb' → MYSQL_* env. mysql/mariadb require database:'managed'.",
"enum": [
"postgres",
"mysql",
"mariadb"
],
"type": "string"
},
"db_extensions": {
"description": "single_vm/managed Postgres: extensions to pre-install (pgvector, postgis, pgaudit, …).",
"items": {
"enum": [
"vector",
"pgvector",
"postgis",
"pgaudit",
"pg_stat_statements",
"hstore",
"pg_trgm",
"uuid-ossp",
"citext",
"pgcrypto",
"ltree",
"btree_gin",
"btree_gist"
],
"type": "string"
},
"type": "array"
},
"db_flavor_id": {
"description": "managed only: the dedicated DB VM size (from list_flavors). Defaults to the app flavor.",
"type": "string"
},
"db_name": {
"description": "DB name for single_vm/managed (default 'app').",
"type": "string"
},
"db_superuser": {
"description": "single_vm/managed Postgres: grant the DB user SUPERUSER (dedicated DB VM, so safe).",
"type": "boolean"
},
"db_user": {
"description": "DB user for single_vm/managed (default 'appuser').",
"type": "string"
},
"db_version": {
"description": "DB version for single_vm/managed (Postgres '16'|'15'|'14'; MySQL '8.0'; MariaDB '11.4').",
"type": "string"
},
"dname": {
"description": "Custom *.redu.cloud subdomain. For a STABLE, KNOWN-AHEAD URL (needed when the app must be told its OWN url — OAuth callbacks, cookie domain, a frontend that calls its API), generate the FULL auto-gen form yourself: `<label>-<8 lowercase letters/digits>.redu.cloud` (e.g. `myapp-7k2m9x4p.redu.cloud`) — that exact form is used VERBATIM — and wire the app's own URL env to that SAME value. A BARE `<label>.redu.cloud` is NOT used as-is: redu appends a random 8-char suffix for uniqueness, so anything wired to the bare name will NOT match the real URL. Omit to auto-generate (then read the real URL from get_deployment).",
"type": "string"
},
"env": {
"additionalProperties": {
"type": "string"
},
"description": "Env vars APPENDED to the compose project's .env (for ${VAR} interpolation). We never rewrite your compose file.",
"type": "object"
},
"expose": {
"description": "MULTI-SURFACE apps: expose several services, each gets its OWN *.redu.cloud URL. The FIRST entry is the primary one redu health-gates (its port overrides `port`). CRITICAL when one surface must KNOW another's URL (a frontend that calls its API — e.g. Lago's front -> API): GENERATE each surface's dname up front as `<label>-<8 lowercase letters/digits>.redu.cloud` (used VERBATIM) and wire the app's cross-surface URL env (LAGO_API_URL, LAGO_FRONT_URL, FRONTEND_URL, NEXT_PUBLIC_API_URL, …) to those EXACT values. Do NOT use a bare `<label>.redu.cloud` — redu appends a random suffix, so config wired to the bare name breaks.",
"items": {
"additionalProperties": false,
"properties": {
"dname": {
"type": "string"
},
"port": {
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"service": {
"type": "string"
}
},
"required": [
"port"
],
"type": "object"
},
"type": "array"
},
"flavor_id": {
"default": "3",
"description": "App VM size — from list_flavors. A multi-container stack often wants m1.large+; plan_deploy sizes it.",
"minLength": 1,
"type": "string"
},
"git_ref": {
"description": "git mode only: branch/tag/commit (default: the repo's default branch).",
"type": "string"
},
"git_token": {
"description": "git mode only: token to clone a PRIVATE repo.",
"type": "string"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"keypair_name": {
"description": "REQUIRED. An EXISTING SSH keypair name — from list_keypairs / import_keypair.",
"minLength": 1,
"type": "string"
},
"media_mount_path": {
"description": "Host mount path on the app VM/member. Redu mounts this into /var/www/html/wp-content/uploads.",
"type": "string"
},
"media_origin_url": {
"description": "Public base URL where WordPress wp-content/uploads is served when using central_media_origin. Do not put this on the DB VM.",
"format": "uri",
"type": "string"
},
"media_space_flavor_id": {
"description": "Flavor id for the media VM when create_media_space:true. Defaults to m1.small/backend default.",
"minLength": 1,
"type": "string"
},
"media_space_id": {
"description": "Existing Redu media space id to mount at WordPress wp-content/uploads. Get it from list_media_spaces.",
"exclusiveMinimum": 0,
"type": "integer"
},
"media_space_name": {
"description": "Optional media space name when create_media_space:true. Defaults to <deployment-name>-media.",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"media_space_size_gb": {
"description": "Media space data volume size in GB when create_media_space:true (default 20).",
"maximum": 2048,
"minimum": 1,
"type": "integer"
},
"migrate_command": {
"description": "One-time DB prepare/migrate/seed, run AFTER `up -d` and BEFORE the app is marked ready — redu runs `podman-compose run --rm <migrate_service|service> <cmd>` (Rails `bundle exec rails db:prepare`, Django `python manage.py migrate`, Prisma `prisma migrate deploy`). REQUIRED for any stack whose schema is NOT auto-created on boot (e.g. Lago): WITHOUT it the stack deploys and flips to 'ready' but 502s on real use because the DB schema is missing.",
"type": "string"
},
"migrate_service": {
"description": "Compose service to run migrate_command in (defaults to `service`).",
"type": "string"
},
"name": {
"description": "Name for the deployment / VM (lowercase letters, numbers, hyphens).",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Private network id — auto-selected from check_deploy_prerequisites if omitted.",
"minLength": 1,
"type": "string"
},
"port": {
"default": 8080,
"description": "REQUIRED in practice: the HOST port the exposed service publishes (the LEFT side of its compose `ports:` mapping) — redu health-probes + proxies THIS port, so a wrong value fails the deploy. plan_deploy detects it.",
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"redis": {
"description": "'managed' = a SEPARATE managed Redis VM, auto-provisioned + wired; its REDIS_URL/REDIS_* is APPENDED to the project .env — your compose service must REFERENCE it to use it (we never rewrite your compose file). Omit/'none' to use the stack's own redis container. With 'managed' you do NOT call create_redis.",
"enum": [
"none",
"managed"
],
"type": "string"
},
"redis_flavor_id": {
"description": "managed Redis only: the dedicated Redis VM size (from list_flavors). Defaults to the app flavor.",
"type": "string"
},
"redis_password": {
"description": "managed Redis only: a specific password to set (otherwise auto-generated).",
"type": "string"
},
"redis_version": {
"description": "managed Redis only: version (default '7').",
"enum": [
"7",
"6"
],
"type": "string"
},
"repo": {
"description": "GIT MODE: public git repo URL that ships a docker-compose file. Private repo also needs git_token. Omit when using source_token.",
"format": "uri",
"type": "string"
},
"security_group_names": {
"default": [
"default"
],
"items": {
"type": "string"
},
"type": "array"
},
"service": {
"description": "The compose SERVICE to expose at the public URL (informational; the exposed port is `port`).",
"type": "string"
},
"source_token": {
"description": "UPLOAD MODE: token from prepare_upload — deploys an uploaded tarball of your LOCAL dir (no git). Omit `repo` when set.",
"minLength": 16,
"type": "string"
},
"subdir": {
"description": "Directory within the source that contains the compose file (if not at the root).",
"type": "string"
}
},
"required": [
"name",
"keypair_name"
],
"type": "object"
},
"name": "deploy_compose",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Orientation for deploying an app on redu.cloud: the end-to-end flow, the two source modes (git vs upload), how to handle a missing Dockerfile or a database, when (and when NOT) to split a deploy across multiple VMs, verifying a deploy functionally, and how to debug a failed build. Call this first when a user asks to deploy/ship/host an app and you're unsure where to start.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "deploy_overview",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"flow": {
"items": {
"type": "string"
},
"type": "array"
},
"multi_vm": {
"items": {
"type": "string"
},
"type": "array"
},
"next": {
"type": "string"
},
"source_modes": {},
"tips": {
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Stands up a redu VPN gateway (WireGuard tunnelled over wstunnel on WSS/443) on your PRIVATE network, so you (or your team) can reach the private IPs of the VMs you deployed on redu, over a single https endpoint — no UDP, no extra ports. This is the TENANT VPN for reaching your OWN deployed resources; it is NOT a general internet VPN. GUARDRAIL: the first call does NOT deploy — it returns the plan + hourly cost for approval, because it creates a BILLED VM. Show the user, get their explicit 'go', then call again with confirm:true (required even in yolo mode). The gateway asset is redu's, deployed onto YOUR VM (you can SSH in to read it). Fresh WireGuard keys are generated per deploy; the client PRIVATE key is returned ONCE and is never stored server-side.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"client_cidr": {
"description": "Your tenant PRIVATE-network CIDR (e.g. '10.1.0.0/24') so the client routes it through the tunnel. If omitted, the returned client config only routes the tunnel subnet and you add your CIDR to AllowedIPs yourself.",
"type": "string"
},
"confirm": {
"default": false,
"description": "Set true ONLY after the user has explicitly approved (the first call returns the plan + cost). Do NOT set it yourself.",
"type": "boolean"
},
"flavor_id": {
"default": "2",
"description": "Gateway VM size (from list_flavors). The gateway is light; m1.small ('2') is plenty.",
"minLength": 1,
"type": "string"
},
"keypair_name": {
"description": "REQUIRED. An EXISTING SSH keypair name (from list_keypairs / import_keypair) for the gateway VM.",
"minLength": 1,
"type": "string"
},
"name": {
"description": "Deployment name (default auto 'vpn-<id>'). Becomes the VM name + the wss://<name>.redu.cloud endpoint.",
"maxLength": 48,
"minLength": 1,
"pattern": "^[a-z0-9-]+$",
"type": "string"
},
"network_id": {
"description": "Private network id (from list_networks). Auto-selected from your default private network if omitted — the VMs you want to reach must be on this network.",
"minLength": 1,
"type": "string"
}
},
"required": [
"keypair_name"
],
"type": "object"
},
"name": "deploy_vpn",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"access_point": {
"type": "string"
},
"billing_notice": {
"type": "string"
},
"client_config": {
"type": "string"
},
"client_launch": {
"items": {
"type": "string"
},
"type": "array"
},
"deployment": {},
"error": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_confirmation": {
"type": "boolean"
},
"next": {
"type": "string"
},
"server_public_key": {
"type": "string"
},
"validation": {},
"warning": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Detaches a block volume from an instance, leaving the volume (and its data) intact and 'available' to attach elsewhere. Unmount it INSIDE the guest first (`umount /mnt/...`) or you risk a dirty filesystem. You cannot detach an instance's ROOT/boot disk while it runs. Use this to move a data volume to another VM, or to free a restore target before restore_backup writes into it. Poll list_volumes until status is 'available'.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"instance_id": {
"description": "Instance to attach it to / detach it from — from list_instances (id).",
"minLength": 1,
"type": "string"
},
"volume_id": {
"description": "Volume to attach/detach — from list_volumes (id). For a recovery, this is the volume restore_backup created (restore_backup with volumeName makes a NEW volume from a backup).",
"minLength": 1,
"type": "string"
}
},
"required": [
"volume_id",
"instance_id"
],
"type": "object"
},
"name": "detach_volume",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Returns the current status of your controller VM. On a freshly set-up VM also returns the trigger token needed to authorize the fleet and to call trigger_agent_batch. Call this after setup_agent_fleet to confirm the VM is ready.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"controller_url": {
"description": "HTTPS URL of your controller VM, e.g. https://redu-controller-abc123.redu.cloud",
"format": "uri",
"type": "string"
},
"log_token": {
"description": "Log token for fetching controller logs (optional — returned by this tool on first call)",
"type": "string"
}
},
"required": [
"controller_url"
],
"type": "object"
},
"name": "get_agent_status",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"status": {}
},
"type": "object"
}
},
{
"description": "THE APPLICATION'S OWN LOGS - what `docker logs`/`podman logs` would show for each container in a deployment. This is the tool for 'it deployed fine but it does not work': a 500, a crash loop, a failed DB connection, a missing env var all announce themselves here and NOWHERE else. ⛔ DO NOT use get_instance_logs for this. That returns the VM's SERIAL CONSOLE (kernel messages and cloud-init), which answers a question nobody debugging an app has - and on this platform it goes permanently silent once the machine finishes booting. build_log does not contain runtime output either; it stops when the build does. Default depth answers instantly from the VM's last report; a bigger `tail` or any `since` asks the VM for a fresh pull and takes up to ~15s. Secret-shaped values (PASSWORD=, TOKEN=, API_KEY=) are redacted in transit.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deployment_id": {
"description": "A deployment's id, name, or instance_id - all three resolve.",
"minLength": 1,
"type": "string"
},
"service": {
"description": "Container/service name (substring matches). Omit for every container the VM reports. Get exact names from get_containers.",
"maxLength": 200,
"type": "string"
},
"since": {
"description": "Only logs newer than this, e.g. '30m', '2h', '7d'. Always triggers a fresh pull from the VM.",
"maxLength": 10,
"type": "string"
},
"tail": {
"description": "Lines per container (default 120). Over 120 asks the VM for a fresh pull, which takes up to ~15s.",
"maximum": 1000,
"minimum": 1,
"type": "integer"
}
},
"required": [
"deployment_id"
],
"type": "object"
},
"name": "get_container_logs",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"age_seconds": {
"type": "number"
},
"available_services": {},
"hint": {
"type": "string"
},
"logs": {},
"measured": {
"type": "boolean"
},
"message": {
"type": "string"
},
"reason": {
"type": "string"
},
"source": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "WHAT IS ACTUALLY RUNNING on a deployment's VM: every container's name, image, state, health, restart count, exit code and published ports - including the ones that have CRASHED, which are the ones you need. Use this the moment a deployment says 'ready' but the URL misbehaves, and before you reach for SSH. get_deployment returns the deployment ROW (status, URL, build log); it cannot see inside the VM, so a stack where one of five services is restarting looks identical to a healthy one there. This is that missing view. It answers from the VM's own report (every 15s), so it costs no SSH round trip. If the VM has never reported, it SAYS SO rather than returning an empty list - 'no containers' and 'I cannot see' are different answers and only one of them means your app is broken. ⚠️ For a deployment that has been turned into a CLUSTER, this describes the original source VM only; autoscaled members are deliberately silent, so use list_clusters for member-level state.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deployment_id": {
"description": "A deployment's id, name, or instance_id - all three resolve.",
"minLength": 1,
"type": "string"
}
},
"required": [
"deployment_id"
],
"type": "object"
},
"name": "get_containers",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"age_seconds": {
"type": "number"
},
"containers": {},
"deployment": {},
"engine": {
"type": "string"
},
"hint": {
"type": "string"
},
"measured": {
"type": "boolean"
},
"message": {
"type": "string"
},
"reason": {
"type": "string"
},
"stale": {
"type": "boolean"
}
},
"type": "object"
}
},
{
"description": "Fetches ONE deployment by its numeric id (from list_deployments). Returns its current status, the public access_point URL, the underlying VM id, AND the build_log - IN FULL when status is 'build_failed' or 'error', which is when it is the diagnosis and you should read it; otherwise the LAST 40 LINES only, with build_log_truncated:true and build_log_total_lines so you can tell a short build from a shortened log. A ready deployment's log is not worth the tokens and used to make this response exceed the caller's output limit. Also returns a reality report_markdown showing the REAL provisioned size + cost (the plan was only an estimate; the user may have up-sized).",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "Deployment id from list_deployments.",
"exclusiveMinimum": 0,
"type": "integer"
},
"redu_md": {
"description": "⭐ USUALLY UNNECESSARY NOW: if you POST your current redu.md to the `redu_md_post_url` this tool returns (`curl -fsS --data-binary @redu.md <url>`), redu merges against that and you never paste the file at all - it is 20-29 KB and every byte costs you output. Use this parameter only when you cannot run that command. PASTE the raw contents of the repo's existing `redu.md` if it has one (`cat redu.md`). redu MERGES it server-side - recorded quirks, the pinned dname and the deploy log are carried forward - and returns the COMPLETE new file in redu_md_markdown, which you write out whole. Do NOT merge it yourself. If you OMIT this, redu returns NO redu_md_markdown at all - only redu_md_bootstrap_markdown (for a repo with no redu.md yet) and redu_md_deploy_log_line (to hand-append) - so a forgotten pass can no longer overwrite a repo's accumulated memory. If redu cannot merge what you passed it sets redu_md_merge_blocked and returns NO redu_md_markdown, precisely so there is nothing to overwrite the file with - in that case follow `next` and hand-edit the existing file instead of replacing it.",
"maxLength": 60000,
"type": "string"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "get_deployment",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"build_log": {
"type": [
"string",
"null"
]
},
"build_log_total_lines": {
"type": "number"
},
"build_log_truncated": {
"type": "boolean"
},
"deployment": {},
"failure_analysis": {
"anyOf": [
{},
{
"type": "null"
}
]
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"redu_md_bootstrap_markdown": {
"type": "string"
},
"redu_md_deploy_log_line": {
"type": "string"
},
"redu_md_filename": {
"type": "string"
},
"redu_md_last_deploy_block": {
"type": "string"
},
"redu_md_markdown": {
"type": "string"
},
"redu_md_merge_blocked": {
"type": "boolean"
},
"redu_md_merge_lost": {
"items": {
"type": "string"
},
"type": "array"
},
"redu_md_merge_mode": {
"enum": [
"full",
"in_place"
],
"type": "string"
},
"redu_md_merged_url": {
"type": "string"
},
"redu_md_omitted_bytes": {
"type": "number"
},
"redu_md_post_url": {
"type": "string"
},
"report_markdown": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Returns the DNS TXT record to add for custom domain ownership verification. Add the record to your DNS provider, then call verify_domain.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"domain": {
"description": "The custom domain to verify ownership of (e.g. app.example.com).",
"minLength": 1,
"type": "string"
}
},
"required": [
"domain"
],
"type": "object"
},
"name": "get_domain_verification",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "WHICH ENVIRONMENT VARIABLES the deployment was actually given, BY NAME - the keys in the env files on the VM (/opt/app-src/.env and friends), never the values. Most of the time the real question is 'did DATABASE_URL get wired, or is the app reading a name I did not set', and the name answers it: you can confirm the DB was connected, spot a typo'd key, and see which of the app's documented variables are missing, without touching a secret. ⛔ VALUES ARE NOT AVAILABLE HERE AND WILL NOT BE. They never leave the VM. If you genuinely need one (a generated admin password, say), SSH in with get_ssh_command and read that single key: `sudo grep '^KEY=' /opt/app-src/.env`. The file is chmod 600 root:root, so it needs sudo. Also reports each file's permission mode, which is how you catch a world-readable env file.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deployment_id": {
"description": "A deployment's id, name, or instance_id - all three resolve.",
"minLength": 1,
"type": "string"
}
},
"required": [
"deployment_id"
],
"type": "object"
},
"name": "get_env_keys",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"age_seconds": {
"type": "number"
},
"env_files": {},
"measured": {
"type": "boolean"
},
"message": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "The VM's SERIAL CONSOLE: kernel messages and early-boot/cloud-init output. ⛔ THIS IS NOT YOUR APPLICATION'S LOGS. For a deployed app - a crash loop, a 500, a failed DB connection - use get_container_logs, which returns what the containers actually printed. Reach for this one only for boot-level questions: the machine never came up, the disk did not mount, cloud-init failed before the app existed. Note that on this platform the console goes silent once a machine is past early boot, so an empty result here says nothing about a running VM.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"instance_id": {
"description": "ID of the instance to fetch BOOT/console logs for. For app logs use get_container_logs instead.",
"minLength": 1,
"type": "string"
}
},
"required": [
"instance_id"
],
"type": "object"
},
"name": "get_instance_logs",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"logs": {},
"mode": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "HOW MUCH ROOM IS LEFT on a deployment's VM: total/used/available memory, swap, disk free, load average, CPU count, and OOM kills in the last 24h - plus per-container memory and CPU. Use it to answer 'is this flavor big enough', to size the NEXT deploy of the same app honestly instead of guessing, and to explain a container that keeps dying (an OOM kill leaves no application log at all - the process is shot, so the only trace is the kernel's, and that is why oom_kills_24h is here). A full disk presents as a dozen unrelated failures and is the first thing to rule out. Every one of 34 measured deploy runs got these numbers by SSHing in and running free/df/docker stats; this is the same data with no round trip.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deployment_id": {
"description": "A deployment's id, name, or instance_id - all three resolve.",
"minLength": 1,
"type": "string"
}
},
"required": [
"deployment_id"
],
"type": "object"
},
"name": "get_resource_usage",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"age_seconds": {
"type": "number"
},
"containers": {},
"host": {},
"measured": {
"type": "boolean"
},
"message": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Returns the SSH command to connect to an instance via the redu.cloud TCP proxy. ACCEPTS an instance id, a deployment's id / name / instance_id, OR a MANAGED DATASTORE's id / name / instance_id (Postgres, MySQL/MariaDB, ClickHouse, Redis) - it resolves all of them, so you do not have to work out which one you are holding (a deployment's instance_id is a placeholder until provisioning finishes, and passing it used to fail). ⭐ SSH TO THE DATASTORE VM IS HOW YOU REACH A PRIVATE-NETWORK-ONLY DATABASE BEFORE ANY APP VM EXISTS: the datastore's own VM is SSH-reachable and ships the client on PATH (psql at /usr/bin/psql), so you can apply a schema or rotate a seeded admin account without an app VM to tunnel through. For a DEPLOYMENT VM (created by deploy_app/deploy_compose) pass keypair_name — read it from get_deployment — so the command uses `-i ~/.ssh/<keypair_name>` and authenticates with the RIGHT key instead of your default identity (without it, SSH to a deploy VM usually fails). The tool also best-effort looks up the keypair from the deployment if you omit it. Example: ssh -i ~/.ssh/redu-deploy -o IdentitiesOnly=yes -p 22011 [email protected]",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"instance_id": {
"description": "An instance id, a deployment's id / name / instance_id, or a managed datastore's id / name / instance_id. All are resolved.",
"minLength": 1,
"type": "string"
},
"keypair_name": {
"description": "The SSH keypair the instance was created with (read it from get_deployment for a deploy VM). When given, the command uses `-i ~/.ssh/<keypair_name>` so it authenticates with the right key.",
"type": "string"
}
},
"required": [
"instance_id"
],
"type": "object"
},
"name": "get_ssh_command",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"floating_ip": {
"type": "string"
},
"host": {
"type": "string"
},
"instance_name": {
"type": "string"
},
"instance_status": {
"type": "string"
},
"keypair_name": {
"type": "string"
},
"mode": {
"type": "string"
},
"port": {
"type": "string"
},
"ssh_command": {
"type": "string"
},
"user": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Registers an existing SSH public key on your account. Use this to import your own public key so you can SSH into instances. The private key never leaves your machine.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"name": {
"description": "Name for the keypair (e.g. 'my-laptop'). Must be unique on your account.",
"maxLength": 255,
"minLength": 1,
"type": "string"
},
"public_key": {
"description": "SSH public key to import (the contents of your id_rsa.pub or id_ed25519.pub).",
"minLength": 1,
"type": "string"
}
},
"required": [
"name",
"public_key"
],
"type": "object"
},
"name": "import_keypair",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Start, stop, or reboot an instance. action must be START, STOP, REBOOT_SOFT, or REBOOT_HARD.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"action": {
"enum": [
"START",
"STOP",
"REBOOT_SOFT",
"REBOOT_HARD"
],
"type": "string"
},
"instanceId": {
"minLength": 1,
"type": "string"
}
},
"required": [
"instanceId",
"action"
],
"type": "object"
},
"name": "instance_action",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Orientation for wiring a redu.cloud capability (backups, DNS, extra storage, a managed DB, ...) INTO an app already deployed on redu, e.g. 'add a backup feature to the Supabase I deployed on redu'. Explains the pattern: mint a LEAST-PRIVILEGE scoped API key (with the user's approval via create_api_key), inject it into the app, and call the redu API from the app. Call this when a user asks to add/integrate a redu feature into a running deployment and you are unsure how.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "integrate_overview",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"next": {
"type": "string"
},
"pattern": {
"items": {
"type": "string"
},
"type": "array"
},
"recipes": {},
"scopes_hint": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your volume backups.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_backups",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"backups": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your managed ClickHouse databases (OLAP / analytics — its own resource, not a relational DB). Once a row's status is 'ready' it carries the private-network connection details (private_ip, ClickHouse HTTP port 8123, db_name, db_user).",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_clickhouse_databases",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"clickhouse": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your autoscaling clusters.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_clusters",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"clusters": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your managed PostgreSQL databases. Once a row's status is 'ready', it carries the private-network connection details (private_ip, port 5432, db_name, db_user).",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_databases",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"databases": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your app deployments. Each row carries status (provisioning/ready/build_failed/error), the current build phase while provisioning (installing/source/building/built/starting), the public access_point URL, port, repo, and build_log on failure.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_deployments",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deployments": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists DNS proxy host entries.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_dns_entries",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"dns": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists custom domains you have verified ownership of.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_domains",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"domains": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists available instance sizes.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_flavors",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"flavors": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists available OS images.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_images",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"images": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your compute instances.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_instances",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"instances": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your SSH keypairs. If empty, call import_keypair first before creating instances.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_keypairs",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"keypairs": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists Redu media spaces: private NFS media VMs backed by persistent volumes. For WordPress/WooCommerce clusters, reuse one on the app's private network for wp-content/uploads, or pass create_media_space:true to deploy_app/deploy_compose/upgrade_to_cluster so Redu creates one.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_media_spaces",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"media_spaces": {
"items": {},
"type": "array"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your private networks.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_private_networks",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"networks": {
"items": {},
"type": "array"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Lists your managed Redis instances. Once a row's status is 'ready' it carries the private-network connection details (private_ip, port 6379) — connect from another instance on the same private network with redis-cli -h <private_ip> -p 6379 -a <password>.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_redis",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"redis": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Lists available regions.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_regions",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"regions": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Lists your managed MySQL/MariaDB databases (the relational-database resource). Each row carries its engine ('mysql'|'mariadb'); once status is 'ready' it has the private-network connection details (private_ip, port 3306, db_name, db_user).",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_relational_databases",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"relational_databases": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Lists your security groups.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_security_groups",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"security_groups": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Lists your instance snapshots.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_snapshots",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"snapshots": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Lists your block storage volumes.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "list_volumes",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"volumes": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Turns YOUR repo classification (you scan the repo and pass what you found) into a complete, approvable deploy plan WITHOUT creating anything. ⚡ REDU NEEDS THREE FILES IF THEY EXIST - redu.md, the compose file, the Dockerfile - and there are two ways to give them. ⭐ BEST, for an upload-mode deploy: run prepare_upload FIRST and pass its `source_token`; redu reads all three straight out of the upload you already made, the upload stays deployable, and you emit nothing. Pasting the same files costs you 20-29 KB of output for bytes the server already has. Otherwise (git mode) paste `redu_md` (cat redu.md), `compose_yaml`, `dockerfile`. Either way you do NOT read or interpret them; redu parses them SERVER-SIDE and returns (a) a short digest, (b) `pin_dname` so a redeploy keeps the SAME public URL, and (c) `preflight` - preemptive fixes for known failure patterns found in YOUR repo, each learned from a real failed build. Giving redu these files is the single highest-value thing you can do for a first deploy. picks the VM + managed-Postgres sizes, prices them at the real pricing_rules rates, and checks they FIT your quota — so a plan that can't provision is caught HERE, before any spend. You pass what you detected in the repo (runtime, port, needs_postgres/redis/clickhouse/vector_db); it returns resources + £/hr + £/mo + a feasibility verdict + a checkpoint summary to confirm with the user. Defaults: app VM m1.medium, managed Postgres m1.small, managed ClickHouse m1.medium; pass single_vm to collapse the app + Postgres onto one VM. SET needs_clickhouse:true FOR ANY ANALYTICS-SHAPED APP (Plausible, PostHog, Langfuse, Matomo, SigNoz, or anything with a clickhouse image / CLICKHOUSE_* env / a ClickHouse client dep): those products keep config in Postgres and EVERY EVENT in ClickHouse, so the events tier is a second VM with a second line on the bill: measured 2026-08-07, omitting it quoted GBP 53.29/mo for a GBP 65.99/mo deployment. It is sized, quota-checked and priced here; unlike Postgres and Redis it is not auto-wired by deploy_app, so the plan tells you to run plan_managed_datastore engine:'clickhouse' -> create_clickhouse and pass CLICKHOUSE_* env yourself. Vector-DB needs are flagged, not provisioned. Any containerizable app works (node, python, go, ...) — it deploys as a container, so the language doesn't gate it. Set serves_http:false for a non-web repo (a library, CLI, or language runtime with no HTTP server) and it returns a clean not-a-web-service verdict instead of a costed VM plan. Set heavy_build:true for resource-heavy builds (compiled-from-source native code, a monorepo/turborepo build, a large Node heap) and it raises the app VM to a build-capable floor so the on-VM build doesn't get OOM-killed. Set memory_heavy:true for a RAM-forward app whose persistent state lives in a MANAGED DB / external store (Next.js like cal.com/cal.diy, Rails, Django, JVM/Java apps) — it sizes onto a memory-optimized SMALL-DISK flavor (m1.mem16/m1.mem32: full RAM, a lean 40 GB disk instead of 160 GB) that costs less and snapshots/clusters far faster; do NOT set it if the app keeps lots of data on local disk. Also returns a brand-named markdown report (Mermaid diagram + cost) to save as redu-deploy-plan.md and show the user. Every deploy leaves TWO MANDATORY files at the repo root with DIFFERENT purposes: redu-deploy-plan.md = THIS run's plan/estimate, and redu.md = the DURABLE deploy memory the NEXT deploy reads. If a redu.md exists, READ it FIRST and reuse its known-good plan + recorded fixes; if NONE exists, one MUST be created at the end of the deploy (get_deployment returns redu_md_bootstrap_markdown for exactly that case; when a redu.md DOES exist, pass it as redu_md and write the merged redu_md_markdown). They are SEPARATE files — even if your own memory/notes from a prior deploy call redu-deploy-plan.md 'the record', the durable record is redu.md, so do not skip creating it.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"app_flavor": {
"default": "m1.medium",
"description": "Preferred app VM size (default m1.medium). Resolved against the real flavor list.",
"type": "string"
},
"app_profile": {
"default": "generic",
"description": "Detected app profile. Set wordpress/woocommerce when source inspection finds wp-config.php, wp-content, WordPress/WooCommerce packages/plugins, or wordpress/woocommerce compose images. This is used for cluster safety.",
"enum": [
"generic",
"wordpress",
"woocommerce"
],
"type": "string"
},
"clickhouse_flavor": {
"default": "m1.medium",
"description": "Preferred managed ClickHouse VM size (default m1.medium, a tier above the other datastores, which is what plan_managed_datastore also picks for ClickHouse). Resolved against the real flavor list.",
"type": "string"
},
"cluster_media_mode": {
"default": "local_uploads",
"description": "For WordPress/WooCommerce cluster_target:true, use media_space as the default real fix: Redu creates/reuses an NFS media VM backed by a volume and mounts it at wp-content/uploads. central_media_origin is only for an already-configured external origin. local_uploads is refused at cluster time.",
"enum": [
"local_uploads",
"central_media_origin",
"media_space"
],
"type": "string"
},
"cluster_target": {
"default": false,
"description": "Set TRUE when the user asked to deploy this app as an autoscaling cluster. For WordPress/WooCommerce this requires managed MySQL/MariaDB plus a Redu media space mounted at wp-content/uploads (or an explicitly configured central_media_origin); local uploads are unsafe.",
"type": "boolean"
},
"compose_yaml": {
"description": "PASTE the raw contents of the repo's docker-compose.yml / compose.yaml if it has one. redu scans it server-side for known deploy-breaking patterns (Docker-only `local` log driver, depends_on:service_healthy, profiles:, image+build, service count) and returns preemptive fixes BEFORE anything is provisioned. Costs you nothing to include; prevents whole classes of first-deploy failure.",
"maxLength": 20000,
"type": "string"
},
"containerizable": {
"default": true,
"description": "Whether the app can run in a container. Almost always true (a container is the delivery unit); set false ONLY if the app fundamentally can't be containerized — then redu can't deploy it yet.",
"type": "boolean"
},
"create_media_space": {
"default": false,
"description": "For WordPress/WooCommerce cluster_target:true: set TRUE when no suitable media space exists. The plan will include a small media VM + attached volume and deploy_app/deploy_compose will create it.",
"type": "boolean"
},
"db_engine": {
"default": "postgres",
"description": "Managed DB engine to plan. For WordPress/WooCommerce clusters use mariadb or mysql.",
"enum": [
"postgres",
"mysql",
"mariadb"
],
"type": "string"
},
"db_flavor": {
"default": "m1.small",
"description": "Preferred managed Postgres VM size (default m1.small). Resolved against the real flavor list.",
"type": "string"
},
"deploy_mode": {
"description": "guided (default) = ONE approval gate: the user approves the plan, then you proceed with the plan's defaults WITHOUT further sub-questions. yolo = auto-proceed end to end without stopping to ask, using sensible defaults. Sticky for the session once set here or via check_deploy_prerequisites.",
"enum": [
"guided",
"yolo"
],
"type": "string"
},
"dockerfile": {
"description": "PASTE the raw contents of the repo's Dockerfile if it has one. redu scans it server-side for BuildKit heredocs, empty `EXPOSE ${ARG}`, and source-build fetches (go/cargo/npm/pip/apt) that are known to fail, and returns preemptive fixes.",
"maxLength": 20000,
"type": "string"
},
"has_container": {
"default": false,
"description": "Whether the repo already has a Dockerfile / compose file.",
"type": "boolean"
},
"heavy_build": {
"description": "Set TRUE when the BUILD (not the running app) is resource-heavy — the container is built ON the VM today, so a build that needs more RAM/CPU than the idle app must be sized up or it OOMs mid-build. Signals: compiled-from-source native code (Rust/C++/CGO/Go-from-source), a monorepo/turborepo build, a Node build with a large heap (NODE_OPTIONS=--max-old-space-size), or bundling Chromium. When true, the plan raises the app VM to a build-capable floor (≥ m1.large) so `deploy_app`'s on-VM build doesn't get OOM-killed. The default m1.medium (4 GB) builds light apps fine but OOMs heavy ones.",
"type": "boolean"
},
"media_mount_path": {
"default": "/mnt/redu-media/uploads",
"description": "Host mount path on app members for the shared uploads filesystem. Redu mounts this into /var/www/html/wp-content/uploads.",
"type": "string"
},
"media_origin_url": {
"description": "Public base URL where WordPress wp-content/uploads is served when cluster_media_mode:'central_media_origin'. Do not use the DB VM as this origin.",
"format": "uri",
"type": "string"
},
"media_space_flavor": {
"default": "m1.small",
"description": "Preferred media VM size when create_media_space:true. Resolved against list_flavors.",
"type": "string"
},
"media_space_id": {
"description": "Existing Redu media space id to reuse for WordPress/WooCommerce cluster uploads. Get it from list_media_spaces. Omit when create_media_space:true.",
"exclusiveMinimum": 0,
"type": "integer"
},
"media_space_size_gb": {
"default": 20,
"description": "Size of the media space data volume in GB when create_media_space:true.",
"maximum": 2048,
"minimum": 1,
"type": "integer"
},
"memory_heavy": {
"description": "Set TRUE for a RAM-forward RUNNING app whose persistent state lives in a MANAGED DB / external store (NOT on the VM's local disk) — so it needs lots of memory but only a lean disk. Sizes onto a memory-optimized SMALL-DISK flavor (m1.mem16 = 4 vCPU / 16 GB / 40 GB; pass app_flavor:'m1.mem32' for 32 GB): the SAME RAM as the big standard flavors but a 40 GB disk instead of 160 GB — so it costs less AND snapshots/clusters FAR faster (a 40 GB snapshot is ~a third the time of 160 GB), which matters a lot if you'll scale the app into a cluster. Signals: heavy web frameworks (Next.js — e.g. cal.com / cal.diy — Rails, Django), JVM/Java apps, apps running several in-process services, or anything holding large working sets in memory, AND it uses managed Postgres/MySQL (needs_postgres with single_vm:false) or an external DB so the VM disk is just the app image. Do NOT set it if the app stores lots of data on LOCAL disk (user uploads, an on-VM database, large on-disk caches) — that needs a bigger standard-flavor disk. Distinct from heavy_build (BUILD-time RAM); mem16's 16 GB also covers most heavy builds, so the two compose.",
"type": "boolean"
},
"needs_clickhouse": {
"default": false,
"description": "App uses ClickHouse: the ANALYTICS / EVENTS store, a separate tier from the app's Postgres/MySQL. Set TRUE on ANY signal: a clickhouse/clickhouse-server image in the compose, a CLICKHOUSE_* / CLICKHOUSE_URL / CH_ env var, a clickhouse client dep (clickhouse-connect, @clickhouse/client, clickhouse-driver, clickhouse-go), or an `ON CLUSTER` / MergeTree migration. Analytics-shaped products (Plausible, PostHog, Langfuse, Matomo, SigNoz) run BOTH a relational DB for config AND ClickHouse for every event, so needs_postgres and needs_clickhouse are usually both true. Leaving it false when the app needs it under-quotes the user by a WHOLE datastore: measured 2026-08-07 on an analytics app, GBP 53.29/mo quoted against GBP 65.99/mo real, because the events tier was simply absent from the plan.",
"type": "boolean"
},
"needs_compose": {
"default": false,
"description": "Set TRUE when the repo ships a docker-compose.yml / compose.yaml that runs MULTIPLE services (app + its own db/redis/worker containers) as a stack — then deploy with deploy_compose (podman-compose on ONE VM), NOT deploy_app. A single-service compose that just wraps one Dockerfile can still use deploy_app. plan_deploy raises the app VM to a build-capable floor since a multi-container stack needs more RAM.",
"type": "boolean"
},
"needs_postgres": {
"default": false,
"description": "App uses Postgres (deps pg/prisma/sqlalchemy/drizzle or DATABASE_URL).",
"type": "boolean"
},
"needs_redis": {
"default": false,
"description": "App uses Redis (deps redis/ioredis/bullmq/celery or REDIS_URL).",
"type": "boolean"
},
"needs_vector_db": {
"default": false,
"description": "App uses a vector DB (deps qdrant-client / langchain+embeddings or QDRANT_URL).",
"type": "boolean"
},
"port": {
"description": "Port the app's HTTP server listens on (PORT env / framework default / Dockerfile EXPOSE of a real app port — NOT a debug/IPC port). Omit if the repo has no HTTP server.",
"exclusiveMinimum": 0,
"type": "integer"
},
"redu_md": {
"description": "PASTE the raw contents of `redu.md` from the repo root if the file exists (just `cat redu.md`). ⚠️ Only needed when you have NO source_token (a git-mode deploy): with a token, redu reads this file out of your upload and pasting it wastes 20-29 KB of output. You do NOT need to read or interpret it - redu parses it server-side and returns only a short digest. This is how a REDEPLOY keeps the SAME public URL and reuses the recorded fixes instead of rediscovering them.",
"maxLength": 60000,
"type": "string"
},
"runtime": {
"description": "Detected app language/runtime (node, python, go, ruby, ...). Informational — the app deploys as a container, so the language does not gate the deploy.",
"minLength": 1,
"type": "string"
},
"serves_http": {
"description": "Whether the repo actually RUNS an HTTP server that listens on a port. Set FALSE for a library, CLI tool, or language runtime that has no web server (e.g. a package you `import`, or a `bun`/`node`-style runtime) — redu can only deploy web services, so a non-server repo is reported as not-a-web-service instead of being costed. Note: an `EXPOSE` of a debug/IPC port (Chrome DevTools 9222/9242, metrics) is NOT an HTTP listener — set false. Leave unset only if you're confident it serves HTTP.",
"type": "boolean"
},
"single_vm": {
"default": false,
"description": "Force everything onto a single VM if the user prefers.",
"type": "boolean"
},
"source_token": {
"description": "The `source_token` returned by your prepare_upload upload. ⚡ PREFER THIS over pasting: redu reads redu.md, the compose file and the Dockerfile straight out of the upload you ALREADY made, so you do not have to emit a 20-29 KB file as tool input. Run prepare_upload FIRST, then pass the token here. The upload is NOT consumed by this: it stays deployable. Anything you also paste below wins over what is read from the upload.",
"type": "string"
},
"start": {
"description": "How the app starts (package.json start script / Procfile / Dockerfile CMD).",
"type": "string"
},
"vm_count": {
"default": 1,
"description": "Number of app VMs (microservices). v1: usually 1.",
"maximum": 10,
"minimum": 1,
"type": "integer"
},
"worker": {
"description": "HEADLESS WORKER / daemon: the repo runs a long-lived process with NO HTTP server (a chaos-monkey, a background/queue worker, an on-infra agent runner). Set TRUE to plan a WORKER deploy — a private VM with SSH access but NO public URL and NO health probe. serves_http/port are then not required (a serves_http:false repo is NOT rejected). Deploy it with deploy_app worker:true.",
"type": "boolean"
}
},
"required": [
"runtime"
],
"type": "object"
},
"name": "plan_deploy",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deployable": {
"type": "boolean"
},
"feasible": {
"type": "boolean"
},
"generated_dockerfile": {
"type": [
"string",
"null"
]
},
"memory_digest": {
"type": [
"string",
"null"
]
},
"memory_source": {
"type": [
"string",
"null"
]
},
"memory_used": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"pin_dname": {
"type": [
"string",
"null"
]
},
"plan": {},
"preflight": {
"items": {},
"type": "array"
},
"reason": {
"type": "string"
},
"report_filename": {
"type": "string"
},
"report_markdown": {
"type": "string"
},
"source_files_error": {
"type": [
"string",
"null"
]
},
"source_files_read": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
]
}
},
"type": "object"
}
},
{
"description": "Aggregates images/flavors/keypairs/networks/security groups into human-friendly choices. Does not create anything. Happy path: import_keypair → plan_instance → create_instance → get_ssh_command. Call this second (after import_keypair, if you have no keypairs).",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"osHint": {
"default": "",
"type": "string"
},
"sizeHint": {
"enum": [
"small",
"medium",
"large"
],
"type": "string"
}
},
"type": "object"
},
"name": "plan_instance",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"options": {}
},
"type": "object"
}
},
{
"description": "Plans a direct managed datastore create without provisioning anything. Use this before create_database, create_relational_database, create_redis, or create_clickhouse. For app deployments, prefer deploy_app database:'managed' so plan_deploy includes and wires the datastore automatically. Pass ha:true to plan a highly available datastore (postgres or clickhouse): three machines instead of one, surviving the loss of a machine without manual failover, at roughly 3x the hourly rate. The returned plan carries the machine count so you can show the user the cost before anything is created.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"engine": {
"default": "postgres",
"enum": [
"postgres",
"mysql",
"mariadb",
"redis",
"clickhouse"
],
"type": "string"
},
"ha": {
"default": false,
"description": "Plan a highly available datastore (three machines on three different physical hosts behind a load balancer, surviving the loss of a machine with no manual failover) instead of the default single machine. Set it when the user has said they need to survive a machine failure. It prices at roughly 3x the single-machine hourly rate because it runs three machines, so the plan you show the user must carry that number. Available for postgres and clickhouse; for the other engines the plan comes back single-machine with a note saying so.",
"type": "boolean"
},
"sizeHint": {
"enum": [
"small",
"medium",
"large"
],
"type": "string"
}
},
"type": "object"
},
"name": "plan_managed_datastore",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"notes": {
"items": {
"type": "string"
},
"type": "array"
},
"options": {},
"suggested": {}
},
"type": "object"
}
},
{
"description": "Returns the LOCAL shell commands to package your working directory and upload it for an upload-mode deploy (no git, no PAT). Run them in the user's terminal, capture `source_token` from the upload's JSON response, then call deploy_app with that source_token (omit repo). The upload authenticates AUTOMATICALLY with a short-lived ticket minted from your MCP credential — NO API key needed in the command and nothing secret is printed (it falls back to needing $REDU_API_KEY only if minting is unavailable). Excludes node_modules/.git/.venv/build output and .env by default; honors .gitignore when is_git_repo=true.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"is_git_repo": {
"default": false,
"description": "If the dir is a git checkout, set true to honor .gitignore (tar only tracked + non-ignored files).",
"type": "boolean"
},
"project_dir": {
"default": ".",
"description": "Path to the local app directory to upload (default current dir).",
"type": "string"
}
},
"type": "object"
},
"name": "prepare_upload",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"commands": {
"items": {
"type": "string"
},
"type": "array"
},
"endpoint": {
"type": "string"
},
"excludes": {
"items": {
"type": "string"
},
"type": "array"
},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Restores a backup into an existing volume (volumeId), an instance's volume (instanceId), or a new one (volumeName). Poll list_volumes for the restored volume. DISASTER RECOVERY (the original VM is gone, so its volume died with it): restore with volumeName to rebuild the data as a NEW volume. To bring the MACHINE back, pass that volume as create_instance's boot_volume_id - it boots from the restored disk, so the VM returns with its filesystem intact and nothing to mount. Use attach_volume instead only when you want the data as an EXTRA disk on an existing machine (then get_ssh_command -> lsblk -> mount). A restored volume is inert until it is booted from or attached.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"backupId": {
"description": "ID of the backup to restore (see list_backups).",
"minLength": 1,
"type": "string"
},
"instanceId": {
"description": "Optional: restore into this instance's volume (resolved automatically). The target volume must be available (detached), so stop the instance first.",
"type": "string"
},
"volumeId": {
"description": "Optional: restore into this existing volume.",
"type": "string"
},
"volumeName": {
"description": "Optional: name for a new volume to restore into. Omit volumeId and instanceId to restore into a new volume.",
"type": "string"
}
},
"required": [
"backupId"
],
"type": "object"
},
"name": "restore_backup",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "OPTIONAL preflight: returns a podman-compose.yml + .env so the user can run the app (and a throwaway local Postgres) on THEIR machine before deploying to redu — to see it run / sanity-check the container. Requires local podman/podman-compose. It's a suggestion, not a gate — skip it and go straight to deploy_app any time. Honest caveat: the local Postgres is NOT the managed Postgres, so a green local run does not prove the prod DB wiring.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"has_container": {
"default": false,
"description": "Repo already has a Dockerfile/Containerfile (the compose builds it). If false, write the plan_deploy-generated Dockerfile to ./Dockerfile first.",
"type": "boolean"
},
"needs_postgres": {
"default": false,
"description": "Include a local Postgres container + wire PG* env.",
"type": "boolean"
},
"port": {
"default": 3000,
"description": "Port the app listens on.",
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"runtime": {
"description": "App runtime (node/python/go/…) — from plan_deploy.",
"minLength": 1,
"type": "string"
}
},
"required": [
"runtime"
],
"type": "object"
},
"name": "scaffold_local",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"caveats": {
"items": {
"type": "string"
},
"type": "array"
},
"commands": {
"items": {
"type": "string"
},
"type": "array"
},
"files": {},
"next": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "For a repo with SEVERAL runnable parts, call this BEFORE plan_deploy. You enumerate the candidate surfaces (find every Dockerfile/Containerfile, OPEN each, classify by its EXPOSE + CMD — never by directory name) and pass them in; redu RANKS them with fixed rules (a standalone browser desktop/noVNC > a self-contained web app > a keys-required playground > an API > a headless worker > docs/examples). GUIDED: returns the ranked list to present to the user, who picks. YOLO: auto-selects the TOP-ranked surface. Then run plan_deploy on the chosen surface's path + http_port. (A single-surface repo doesn't need this.)",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"mode": {
"description": "Drive mode; omit to use the session's sticky mode.",
"enum": [
"guided",
"yolo"
],
"type": "string"
},
"surfaces": {
"description": "EVERY runnable surface you found (run `find . -name Dockerfile -o -name Containerfile`, open each). Be exhaustive — a missed browser-desktop subdir is the #1 cause of a wrong pick.",
"items": {
"additionalProperties": false,
"properties": {
"exposes": {
"description": "All ports the Dockerfile EXPOSEs (e.g. [5901,6901]) — pass them so the deploy publishes http_port, not the lowest.",
"items": {
"type": "integer"
},
"type": "array"
},
"http_port": {
"description": "The port a user opens in the browser for this surface (the web/noVNC port, e.g. 6901 for an xfce noVNC desktop — NOT a raw VNC/debug port).",
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"kind": {
"description": "What this surface IS, judged by OPENING its Dockerfile (EXPOSE + CMD/start script) — NOT by the directory name. 'browser-desktop' = a GUI/desktop streamed over noVNC/VNC/KasmVNC/Xvfb (a dir named xfce/kasm/sandbox whose Dockerfile runs noVNC IS this). 'web-app' = a browser app/UI a user uses. 'static-site' = static/marketing. 'api' = backend, no UI. 'headless-worker' = no HTTP UI. 'docs' = documentation. 'example'/'library' = not the product.",
"enum": [
"browser-desktop",
"web-app",
"static-site",
"api",
"headless-worker",
"docs",
"example",
"library",
"unknown"
],
"type": "string"
},
"needs_external_keys": {
"default": false,
"description": "TRUE if this surface is INERT until the VISITOR supplies their own API key or an external backend/sandbox (e.g. an agent 'playground' that needs an LLM key + a cloud sandbox). Such a surface is heavily demoted vs one that works standalone.",
"type": "boolean"
},
"path": {
"description": "Directory of this surface, e.g. 'libs/xfce' or '.' for the repo root.",
"type": "string"
},
"summary": {
"description": "One line: what a user does at this surface.",
"type": "string"
}
},
"required": [
"path",
"kind"
],
"type": "object"
},
"minItems": 1,
"type": "array"
}
},
"required": [
"surfaces"
],
"type": "object"
},
"name": "select_surface",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"chosen": {},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"ranked": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "Changes the PUBLIC HOSTNAME a cluster serves on, so the user can put THEIR OWN production domain (shop.acme.com, app.theircompany.com) in front of it instead of the auto-generated redu.cloud URL. That is what this tool is for: a real product on a real brand. The cluster itself is untouched - same members, same load balancer, same data - only the name in front of it changes. TWO RULES, and getting either wrong wastes a call: (1) A CUSTOM DOMAIN MUST BE VERIFIED FIRST. Call get_domain_verification for the TXT record, have the user add it at their DNS provider, then call verify_domain until it reports verified (list_domains shows what is already verified). Binding an unverified domain is refused, on purpose: without proof of ownership anyone could bind, and obtain a certificate for, a hostname that is not theirs. Also make sure the domain's own DNS actually points at this cluster, or it will verify and still not serve. (2) A redu.cloud NAME CANNOT BE FREELY CHOSEN. The redu.cloud base domain is shared by every customer, so names under it always keep a short-id suffix - the real form is `<label>-<8chars>.redu.cloud`. Ask for a bare `myapp.redu.cloud` and you will NOT get it: the server normalises it and binds something else. If the first caller could pick any name under the shared domain, the good ones would all be gone. SO: the response carries `access_point`, the hostname the server ACTUALLY bound, which may differ from what you asked for. Report that value to the user and use it everywhere afterwards - never repeat your requested label back as if it were the URL. Finally, changing the hostname changes the URL: anything wired to the old one (OAuth callbacks, cookie domains, webhooks, an app told its own address) breaks until it is updated, so say so before you call this.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"domain": {
"description": "The hostname to bind. Either your OWN production domain (e.g. shop.acme.com) - which must be verified FIRST, see the tool description - or a .redu.cloud label. A .redu.cloud value is NOT taken literally: it keeps a short-id suffix, so read the bound name from `access_point` in the response instead of assuming you got what you asked for.",
"minLength": 1,
"type": "string"
},
"stack_id": {
"description": "Cluster id, from list_clusters (the row's `id`).",
"minLength": 1,
"type": "string"
}
},
"required": [
"stack_id",
"domain"
],
"type": "object"
},
"name": "set_cluster_domain",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"access_point": {
"type": "string"
},
"domain": {
"type": "string"
},
"error": {
"type": "boolean"
},
"matched_request": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"requested_domain": {
"type": "string"
},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Turns nightly AUTOMATED (scheduled) backups ON or OFF for a MANAGED data service — the toggle that create_backup (a one-off volume snapshot) is NOT. Once enabled, redu's nightly job backs the service up on its own and prunes to the retention window; see them with list_backups and recover with the service's restore. Works for managed Postgres/MySQL/MariaDB/Redis/Qdrant/ClickHouse and can be flipped ANY time after provisioning, not only at create. Requires a card (automated backups are a paid feature; no-card trials cannot enable them). Pass the service type + its numeric id, enabled, and optional retention (days).",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"enabled": {
"description": "true = turn ON nightly AUTOMATED backups for this service; false = turn them off.",
"type": "boolean"
},
"id": {
"anyOf": [
{
"minLength": 1,
"type": "string"
},
{
"exclusiveMinimum": 0,
"type": "integer"
}
],
"description": "Numeric id of the managed service (from list_databases / list_redis / list_relational_databases / etc.)."
},
"retention": {
"description": "How many days of nightly backups to keep (optional; keeps the current setting if omitted).",
"maximum": 90,
"minimum": 1,
"type": "integer"
},
"service": {
"description": "Which managed data-service type the id belongs to. A managed Postgres from create_database/list_databases is 'postgres'; MySQL/MariaDB from create_relational_database; plus redis/qdrant/clickhouse.",
"enum": [
"postgres",
"mysql",
"mariadb",
"redis",
"qdrant",
"clickhouse"
],
"type": "string"
}
},
"required": [
"service",
"id",
"enabled"
],
"type": "object"
},
"name": "set_managed_backups",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"error": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"result": {}
},
"type": "object"
}
},
{
"description": "Complete one-shot setup: validates prerequisites, creates a controller VM + worker VMs, auto-creates a public HTTPS URL on port 7070, seeds a starter ROADMAP.md into the repo if absent, and returns the trigger token. Call this when a user says 'set up autocoding agents for my repo' or 'I want agents to work on my codebase'. HOW THE AGENT WORKS: each worker runs Claude Code inside the repo, implements one task, runs the test suite, and opens a pull request. It excels at focused, single-PR, testable units of work — add an endpoint, write tests for a module, fix a specific bug, add a UI page — and is poor at vague/large tasks, design decisions, or anything needing external credentials. TASK FORMAT (strict, one line each): `- [ ] **Title** — short description *(agent-ready)*` — the `- [ ]` checkbox, `**bold title**`, ` — ` separator, and `*(agent-ready)*` are ALL required; `##` headings and plain bullets are ignored. After this returns, the user needs to: (1) authorize the fleet by running the authorize.sh one-liner it returns (it runs `claude setup-token` for a long-lived token installed on the controller) — agents use the user's existing Claude Max/Pro subscription, NOT an API key. This is a shell command the USER runs in their own terminal; do NOT try to read or push the user's credentials yourself. The controller takes ~7 min to boot, so PREFER to poll get_agent_status until it reports the controller is reachable and present the authorize command only once it's ready — that way the user doesn't run it into a long wait. (The command also waits on its own, showing a live progress counter, so a user who runs it early is fine too.) (2) add well-scoped tasks in the format above to ROADMAP.md; (3) call trigger_agent_batch.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"github_pat": {
"description": "GitHub Personal Access Token with repo + workflow scopes. Used to clone the repo and open PRs.",
"minLength": 1,
"type": "string"
},
"repo": {
"description": "GitHub repo to run agents on, e.g. 'owner/my-app'. Must be accessible with the PAT.",
"minLength": 1,
"type": "string"
},
"worker_count": {
"default": 3,
"description": "Number of parallel worker VMs (default 3). Each works on a separate task simultaneously.",
"maximum": 10,
"minimum": 1,
"type": "integer"
}
},
"required": [
"github_pat",
"repo"
],
"type": "object"
},
"name": "setup_agent_fleet",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"authorize_command": {
"type": "string"
},
"controller_host": {
"type": "string"
},
"controller_url": {
"type": "string"
},
"fleet_id": {
"type": "string"
},
"instance_id": {
"type": "string"
},
"log_token": {
"type": "string"
},
"mode": {
"type": "string"
},
"next_steps": {
"items": {},
"type": "array"
},
"repo": {
"type": "string"
},
"ssh_key_name": {
"type": "string"
},
"ssh_private_key": {
"type": "string"
},
"ssh_user": {
"type": "string"
},
"trigger_token": {
"type": "string"
},
"worker_ids": {
"items": {},
"type": "array"
}
},
"type": "object"
}
},
{
"description": "One-shot tool that guides you through the complete agent setup: checks prerequisites, creates the controller VM, and returns next steps. Ideal for first-time setup. Call this when a user says 'set up autocoding agents for my repo'.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"github_pat": {
"description": "GitHub Personal Access Token (repo + workflow scopes)",
"minLength": 1,
"type": "string"
},
"repo": {
"description": "GitHub repo, e.g. 'owner/repo'",
"minLength": 1,
"type": "string"
}
},
"required": [
"github_pat",
"repo"
],
"type": "object"
},
"name": "setup_agent_session",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"next_tool": {
"type": "string"
},
"ready": {
"type": "boolean"
},
"repo": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Starts the autonomous agent batch on your controller VM. Agents read agent-ready tasks from your ROADMAP.md, implement them in parallel, and open PRs. No VPN needed — runs over HTTPS; the controller stays running after you disconnect. Each ROADMAP task MUST be a single checkbox line `- [ ] **Title** — short description *(agent-ready)*` (the `- [ ]`, `**bold title**`, and `*(agent-ready)*` are all required; `##` headings and plain bullets are ignored). If this returns 'No agent-ready tasks', the tasks are mis-formatted — fix them to that exact format and retry.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"controller_url": {
"description": "HTTPS URL of your controller VM",
"format": "uri",
"type": "string"
},
"trigger_token": {
"description": "Trigger token from the controller",
"minLength": 1,
"type": "string"
}
},
"required": [
"controller_url",
"trigger_token"
],
"type": "object"
},
"name": "trigger_agent_batch",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"finalize_note": {
"type": "string"
},
"fleet_id": {
"type": "string"
},
"message": {
"type": "string"
},
"ok": {
"type": "boolean"
},
"workers_ready": {
"type": "boolean"
}
},
"type": "object"
}
},
{
"description": "Updates a running autoscaling cluster to a new app version IN PLACE, with NO URL change: snapshots the 'hero' VM (the source/master VM you SSH into and change — it is also the always-on baseline member that serves traffic), then redu rolling-updates the autoscaled extra members to that snapshot ONE AT A TIME and refreshes the image the group boots future members from — the SAME load balancer and SAME *.redu.cloud URL are kept, and the service stays up during the roll. This is the ONLY way to update a cluster: you do NOT edit live members (they are disposable clones); you change the hero VM (SSH into it, deploy the new version), then update_cluster snapshots it and spreads it to the fleet (the hero already runs your change; this propagates it to the autoscaled members). Zero-downtime is measured, not aspirational: a full roll of every member of a 3-member cluster served 699/699 requests with no failures and never dropped below a majority. That holds ONLY if the app starts on every boot - a member whose app is launched by a first-boot-only script returns empty after replacement, never rejoins the pool, and the roll silently drains the cluster. The snapshot upload can take several minutes on a large disk; poll list_clusters until the stack status is UPDATE_COMPLETE.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"instance_id": {
"description": "The 'hero' VM to snapshot and roll to every extra member: the source/master VM you upgraded to a cluster and SSH into to make changes. It is your cluster's always-on baseline member. Its CURRENT disk state becomes the new member image, so make your changes on it FIRST, then call this.",
"minLength": 1,
"type": "string"
},
"stack_id": {
"description": "Cluster stack id, from list_clusters (id).",
"minLength": 1,
"type": "string"
},
"stack_name": {
"description": "Cluster stack name, from list_clusters (stack_name).",
"minLength": 1,
"type": "string"
}
},
"required": [
"stack_name",
"stack_id",
"instance_id"
],
"type": "object"
},
"name": "update_cluster",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "THE WAY TO MAKE AN EXISTING APP HIGHLY AVAILABLE. If the user asks for high availability, redundancy, no single point of failure, surviving a host or VM failure, or 'stay up if a machine dies', this is the tool: point it at their running VM and pass high_availability:true. It needs no rebuild and no redeploy, and the URL does not change. Turns a single running instance into a horizontally-autoscaling cluster: snapshots the instance and puts your SOURCE VM behind a load balancer as its ALWAYS-ON BASELINE MEMBER, then an autoscaling group adds EXTRA members (booted from that snapshot) on CPU load and scales them back down to zero. FLAVOR — LEAD WITH A MEMORY-OPTIMIZED SMALL-DISK FLAVOR (m1.mem16/m1.mem32) for the cluster, and ideally have the SOURCE VM on one too: clustering snapshots the source VM's FULL disk, so DISK size (not RAM) drives how long the cluster takes to build and to add each member under load — a 40 GB mem flavor is several times faster to snapshot and scale than a 160 GB standard flavor (m1.xlarge) and costs less for the same RAM. If the source VM is on a big-disk flavor, RECOMMEND migrating the app onto a mem-flavor VM first (a root disk cannot be shrunk in place), even though it is extra work — it pays off on every scale-out, and a clustered app is stateless anyway (state in the managed DB) so the lean disk fits. Expect a DRASTICALLY better clustering experience on a mem flavor: far faster cluster-create, snapshots, and every scale-out. The user can override with any flavor. At rest ONLY your source VM serves — there is NO idle extra VM to pay for (the source VM is the cluster's minimum, so the floor is 0 extra members). Use it to LOAD-SCALE a stateless app tier while managed services hold state: it becomes highly-available UNDER LOAD (multiple members behind the LB), but at rest a SINGLE source VM serves — and that source VM is a plain VM, not an autoscaled member, so it is NOT auto-replaced if it fails while idle (only the autoscaled extra members are ASG-managed and self-healed). If you need always-on redundancy, keep the app under enough load to hold >=1 extra member, or use a separate always-on setup. BILLED — at rest it costs just your source VM (which you already run) plus the load balancer; under load it adds up to max_size EXTRA members at the member flavor (flavor_id), billed only while they run. In guided mode show the cost that way (now: source VM already running + the LB; under load: up to max_size x the member flavor) and get the user's explicit go first. redu automatically repoints the extra members from the old single-VM URL to the load-balancer URL across app config. It REFUSES a STATEFUL VM with 409 cluster_needs_stateless unless confirm_stateless:true. To have redu FIX a stateful VM for you instead of refusing, pass auto_restructure:true — for a single_vm Postgres it fully-automatically provisions a managed DB + migrates the data + repoints the members; for a compose-stack DB it provisions the matching managed DB (set restructure_engine, e.g. 'mysql'/'mariadb' for WordPress) and returns migration commands to run from the app VM. WordPress/WooCommerce is not generic autoscaling: managed DB alone is not enough because wp-content/uploads is file state. Use app_profile:'wordpress'/'woocommerce', cluster_media_mode:'media_space', and either media_space_id or create_media_space:true so all members mount the same uploads filesystem; otherwise the backend refuses with 409 cluster_needs_media_space. PUT THE CLUSTER ON THE SAME private network as the managed DB and media space. HA: cluster members are spread across DIFFERENT physical hosts automatically, and an autoscaled member that is destroyed is REBUILT AUTOMATICALLY in 1.5 to 5 minutes depending on how it failed with no action from you (the always-on source/hero VM is a plain VM and is NOT covered by that). CRITICAL for members: the app must start on EVERY boot (systemd unit or container restart policy) - if it only starts from a first-boot cloud-init script, a rebooted or resized member comes back with no app, silently never rejoins the load balancer, and the cluster quietly loses capacity with nothing reporting an error. Pass startup_command if the app does not already auto-start on boot, and have it bind its port only once it is genuinely ready to serve (the health check can only see whether the port is open). SEQUENCING - this catches people: the snapshot is taken IMMEDIATELY, and every member boots from it, so the source VM's app must already be RUNNING before you call this. Clustering a freshly-created VM whose cloud-init has not finished captures an image with no enabled service, and all members then come up ACTIVE while failing the load-balancer health check forever - a cluster that looks built and serves nothing. Verify the app answers on its port first (get_ssh_command, or just fetch the VM's URL). The snapshot upload can take several minutes; poll list_clusters until CREATE_COMPLETE.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"app_profile": {
"description": "Set wordpress/woocommerce when upgrading an older deployment or raw VM that the backend has not profiled. WordPress/WooCommerce clusters require managed MySQL/MariaDB plus a media_space or explicitly configured central_media_origin.",
"enum": [
"generic",
"wordpress",
"woocommerce"
],
"type": "string"
},
"auto_restructure": {
"description": "Instead of just refusing a stateful VM, have redu get its co-located database OFF the VM first, then cluster. FULLY AUTO for a single_vm Postgres (redu deployed it): redu provisions a managed Postgres on the cluster's private network and the members migrate the data + repoint the app on boot — no manual steps (response restructure.mode='auto'). For a compose/unknown DB, ASSISTED: redu provisions the matching managed DB (see restructure_engine) and returns the exact dump/restore/repoint commands to run FROM THE APP VM (response restructure.mode='assisted') — redu will not run an unattended dump against a database it does not own. Only addresses the co-located-DB refusal (cluster_needs_stateless); it does NOT bypass the WordPress media guard. Writes to the OLD VM during the (several-minute) upgrade are not migrated; cut traffic over to the cluster URL once it is up.",
"type": "boolean"
},
"cluster_media_mode": {
"description": "For WordPress/WooCommerce, use media_space as the default real cluster fix. local_uploads is refused because wp-content/uploads would diverge across members.",
"enum": [
"local_uploads",
"central_media_origin",
"media_space"
],
"type": "string"
},
"confirm_stateless": {
"description": "Override the stateful-VM safety refusal. redu REFUSES to cluster a VM that holds its own data (deployed with database:'single_vm' or a compose 'db' service) - a cluster clones the VM into members, so on-VM data diverges and is lost on scale-in; it returns 409 cluster_needs_stateless + a restructure_plan (move the DB to managed, repoint, redeploy, then cluster). Set true ONLY when the VM is genuinely stateless (state in a managed DB / external store, e.g. database:'managed'), or a raw VM you have made stateless.",
"type": "boolean"
},
"create_media_space": {
"description": "Set TRUE when upgrading a WordPress/WooCommerce deployment that has no media_space_id yet. Redu creates a small NFS media VM + volume, records it, and mounts it on all members.",
"type": "boolean"
},
"dname": {
"description": "Optional public DNS name for the cluster's load-balancer access point (a .redu.cloud name is auto-generated if omitted).",
"type": "string"
},
"flavor_id": {
"description": "Member VM size — from list_flavors. Every autoscaling member uses this flavor. STRONGLY prefer m1.mem16/m1.mem32 (memory-optimized, 40 GB disk): clustering snapshots the source disk, so a lean disk makes cluster-create and every scale-out several times faster than a 160 GB standard flavor (and costs less for the same RAM). The member flavor's disk must be >= the source VM's disk, so the biggest win comes from the SOURCE VM being on a mem flavor too — if it is on a big-disk flavor, migrate the app onto a mem-flavor VM before clustering. The user can override with any flavor.",
"minLength": 1,
"type": "string"
},
"high_availability": {
"description": "Set TRUE whenever the user asks for HIGH AVAILABILITY, redundancy, 'no single point of failure', 'survive a host/node failure', or 'stay up if a VM dies'. This is the ONLY way to get an HA cluster and you should pick it yourself when the user's intent is availability rather than load-scaling — do not make them discover it. It runs 3 permanent members spread across 3 DIFFERENT physical hosts; a member that is destroyed is rebuilt AUTOMATICALLY in 1.5 to 5 minutes depending on how it failed (both ends measured), with no action from anyone; updates roll one member at a time, keeping the cluster serving throughout (measured: about 2% of requests blip as each member is swapped, capacity never below 2 of 3, URL unchanged). Trade-off to state in the plan: 3 members run continuously (so 3x the member cost, versus 0 idle members in the default mode), and the source VM leaves the load balancer to become the machine you deploy changes to, so it keeps running and is billed too. Default false = load-scaling mode: the single source VM serves at rest and is NOT auto-replaced if it dies.",
"type": "boolean"
},
"idempotency_key": {
"minLength": 8,
"type": "string"
},
"instance_id": {
"description": "The instance to turn into a cluster (from list_instances, or the VM of a deploy_app/deploy_compose deployment). It is snapshotted, and the autoscaling members are booted from that snapshot.",
"minLength": 1,
"type": "string"
},
"keypair_name": {
"description": "SSH keypair name — from list_keypairs.",
"minLength": 1,
"type": "string"
},
"max_size": {
"description": "Peak member count under load (default 5). In the default mode this is EXTRA members added on top of your always-on source VM, and the floor is 0 extra. With high_availability:true the group runs between 3 and this number, so set it >= 3.",
"maximum": 10,
"minimum": 1,
"type": "integer"
},
"media_mount_path": {
"description": "Host mount path on cluster members. Redu mounts this into /var/www/html/wp-content/uploads for WordPress/WooCommerce.",
"type": "string"
},
"media_origin_url": {
"description": "Public base URL where WordPress wp-content/uploads is served from the central media origin. This should be a separate media origin, not the DB VM.",
"format": "uri",
"type": "string"
},
"media_space_flavor_id": {
"description": "Flavor id for the media VM when create_media_space:true. Defaults to the cluster member flavor.",
"minLength": 1,
"type": "string"
},
"media_space_id": {
"description": "Existing Redu media space id to mount on every WordPress/WooCommerce cluster member.",
"exclusiveMinimum": 0,
"type": "integer"
},
"media_space_name": {
"description": "Optional name for the media space when create_media_space:true. Defaults to <cluster-name>-media.",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"media_space_size_gb": {
"description": "Media space data volume size in GB when create_media_space:true (default 20).",
"maximum": 2048,
"minimum": 1,
"type": "integer"
},
"name": {
"description": "Name for the cluster (lowercase letters, numbers, hyphens).",
"maxLength": 63,
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Private network id — from list_private_networks. Use the SAME network the app's managed DB / other services are on, so members can reach them by private IP.",
"minLength": 1,
"type": "string"
},
"port": {
"description": "Port the app listens on / the load balancer forwards to (default 80). Your source VM must actually serve on this port — the load balancer health-checks it.",
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"restructure_db_flavor_id": {
"description": "Optional VM size for the provisioned managed database (from list_flavors). Defaults to the member flavor; pick a smaller one (e.g. m1.small) if the DB is light.",
"type": "string"
},
"restructure_engine": {
"description": "For the ASSISTED path (a compose-stack database), which managed engine to provision. WordPress/WooCommerce = 'mysql' (or 'mariadb'). Required when auto_restructure is set and the DB runs inside the compose stack; ignored for a single_vm Postgres (always Postgres).",
"enum": [
"postgres",
"mysql",
"mariadb"
],
"type": "string"
},
"scale_in_threshold": {
"description": "Average CPU fraction (0-1) below which a member is removed (default 0.2).",
"maximum": 1,
"minimum": 0,
"type": "number"
},
"scale_out_threshold": {
"description": "Average CPU fraction (0-1) above which a member is added (default 0.7).",
"maximum": 1,
"minimum": 0,
"type": "number"
},
"startup_command": {
"description": "Single-line shell command to (re)start the app on each fresh member if the snapshot's own services do not auto-start it. For a podman-compose app: 'cd /opt/app && podman-compose up -d'. Omit to rely on the image's own auto-starting services.",
"maxLength": 2000,
"pattern": "^[^\\n\\r]*$",
"type": "string"
}
},
"required": [
"instance_id",
"name",
"flavor_id",
"network_id",
"keypair_name"
],
"type": "object"
},
"name": "upgrade_to_cluster",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"available_keypairs": {
"items": {
"type": "string"
},
"type": "array"
},
"duplicate": {
"type": "boolean"
},
"error": {
"type": "boolean"
},
"idempotency_key": {
"type": "string"
},
"invalid_keypair_name": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"needs_keypair": {
"type": "boolean"
},
"needs_keypair_name": {
"type": "boolean"
},
"needs_plan": {
"type": "boolean"
},
"next": {
"type": "string"
},
"plan_tool": {
"type": "string"
},
"request": {},
"result": {},
"validation": {}
},
"type": "object"
}
},
{
"description": "Turns an EXISTING, running single-machine managed Postgres or ClickHouse into a THREE-machine highly available cluster, in place, keeping the data and the same username, password and database name. Use this when the user has a live database and wants it to survive a machine or host failure: they do NOT have to create a new one and migrate by hand. How it works: a full backup is taken first (kept whatever happens), the cluster is built alongside, the data is copied in and verified by counting both sides, and only then does the database's address move to the load balancer. The original machine is left running and untouched throughout, so a failure changes nothing and the user is never without a database. SAY THREE THINGS TO THE USER AND GET A YES BEFORE CALLING: (1) COST, it becomes three machines behind a load balancer instead of one, so roughly 3x the hourly rate, and the old single machine keeps billing until they delete it; (2) WRITE LOSS, anything written to the old machine during the copy is not carried over (see acknowledge_write_loss); (3) THEY MUST REPOINT THE APP afterwards, the connection address changes and nothing redeploys their app for them. Takes about 20 minutes and returns immediately; poll list_databases / list_clickhouse_databases until the address changes. Requires a payment method: high availability is not available on the no-card trial. ⛔ CLICKHOUSE ONLY, a FOURTH thing to say: after the upgrade every table has to use a REPLICATED engine. The existing tables are converted for them as part of the copy, but any NEW `CREATE TABLE ... ENGINE = MergeTree` is refused from then on (ClickHouse error 56) and has to be written `ENGINE = ReplicatedMergeTree`. It is refused rather than accepted because a plain MergeTree on three machines replicates its schema and not its rows, which would leave two of the three answering with an empty table and no error. If their app ships plain-MergeTree migrations (Plausible, PostHog and Langfuse all do), those migrations will fail after the upgrade and they should stay on the single machine. For the app tier rather than the database, use upgrade_to_cluster instead.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"acknowledge_write_loss": {
"default": false,
"description": "REQUIRED, and you must tell the user this in your own words and get a yes BEFORE you set it true. The upgrade copies the data while the old machine is still serving, so anything the application writes to the old machine AFTER the copy starts, and before you repoint the app at the new address, is NOT copied and cannot be recovered afterwards. This was measured on a real upgrade: one row written mid-copy was missing (5001 rows on the old machine, 5000 on the cluster). For a quiet database that is nothing; for one taking live orders it is lost orders. The safe sequence is: pause or drain writes, upgrade, repoint the app at the new address, resume. Never set this true on the user's behalf to be helpful.",
"type": "boolean"
},
"flavor_id": {
"description": "Only needed for an old database whose build settings were not recorded; the API says so if it is missing. Otherwise the cluster reuses the machine size the database already has.",
"minLength": 1,
"type": "string"
},
"id": {
"anyOf": [
{
"minLength": 1,
"type": "string"
},
{
"exclusiveMinimum": 0,
"type": "integer"
}
],
"description": "Numeric id of the EXISTING single-machine database (from list_databases / list_clickhouse_databases). It must be status='ready'."
},
"keypair_name": {
"description": "Only needed for an old database whose build settings were not recorded.",
"minLength": 1,
"type": "string"
},
"network_id": {
"description": "Only needed for an old database whose build settings were not recorded. Otherwise the cluster joins the same private network.",
"minLength": 1,
"type": "string"
},
"service": {
"description": "Which managed database type the id belongs to. 'postgres' for one from create_database/list_databases; 'clickhouse' for one from create_clickhouse/list_clickhouse_databases. MySQL, MariaDB, Redis and Qdrant have no HA option and this tool cannot make them highly available.",
"enum": [
"postgres",
"clickhouse"
],
"type": "string"
}
},
"required": [
"service",
"id"
],
"type": "object"
},
"name": "upgrade_to_ha",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"error": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"result": {}
},
"type": "object"
}
},
{
"description": "Asks redu to re-probe a deployment's public URL and update its status from what it actually observes. USE THIS AFTER YOU HAVE FIXED A FAILED DEPLOY - a deployment that ended 'error' or 'build_failed' but is now serving becomes 'ready' again, and you do NOT need to redeploy it to get a green status. Redeploying working infrastructure to clear a red flag wastes the user's money and throws away the fix. You cannot set the status yourself: this tool triggers a check, and the probe decides. Readiness means the URL answers with a non-5xx (401/403/404 count - they prove the app is answering), the same rule the deploy's own health gate uses. A deployment with no public URL (a headless worker) cannot be verified this way; use get_containers for its container state instead.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "Deployment id from list_deployments.",
"exclusiveMinimum": 0,
"type": "integer"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "verify_deployment",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"changed": {
"type": "boolean"
},
"id": {
"type": "number"
},
"message": {
"type": "string"
},
"next": {
"type": "string"
},
"previous_status": {
"type": "string"
},
"probe": {},
"ready_at": {
"type": "string"
},
"ready_signal": {
"type": "string"
},
"status": {
"type": "string"
}
},
"type": "object"
}
},
{
"description": "Checks the DNS TXT ownership record for a custom domain and marks it verified when the record is present. Call get_domain_verification first, add the TXT record at your DNS provider, then call this until status is verified.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"domain": {
"description": "The custom domain to verify ownership of (e.g. app.example.com).",
"minLength": 1,
"type": "string"
}
},
"required": [
"domain"
],
"type": "object"
},
"name": "verify_domain",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"domain": {
"type": "string"
},
"mode": {
"type": "string"
},
"next": {
"type": "string"
},
"result": {},
"status": {
"type": "string"
},
"verified_at": {
"type": [
"string",
"null"
]
}
},
"type": "object"
}
},
{
"description": "⭐ CALL THIS INSTEAD OF POLLING. Blocks until the deployment reaches a TERMINAL state (ready, build_failed, error or vm_missing) and returns, in ONE response, everything you would otherwise fetch afterwards: the HTTP status of the PUBLIC url (not the VM's private interface, which can answer 401 while the public path still 502s), the container table with health and restarts, the host's memory/disk/load, and the .env KEY NAMES. Do NOT loop on get_deployment: that is 8-9 round trips per deploy that learn nothing, and then three more asking for what this already returned. ⚠️ A TIMEOUT IS NOT A FAILURE: if it returns timed_out:true the build is simply still running, so call again, optionally with a larger timeout_s. Only status decides success. On build_failed or error, call get_deployment(id) for the full build_log, which is the diagnosis.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"id": {
"description": "Deployment id, from the deploy call or list_deployments.",
"exclusiveMinimum": 0,
"type": "integer"
},
"timeout_s": {
"description": "How long to wait before returning timed_out:true (default 300, max 600). A typical deploy reaches ready in 90 to 250 seconds.",
"exclusiveMinimum": 0,
"maximum": 600,
"type": "integer"
}
},
"required": [
"id"
],
"type": "object"
},
"name": "wait_for_deployment",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"deployment": {},
"evidence": {},
"next": {
"type": "string"
},
"note": {
"type": "string"
},
"terminal": {
"type": "boolean"
},
"timed_out": {
"type": "boolean"
},
"waited_seconds": {
"type": "number"
}
},
"type": "object"
}
},
{
"description": "Verifies your redu.cloud credential with a REAL server round-trip and returns your identity + live quota. A green result means the key actually works — not merely that one is configured. Use this first if anything is 401ing.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {},
"type": "object"
},
"name": "whoami",
"outputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"auth_type": {
"type": "string"
},
"next": {
"type": "string"
},
"quota": {},
"user": {},
"verified": {
"type": "boolean"
}
},
"type": "object"
}
}
]
}Verify it yourself
curl -s https://api.teppi.xyz/v1/evidence/sha256:9c15c12eef96e0a5c687f75306a3b6479dff16bed5bf6a21749d3eded4eec602 | sha256sum