# Kuiper docs > Kuiper is a deployment platform for AI agents, the way Vercel is for Next.js apps. It builds, runs and keeps alive Glove Foundry apps, Station daemons and other Node services, with persistent disks, managed Postgres, Redis and object storage, secrets, custom domains, logs and rollbacks. Made by Cradle Research (cradleresearch.io). Kuiper is in early access. Every page of the Kuiper docs (https://kuiper.sh/docs/) in reading order, as Markdown. The short index is https://kuiper.sh/llms.txt. --- # Overview and quickstart > Kuiper deploys Glove Foundry and Station agents. Install the CLI, sign in, deploy an agent and watch it come up. Web page: https://kuiper.sh/docs/ ## What Kuiper does Kuiper is a deployment platform for agents, the way Vercel is for Next.js apps. You give it code. It builds the code, runs it, gives it a public address and keeps it running. - **It knows your framework.** Glove Foundry apps and Station daemons deploy without a Dockerfile or a config file. Kuiper detects them and picks the build, the start command and the port. Any other Node app deploys too, once you give it a start command and a port. - **It keeps state.** Foundry and Station units get a persistent disk at `/data`. Logs survive crashes and redeploys. Managed Postgres, Redis and object storage run inside your project. - **It shows you the agent.** The dashboard reads runs, signals, conversations and events straight from your running agents. There is nothing to wire up. - **It gives agents tools.** Real Chrome [browsers](https://kuiper.sh/docs/browsers.md) behind a Steel-compatible API, and [sandboxes](https://kuiper.sh/docs/sandboxes.md) to run code in, from [ready-made images](https://kuiper.sh/docs/sandbox-images.md). Both are metered by the second and held to your [budgets](https://kuiper.sh/docs/budgets.md). One [model endpoint](https://kuiper.sh/docs/models.md) sends calls to your own provider keys and records every one. - **It stays out of the way.** Variables, secrets, domains and rollbacks are one command or one click each. ## The pieces You will see these words everywhere in the dashboard and the CLI. | Term | What it is | | --- | --- | | Organisation | The team that owns everything. Everyone in it sees every project. | | Project | A group of units that belong together. Secrets, databases, buckets and domains belong to a project. It has a name and a slug, the short handle you pass to `-p`. | | Unit | One agent or service you deploy. Each unit has its own address, logs and deployments. A unit is an `agent` (the default), a `station` daemon or a `service`. | | Build | Turns source, a Git push or an uploaded folder, into an image. | | Deployment | One image running as a unit. Deployments are numbered `#1`, `#2` and so on. One is live at a time. | | Variable | A setting a unit reads from its environment. Its value is a template, so it can point at a secret, a database or another unit. | | Secret | A value Kuiper stores sealed and never shows again. | ## Quickstart You need a Kuiper account. If you do not have one yet, [request early access](https://kuiper.sh/#early-access). You also need a folder with a Glove Foundry app, a Station daemon or a Node app. 1. **Install the CLI.** ```sh curl -fsSL https://kuiper.sh/install | sh ``` Check it with `kuiper --version`. More in [Install and sign in](https://kuiper.sh/docs/install.md). 2. **Sign in.** ```sh kuiper login ``` Your browser opens the dashboard and shows a short code. The terminal shows the same code. Approve it in the browser and the CLI receives its own API token. 3. **Create a project and a unit.** ```sh kuiper projects create my-agent kuiper use my-agent kuiper units create web ``` `kuiper use` sets the default project, so you can leave out `-p` from now on. The unit is called `web`, which is also the default for `-u`. Deploying a Station daemon? Create the unit with `--kind station`. 4. **Deploy your folder.** ```sh cd path/to/your-agent kuiper deploy --source . -u web --wait ``` The CLI uploads the folder, streams the build log, then prints each step of the rollout until the deployment is live. ```text uploading 38 files (96 KiB) as your-agent@3f9c2a1 … live ✓ ``` 5. **See it live.** ```sh kuiper status -u web kuiper logs -u web -f ``` `status` prints the unit's address and its live deployment. `logs -f` follows the output. Open the address in your browser, or find the unit in the dashboard. ![The project overview in the dashboard: one card per unit, with a log preview, its address, status and live deployment.](https://kuiper.sh/docs/img/dashboard-overview.webp "The project overview. Every unit gets a card with its address and live deployment.") Prefer the dashboard? Open **New project**, pick a repository or drop a folder, and press **Deploy**. [Deploying](https://kuiper.sh/docs/deploying.md) covers all three ways in. ## Point your agent at the docs Give a coding agent `https://kuiper.sh/llms.txt`: an index of every page, with the facts it needs to use Kuiper up front. `https://kuiper.sh/llms-full.txt` is every page in one file, and any page is plain Markdown with `.md` on its address, like `https://kuiper.sh/docs/models.md`. For an agent that should also see and act in your dashboard, [connect it over MCP](https://kuiper.sh/docs/mcp.md). ## Where to go next - [Install and sign in](https://kuiper.sh/docs/install.md) Install the CLI, sign in from a terminal or CI, and add a passkey. - [Dashboard tour](https://kuiper.sh/docs/dashboard.md) Every page of the dashboard, with screenshots. - [Connect an agent (MCP)](https://kuiper.sh/docs/mcp.md) Let Claude, Cursor or VS Code read your dashboard and, if you allow it, act in it. - [Deploying](https://kuiper.sh/docs/deploying.md) From Git, from a folder or from an image, and what `kuiper.json` does. - [Variables and secrets](https://kuiper.sh/docs/variables.md) Point a unit at a secret, a database or another unit. - [Environments](https://kuiper.sh/docs/environments.md) Run staging and experiments of a project from their own branches. - [Station units](https://kuiper.sh/docs/station.md) Deploy a Station daemon and inspect its signals, runs and beacons. - [Glove Foundry units](https://kuiper.sh/docs/foundry.md) Deploy a Foundry app, protect it and read its runs. - [Models](https://kuiper.sh/docs/models.md) Call any model provider in its own API through one gateway with your own keys, every call recorded. - [Browsers](https://kuiper.sh/docs/browsers.md) Give agents real Chrome browsers through Steel's API, with hibernation, profiles and recordings. - [Sandboxes](https://kuiper.sh/docs/sandboxes.md) Give agents a remote computer to run code in, with a terminal, files and live ports. - [CLI reference](https://kuiper.sh/docs/cli.md) Every command and flag on one page. - [Example: ops-lab](https://kuiper.sh/docs/example.md) A Station network, a service and a Foundry agent in one project. --- # Install and sign in > Install the kuiper CLI on macOS or Linux, sign in from a terminal, a server or CI, and add a passkey to the dashboard. Web page: https://kuiper.sh/docs/install/ ## Install the CLI The `kuiper` command deploys units, manages variables and reads logs. It runs on macOS and Linux, on x86-64 and arm64. Install it with one command: ```sh curl -fsSL https://kuiper.sh/install | sh ``` The installer downloads the prebuilt `kuiper` binary for your machine from kuiper.sh, checks it against its published SHA-256, installs it, and points it at Kuiper, so `kuiper login` needs no extra flags. The CLI is only distributed as these prebuilt binaries; its source is not public, so there is no build-from-source option. Check that it works: ```sh kuiper --version kuiper --help ``` Want to read the script before you run it? Download it first. ```sh curl -fsSL https://kuiper.sh/install -o install.sh less install.sh sh install.sh ``` If the installer has no build for your platform, it says so and installs nothing. Everything the CLI does is also in the dashboard. ## Sign in from a terminal ```sh kuiper login ``` Signing in takes four steps: 1. The CLI opens the dashboard in your browser. 2. A short code, like `WDJB-MJHT`, appears in the browser and in the terminal. Check that the two match. 3. Approve the code in the browser. 4. The CLI receives its own API token and stores it. The token is named after your machine, so you can find it later under **Access**, then **API tokens**. You never type a password or paste a token. Run `kuiper whoami` to see which organisation and instance the CLI is signed in to. ```sh kuiper whoami ``` ### On a machine without a browser A server, a container or an SSH session has no browser to open. Add `--no-browser` and the CLI prints a link instead. ```sh kuiper login --no-browser ``` Open the link on any device where you are signed in to the dashboard, check the code and approve it. Once you approve, the CLI finishes signing in. ### In CI and scripts Automation should not wait for a person. Give it a token instead. Create one under **Access**, then **API tokens**, or with `kuiper tokens create ci`, then pass it to `login`: ```sh kuiper login --token kpr_… ``` You can also send the token on standard input, which keeps it out of your shell history and process list: ```sh printf %s "$KUIPER_TOKEN" | kuiper login ``` Or skip `login` altogether. The CLI reads `KUIPER_TOKEN` and `KUIPER_API_URL` from the environment, and they win over anything stored on disk. ```sh export KUIPER_API_URL=https://app.kuiper.sh export KUIPER_TOKEN=kpr_… kuiper units -p my-agent ``` > **Keep tokens secret** > > An API token acts for your whole organisation. Put it in your CI system's secret store and never commit it. If one leaks, revoke it first and replace it after. See [Teams, roles and tokens](https://kuiper.sh/docs/teams/#api-tokens). ### Self-hosted instances Install from your own control plane and the CLI points at it straight away; `kuiper login` then needs no flags. ```sh curl -fsSL https://kuiper.example.com/install.sh | sh ``` To switch an existing install to another instance, set its address, or pass it once to `login`: ```sh kuiper config set api-url https://kuiper.example.com kuiper login --api-url https://kuiper.example.com ``` ### Where the CLI keeps its sign-in The CLI stores the instance URL, the token and your default project in `~/.config/kuiper/config.json`, or in `$XDG_CONFIG_HOME/kuiper/config.json` if you set that variable. The file is readable only by you. Three environment variables override it: | Variable | Overrides | | --- | --- | | `KUIPER_API_URL` | The instance the CLI talks to. | | `KUIPER_TOKEN` | The API token. | | `KUIPER_PROJECT` | The default project, the same as `-p`. | ## Sign in to the dashboard Open [app.kuiper.sh](https://app.kuiper.sh). The sign-in page offers GitHub and an API token. ![The Kuiper sign-in page with a Continue with GitHub button and an option to use an API token.](https://kuiper.sh/docs/img/signin.webp "The sign-in page.") - **Continue with GitHub** signs you in with the GitHub account your team invited. Someone who signs in without an invitation gets a personal organisation named after their GitHub login. See [Teams, roles and tokens](https://kuiper.sh/docs/teams.md). - **Use an API token** takes an organisation token, `kpr_…`. It is kept in your browser and sent only to Kuiper. A token session acts for the whole organisation, so the **Team** page shows no name for you. ### Add a passkey A passkey lets you sign in with your device instead of going through GitHub every time. It uses Touch ID, Windows Hello, a phone or a security key, whichever your device offers. 1. Sign in with GitHub. 2. Open the **Account** page: choose your name at the bottom of the sidebar, then **Account and passkeys**. Its address is `#/account`. 3. Add a passkey and follow your browser's prompt. Next time, choose **Sign in with a passkey** on the sign-in page. GitHub keeps working as a fallback. ## Next Your CLI is ready. [Deploy your first agent](https://kuiper.sh/docs/deploying.md), or take the [dashboard tour](https://kuiper.sh/docs/dashboard.md). --- # Dashboard tour > Every page of the Kuiper dashboard and what it is for, from the project overview to logs, usage and the Inspect cards. Web page: https://kuiper.sh/docs/dashboard/ The dashboard lives at [app.kuiper.sh](https://app.kuiper.sh). Everything it does, the CLI does too. The dashboard is the place to look at things: logs, runs, usage, and the state of your agents. ## The layout The sidebar on the left changes with where you are. At the top level it lists your projects. Inside a project it lists that project's pages. The bar above the content shows where you are, with a project switcher and an [environment](https://kuiper.sh/docs/environments.md) switcher on the left and **Add unit** on the right. ![The dashboard with the project navigation and the organisation links outlined in the sidebar.](https://kuiper.sh/docs/img/dashboard-overview.webp "The sidebar. Project pages sit in the upper group, organisation pages below.") A project's pages sit in three groups. Some entries hold several pages, which then show as tabs across the top of the page. | Entry | What it shows | | --- | --- | | Overview | One card per unit with a log preview, its address and its live deployment. | | Deployments | Every build and deployment of the project, newest first. | | Logs | Live output from any unit. | | Units | A table of units, and a form to create one. | | Agents | Runs and health of every Foundry and Station unit. | | Activity | A feed of what happened in the project. | | Usage | CPU and memory, used and reserved, what the project spent, plus agent activity. | | **Services** | | | Models | Model calls through your own provider keys: usage by model, unit or key, the call log and each call in detail. | | Browsers | Live browser sessions and their history, with recordings to replay. | | Sandboxes | Live sandboxes and their history. The **Images** tab lists Kuiper's ready-made sandbox images and how to start each one. | | **Configure** | | | Variables | Variables for each unit. | | Domains | Your own hostnames. | | Data | Tabs for **Databases** (managed Postgres and Redis), **Buckets** (S3-compatible object storage) and **Volumes** (persistent disks). | | Settings | **General**: default region, auto-restart, private registries and deleting the project. **Gateway keys**: keys for models, browsers and sandboxes used from outside Kuiper. | The **Organisation** group at the bottom of the sidebar is for the whole organisation: **Usage**, **Team** and **Access**. Below it, your name opens a menu with your **Account and passkeys**, the colour theme and **Sign out**. ### Find anything Press `F` or `Ctrl` + `K` (`⌘` + `K` on a Mac) to open **Find**, or use the **Search** button in the top bar. Type a project, a unit, a deployment (by ID, commit SHA or message), a domain, a variable or secret name, a database or a bucket, and press Enter. The same box runs actions such as restarting a unit or copying its address. See [Finding things](https://kuiper.sh/docs/finding.md). ![The Find palette with a few letters typed: units, actions and activity of one project.](https://kuiper.sh/docs/img/dashboard-palette.webp "Find. It jumps to any project, unit, deployment or page, and runs common actions.") ## Projects The first page lists your projects as cards. Each card shows the project's status, how many units it has and how many are running, and when it last changed. Use the search box to filter, and **New project** to start another. ![The projects page with a card for each project.](https://kuiper.sh/docs/img/dashboard-projects.webp "The projects page.") ## Project overview Open a project and the overview shows each unit as a card. The card previews the unit's latest log lines, and shows its address, its status and the deployment that is live. - **Visit** opens the unit's address. - **Roll back** redeploys the previous deployment. See [Rollbacks](https://kuiper.sh/docs/operations/#rollbacks). - **Open Foundry** or **Open dashboard** appears on Foundry and Station units. It opens the framework's own web interface. - The gear opens the unit's page. A new project also shows a short checklist: deploy a unit, connect a Git repository, add a secret, add a custom domain and turn on auto-restart. ## Units The **Units** page is a table. Each row shows a unit's kind, status, the node it runs on, its address, its current deployment and when it last changed. Below the table, a form creates a unit with a name and a type: agent, Station or service. ![The units table of a project with Foundry, Station and service units.](https://kuiper.sh/docs/img/dashboard-units.webp "The units table.") ### The unit page Click a unit to open its page. Two tabs sit above it: **Overview**, described here, and **Network**, which shows the requests the unit answers, the names it looks up and the connections it makes. See [Network logs](https://kuiper.sh/docs/network.md). From top to bottom the overview has: - the unit's status and kind, with **Visit**, **Logs** and, for Foundry and Station, a button to open the framework's dashboard; - the unit's address and its current deployment; - on Station and Foundry units, the **dashboard** card and the **Inspect** card, described below; - the unit's **Deployments**; - **Source**, where you connect a Git repository or drop in a folder to deploy; - **Build and run settings**; - the **Environment** of the live deployment, with a shortcut to edit it; - **Advanced**, with where the unit runs, a form to deploy an image and the unit ID. ![A Station unit page showing its status, address, dashboard card and the start of the Inspect card.](https://kuiper.sh/docs/img/unit-page.webp "A Station unit.") ### Inspect cards Station and Foundry units have an **Inspect** card. It reads the agent's own API through the node it runs on, so you see real runs without wiring up a thing. Kuiper holds the credentials. Your browser never sees them, and only reads are possible. A Station card has tabs for signals, runs, broadcasts, beacons, schedules, live events and API keys. ![The Inspect card of a Station headquarters with the tab bar outlined, showing signals.](https://kuiper.sh/docs/img/unit-inspect.webp "The Inspect card of a Station unit. Signals, with the last run of each.") Click a run, a broadcast run or a beacon instance and its details open in a drawer on the right. The list stays where it was. Close the drawer with the X or with `Esc`. ![A run opened in the right-hand drawer, with its input, output, steps and logs.](https://kuiper.sh/docs/img/unit-runs.webp "A run, opened in the drawer on the right.") The **Events** tab streams live events from the daemon. Press **Pin to side** and the stream moves to a drawer on the left. It keeps streaming while you look at runs and beacons. ![Live events pinned to a drawer on the left while a run is open on the right.](https://kuiper.sh/docs/img/unit-events.webp "Two drawers. Live events on the left, a run on the right.") A Foundry card has tabs for runs, conversations, events, approvals, activations, connections, capabilities and health. ![The Inspect card of a Foundry agent showing its health.](https://kuiper.sh/docs/img/unit-foundry.webp "The Inspect card of a Foundry unit.") The **API keys** tab of a Station card lists and mints keys for other services. See [Station units](https://kuiper.sh/docs/station/#api-keys-for-other-services). ## Deployments The **Deployments** page lists every deployment, and the builds that have not produced one yet. Filter by unit or status. The menu at the end of a row can inspect, redeploy or roll back, and copy the ID. ![A list of deployments with a Current badge on the live one of each unit.](https://kuiper.sh/docs/img/dashboard-deployments.webp "Deployments. The Current badge marks the live one.") Open a deployment for its details: when it was created, its status, how long it took, its source and its logs. The tabs are **Deployment**, **Logs**, **Resources** and **Source**. **Redeploy** runs it again with the current variables, and **Roll back to this** makes an earlier deployment the live one. ![The page of one deployment with its log preview and runtime logs.](https://kuiper.sh/docs/img/dashboard-deployment.webp "One deployment.") ## Logs Pick a unit, choose all output, stdout only or stderr only, and how many lines to load: 200, 500, 1000 or 5000. The viewer follows the output live. **Pause** freezes it so you can read. Press `/` to search inside the logs. ![The logs page showing live output from a Station unit.](https://kuiper.sh/docs/img/dashboard-logs.webp "Logs. Retained across crashes and redeploys, with secrets redacted.") Kuiper keeps each unit's logs across crashes and redeploys, and redacts secret values as lines are written. See [Logs, deployments and rollbacks](https://kuiper.sh/docs/operations.md). ## Activity **Activity** is a feed of what happened in the project, newest first: deployments starting and going live, Station API keys created, domains moved. ![The activity feed of a project.](https://kuiper.sh/docs/img/dashboard-activity.webp "Activity.") ## Usage **Usage** shows how much CPU and memory your units reserved and how much they used. Choose a period of 24 hours, 7 days or 30 days. You get totals, a chart per resource and a table by unit. Reserved resources are what drives placement and billing. A **Spend** section shows what the project cost in the period, by resource and by unit; the organisation's whole bill is on **Usage** at the bottom of the sidebar ([Usage and billing](https://kuiper.sh/docs/usage.md)). ![The usage page with totals, CPU and memory charts and a table by unit.](https://kuiper.sh/docs/img/dashboard-usage.webp "Usage over the last 30 days.") Below the table, **Agent activity** counts what your agents did in the same period. For Foundry units that is runs, generations, tool calls, tokens and approvals waiting. For Station units it is signal runs, broadcasts, beacons and beacon restarts. ![The agent activity section with totals and a chart of runs, and a row per Foundry and Station unit.](https://kuiper.sh/docs/img/dashboard-agent-activity.webp "Agent activity.") ## Browsers and sandboxes **Browsers** lists the project's live [browser sessions](https://kuiper.sh/docs/browsers.md), with the unit or key that made each one, and their history with how each ended. Owners and admins see what each session cost, watch a live one, and replay a recorded one. **Sandboxes** does the same for [sandboxes](https://kuiper.sh/docs/sandboxes.md), with each one's image, size and exposed ports. Both pages can release a session or sandbox. **Models** shows the project's [model calls](https://kuiper.sh/docs/models.md): usage by model, unit, gateway key, provider key or day, a call log you can filter, and each call's tokens, timings and estimated cost in a drawer. Owners and admins switch on keeping request and answer bodies there, and read them. The organisation's provider keys are under **Access**, then **Providers**, at the foot of the sidebar. **Settings**, then **Gateway keys**, creates and revokes the keys that agents outside Kuiper use for models, browsers and sandboxes ([Gateway keys](https://kuiper.sh/docs/gateway-keys.md)). The **Images** tab of **Sandboxes** lists Kuiper's [ready-made images](https://kuiper.sh/docs/sandbox-images.md) with a snippet to copy for each. The organisation's **Usage** page has a **Budgets** section, and a budget's alerts show as a banner across the dashboard. See [Budgets and limits](https://kuiper.sh/docs/budgets.md). ## Agents The **Agents** page shows every Foundry and Station unit in the project on one page. A Foundry unit lists its agents with instances, runs and failures. A Station unit lists its signals, with runs seen, running, failed and the time of the last run, and counts its broadcasts, beacons and schedules. ![The agents page with a Foundry agent and Station daemons.](https://kuiper.sh/docs/img/dashboard-agents.webp "Agents. Health for every agent in the project.") ## Variables, domains, data and settings These pages manage what your units run with. Each has its own guide. - **Variables** edits a unit's variables. [Variables and secrets](https://kuiper.sh/docs/variables.md). - **Domains** adds your own hostnames. [Domains](https://kuiper.sh/docs/domains.md). - **Data** has **Databases** and **Buckets** tabs, which create managed Postgres, Redis and object storage. A bucket's **Browser access** lets the websites you choose read or upload its files straight from the browser. [Databases, Redis and buckets](https://kuiper.sh/docs/databases.md). - **Settings** sets the project's default region, turns on auto-restart and stores private registry credentials. It is also where you delete the project. ![The environment variables page with a list of variables, some of them secrets.](https://kuiper.sh/docs/img/dashboard-env.webp "Environment variables for a unit.") ![The project settings page with the general details, default region, auto-restart and private registries.](https://kuiper.sh/docs/img/dashboard-settings.webp "Project settings.") ## Team, Git and API tokens These organisation pages sit at the bottom of the sidebar. [Teams, roles and tokens](https://kuiper.sh/docs/teams.md) covers each in detail. - **Team** lists members, invites people and sets roles. - **Access** has four tabs. **Git** connects GitHub and stores access tokens for other Git hosts. **API tokens** creates and revokes the tokens that the CLI and CI use. **Providers** holds the organisation's own provider keys, a section for each service ([Models](https://kuiper.sh/docs/models.md)). **MCP** connects an agent to Kuiper ([Connect an agent](https://kuiper.sh/docs/mcp.md)). ![The Git page with a connected GitHub account.](https://kuiper.sh/docs/img/dashboard-git.webp "Git. A connected GitHub App installation, and a form for personal access tokens.") ## Colour theme Open the menu under your name at the bottom of the sidebar and choose **Theme** to cycle through system, light and dark. System follows your operating system. **Find** has the same action. ![The project overview in the light theme.](https://kuiper.sh/docs/img/dashboard-light.webp "The light theme.") --- # Finding things > Jump to any unit, deployment, domain or variable with Ctrl-K, search a project from the CLI, and filter logs with @level:error. The full reference for the search language. Web page: https://kuiper.sh/docs/finding/ Everything in a project can be found by typing a few letters. The dashboard has one search for names and one for logs, and the CLI has both. They share a single filter language, described in full at the end of this page. ## Find, the palette Press `Ctrl` + `K` (`⌘` + `K` on a Mac) anywhere in the dashboard. `F` and `/` do the same when your cursor is not in a text box, and the **Search** button in the top bar does too. (Inside a log panel, `/` goes to that log's filter instead.) ![The palette with a few letters typed: units, actions and activity of one project.](https://kuiper.sh/docs/img/dashboard-palette.webp "Find. Results from the page appear at once, results from the server a moment later.") Type a few letters. Kuiper answers from what the page already knows (projects, the open project's units, pages and actions) and, a moment later, from a search of the project: | Group | What matches | | --- | --- | | Units | The unit's name, its address (`web-lab.kiprsh.app`) or its kind (`station`). | | Deployments | The start of its ID (with or without `dep_`), the start of its commit SHA, its number (`#12`), its branch, its status, and the commit message and author of pushed commits. Newest first. | | Domains | Your custom domains and the host they redirect to. | | Variables | Variable names of every unit, and the names of shared values. | | Secrets | Project secret names. | | Databases and Buckets | Their name or engine. | | Activity | The text of recent project events. | Variable and secret **values are never searched and never shown**, only their names. Matching ignores case, and `%` and `_` mean themselves. With no project open, the palette searches the whole organisation: projects (by name or slug), units and domains. | Key | What it does | | --- | --- | | `↑` `↓` | Move through the results. | | `Tab` and `Shift` + `Tab` | Jump to the next or previous group. | | `Enter` | Open the result, or run the action. | | `Esc` | Close. Inside a confirmation, go back one step. | Opened results are remembered and listed under **Recent** the next time you open the palette with nothing typed. ### Actions Commands appear in the same list. On a unit's page the palette lists that unit's actions first. Anywhere else, type the action: `restart web`, `copy url web`, `logs web`. | Action | What it does | | --- | --- | | Deploy `unit` | Builds the unit from its linked repository and deploys it. | | Redeploy `unit` | Deploys the live image again with the current environment. | | Restart `unit` | Applies pending variable changes. | | Open logs of `unit` | Opens **Logs** on that unit. | | Open settings of `unit` | Opens the unit's page, with its build and run settings. | | Copy URL of `unit` | Copies `https://` and the unit's address. | | Switch to `organisation` | Changes organisation, for people who belong to more than one. | | Toggle theme | Cycles system, light and dark. | | Sign out | Ends your session. | Deploy, Redeploy and Restart change something, so they ask first and need the admin or owner role. Press `Enter` to confirm or `Esc` to go back. ## Search from the CLI ```sh kuiper search web kuiper search 3f2a1b -p lab kuiper search "fix timeout" kuiper search web --org ``` Without `-p` and a default project, `kuiper search` searches the whole organisation. `--org` does that even when a project is set, and `--limit N` changes how many results each group shows (up to 50). `--json` prints the raw answer, which has a dashboard `route` and an API `path` for every result. The same search is an API call: `GET /v1/projects/{project}/search?q=…&limit=…`, and `GET /v1/search?q=…` for the organisation. ## Filter logs Every log panel has a filter box: **Logs** (any unit), a deployment's **Logs** tab and its **Runtime logs** card, and the **Build logs** of a build. Press `/` while a log is focused, or on the **Logs** page, to jump to it. ![The Logs page with JSON lines shown as a level, a message and attributes.](https://kuiper.sh/docs/img/dashboard-logs.webp "Logs. JSON lines become a level, a message and attribute chips.") The filter runs on Kuiper, over the newest 20,000 lines the node kept, not just the lines on screen. It keeps working while the log follows live. - **JSON lines** are shown as the time, a level badge, the message and the rest of the keys as `key=value` chips. Click a line to see the JSON. Click a chip to add it to the filter, or shift-click to leave lines with it out. Click a level badge to filter by level. Nested keys read as `http.status`. - **Plain lines** stay as they are, with ANSI colours drawn instead of printed as escape codes. - **Matches** shows only the lines that match. **All lines** shows every line and marks the matches, so you can read around them. `Enter` and `Shift` + `Enter` (or the arrows) jump to the next and previous match, and the box shows how many there are. - **Wrap** switches between wrapped lines and one line per row. **Time** hides the timestamps. - The download button saves the log as a text file. With a filter on, it saves the lines that match. ```sh kuiper logs -u web -q '@level:error' kuiper logs -u web -q '@level:error,warn -health @user_id:42' --tail 100 kuiper logs -u web -f -q '@rid:r-1003' ``` `kuiper logs -q` (or `--filter`) takes the same filter and prints the last `--tail` matching lines. With `-f` it follows. The API is `GET /v1/projects/{project}/units/{unit}/logs?q=…`, and it answers with how many lines matched and were scanned. `GET /v1/builds/{id}/log?q=…` does the same for a build, and both have a `/download` form that returns the whole log as a file. ## The filter language One small language is used everywhere: the log filter, the `-q` flag, and (in part) the palette. Terms are separated by spaces, and all of them must hold. ```text @level:error @user_id:42 timeout -health "connection reset" ``` ### Words | You type | It matches | | --- | --- | | `timeout` | Lines that contain `timeout`, in any case. | | `"connection reset"` | Lines that contain that exact phrase. | | `-health` | Lines that do **not** contain `health`. A leading `-` negates any term. | A word searches the line without its timestamp and stream. ANSI colour codes are ignored, so `ERROR` printed in red still matches `error`. ### Fields: `@key:value` `@key:value` matches one field of a line. Keys ignore case. The value can be: | Value | It matches | Example | | --- | --- | --- | | `text` | That exact text, ignoring case. | `@level:error` | | `a*` | A wildcard. `*` matches anything. | `@path:/api/*`, `@msg:*timeout*` | | `>n`, `>=n`, `500` | | `a..b` | A number from `a` to `b`, inclusive. | `@http.status:400..499` | | `5xx` | Shorthand for `500..599`. | `@http.status:5xx` | | `a,b,c` | Any of the values. | `@level:warn,error` | A number can end in `ms` or `s` (`250ms`, `1.5s`); seconds are turned into milliseconds. A value with spaces goes in quotes: `@msg:"two words"`. `-@level:debug` leaves debug lines out. A line that has no such field never matches a positive field term and always matches a negated one. A malformed term (`@level` with no value, a number where text was expected) is an error shown next to the box, never a silent empty result. ### Fields of runtime logs | Field | Also written | What it is | | --- | --- | --- | | `@level` | `@lvl`, `@severity` | `trace`, `debug`, `info`, `warn`, `error` or `fatal`. Read from a JSON `level` (a name, or a pino or bunyan number from 10 to 60), a logfmt `level=`, or the start of a plain line (`ERROR …`, `[warn] …`, `Error: …`). Names such as `warning`, `err` and `critical` map to these. A lower-case word in the middle of a sentence is not a level. | | `@stream` | `@fd` | `stdout` or `stderr`. | | `@deployment` | `@dep` | The deployment the log belongs to. `@deployment:dep_x` picks that deployment's log; wildcards and `-` are checked against the log being read. | | `@rid` | `@request_id`, `@req_id` | A request ID: a JSON `rid` or `request_id` key, or `rid=…` in a logfmt line. | | `@message` | `@msg` | A JSON `message` or `msg`, or the whole line when it is not JSON. | | `@any.other.key` | | Any key of a JSON line, or any `key=value` pair of a logfmt line. A nested key is written with dots. | A line is JSON when it is an object (`{ … }`). A line is logfmt when it has `key=value` pairs (`level=info msg="two words" user_id=7`). ### Fields of build logs | Field | What it is | | --- | --- | | `@level` | As above. `error: …`, `npm ERR!` and `npm WARN` count. | | `@line` | The line's number in the log. `@line:>100` skips the first 100. | | `@message`, `@msg` | The line. | Build logs have no streams and no JSON attributes, so any other `@key` is an error. ### Examples | Filter | Finds | | --- | --- | | `@level:error` | Every error line. | | `@level:warn,error -@stream:stdout` | Warnings and errors written to stderr. | | `@http.status:5xx @path:/api/*` | Server errors under `/api`, in JSON request logs. | | `@user_id:42 -@level:debug` | What one user did, without the debug noise. | | `@rid:r-1003` | Everything one request logged. | | `@ms:>1s "payment"` | Slow lines that mention payment. | | `-health -ping` | Everything except health checks. | ### Limits - A search reads the newest 20,000 lines the node holds. When the log is longer, Kuiper says it searched the newest lines. - One answer carries at most 5,000 lines. A download carries the whole retained log (up to 200,000 lines). - A node that runs an older kuiperd serves at most 5,000 lines per request, so a search or a download there covers the newest 5,000. - Logs are filtered as stored: redaction of secret values is unchanged, and so is the text of every line. --- # Connect an agent (MCP) > Let Claude Code, Claude Desktop, claude.ai, Cursor, VS Code or any MCP client see your Kuiper dashboard and, if you allow it, deploy, restart and change configuration. It signs in with OAuth and you can disconnect it at any time. Web page: https://kuiper.sh/docs/mcp/ Kuiper is an MCP server. Connect an agent to it and the agent sees what the dashboard shows: projects, environments, units, deployments, builds, logs, network logs, usage and activity. If you allow it, the agent can also deploy, restart, roll back, set variables and secrets, manage domains and environments, and create or revoke API tokens. You don't paste a token anywhere. The agent signs in with OAuth: it opens the dashboard, you approve it for one organisation with read-only or read and write access, and it keeps itself signed in from then on. ## The server | What | Value | | --- | --- | | Server URL | `https://app.kuiper.sh/mcp` | | Transport | Streamable HTTP | | Sign-in | OAuth 2.1 with dynamic client registration and PKCE | | Protocol versions | 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05 | The URL is also on the dashboard: open **Access**, then **MCP**. If you run Kuiper yourself, use your dashboard's address followed by `/mcp`. ## Set up your client The dashboard's **MCP** page has each of these with your server URL filled in and a copy button. ### Claude Code ```sh claude mcp add --transport http kuiper https://app.kuiper.sh/mcp ``` Then run `/mcp` in Claude Code, select **kuiper** and choose **Authenticate**. Your browser opens the dashboard to approve it. Add `--scope user` to use Kuiper in every project, or `--scope project` to share the server with your team in `.mcp.json`. Each person still approves their own connection. ### Claude Desktop and claude.ai 1. Open **Settings**, then **Connectors**, and choose **Add custom connector**. 2. Name it `Kuiper` and paste `https://app.kuiper.sh/mcp` as the URL. 3. Choose **Connect** and approve it in the dashboard. On Team and Enterprise plans an owner adds the connector once for the organisation. Each person then connects it for themselves. ### Cursor Add Kuiper to `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one: ```json mcp.json { "mcpServers": { "kuiper": { "url": "https://app.kuiper.sh/mcp" } } } ``` Then open **Settings**, then **MCP**, and choose **Connect** next to kuiper. ### VS Code Add Kuiper to `.vscode/mcp.json` in your workspace, or run **MCP: Add Server…** from the command palette and choose HTTP: ```json .vscode/mcp.json { "servers": { "kuiper": { "type": "http", "url": "https://app.kuiper.sh/mcp" } } } ``` Start the server from the file or the **MCP Servers** list. VS Code asks you to sign in, and the dashboard opens to approve it. ### Other clients Any client that can connect to a remote MCP server over Streamable HTTP with OAuth works. Give it the server URL. It finds the sign-in details at `/.well-known/oauth-protected-resource`, registers itself, sends you to the dashboard to approve, and refreshes its own access afterwards. A client that can't do OAuth can't connect. The MCP server never takes API tokens. ## Approving an agent When a client connects for the first time, your browser opens a Kuiper page that asks whether to allow it. Sign in first if you aren't already. The page shows: - the name the client gave itself, such as `Claude Code`; - where you will be sent back to: a website's host, `127.0.0.1 (this computer)` for a desktop tool, or an app's link; - the organisation to connect, if you are in more than one; - the access to give it. Only approve a request you just started. If you don't recognise the name, choose **Deny**: the client is told no and gets nothing. Approving needs a person, so sign in with GitHub or a passkey. A dashboard signed in with an API token can't approve. A request lasts ten minutes. ### Access | Access | Scope | The agent can | | --- | --- | --- | | **Read only** (the default) | `kuiper:read` | Use the read tools: projects, environments, units, deployments, builds, logs, network logs, variables, domains, usage, activity, search and the list of API tokens. Secret values are never shown. | | **Read and write** | `kuiper:read kuiper:write` | Everything above, and the write tools: deploy, restart, roll back, set and unset variables and secrets, add and remove domains, create and sync environments, and create and revoke API tokens. | The agent acts as you, with your role in that organisation. A member's agent can do what a member can and no more, whatever access you give it. If your role changes, the agent's access changes with it. A read-only agent doesn't see the write tools. If it asks for one anyway, Kuiper tells the client it needs `kuiper:write`. Some clients then offer to sign in again. To give an agent write access, approve it again and choose **Read and write**: the same connection is updated. ## Tools Every tool takes project and unit names the way the CLI does: a slug, an ID or a unique name, and `slug@env` for another environment, as in `proxima@staging`. Long answers are cut short and say so, so they fit in an agent's context. ### Read | Tool | What it does | | --- | --- | | `list_projects` | The organisation's projects with their environments, unit counts and how many units are running. | | `get_project` | One project: its environments and every unit with status, address and live deployment. | | `list_units` | A project's units with status, address, framework, region and live deployment. | | `get_unit` | One unit in detail: placements, node, region, resources, health check, pending restart, live and latest deployment. | | `list_deployments` | A unit's deployments, newest first, ten to a page. | | `get_deployment` | One deployment: status, error, image, source, the variable names it started with, and where it runs. | | `list_builds` | A unit's recent builds: status, branch, commit, error and the deployment each produced. | | `get_build` | One build and the end of its log. `q` filters the log, as in `@level:error`. | | `get_logs` | A unit's logs. `q` takes the [log filter language](https://kuiper.sh/docs/finding/#the-filter-language), `tail` up to 500 lines, `since` to read on from a time. | | `get_network` | A unit's traffic summary, or recent HTTP, DNS and connection rows, filtered like the [Network page](https://kuiper.sh/docs/network.md). | | `get_usage` | Without a project: the organisation's spend for the month (or a `month` you name) per resource, project and unit, with month to date and the projected month end. With a project: its reserved and used CPU and memory per unit and its spend. Without a project it also lists the organisation's [budgets](https://kuiper.sh/docs/budgets.md). Members see quantities; owners and admins also see amounts. | | `list_variables` | A unit's variables, the project's shared variables and its secret names. Never secret values. | | `list_domains` | A project's domains with their DNS and certificate status and the records still needed. | | `list_environments` | A project's environments with branch and unit status. | | `diff_environment` | What differs between two environments. Its keys are what `sync_environment` takes. | | `search` | Search the organisation or one project: units, deployments, domains, variables, secret names, databases and activity. | | `get_activity` | A project's activity log, or with `kind: agents`, what its Foundry and Station agents did over some days. | | `list_browser_sessions` | A project's [browser sessions](https://kuiper.sh/docs/browsers.md): live ones and the history, with how each ended and, for owners and admins, what each cost. | | `list_sandboxes` | A project's [sandboxes](https://kuiper.sh/docs/sandboxes.md): live ones with their image, size and ports, and the history, with what each cost for owners and admins. Sandboxes are made and used with a gateway key, not through MCP. | | `list_model_calls` | A project's [model calls](https://kuiper.sh/docs/models.md), newest first: model and provider key, status and error, tokens, timings, conversation and, for owners and admins, the estimated cost at the provider. Filters by conversation and by what a logged call held. Never prompts or completions. | | `get_model_call` | One model call. For owners and admins of a project that logs calls, its trace too: parameters, tools offered, and the messages in and out with tool calls, results and reasoning. | | `get_model_usage` | Model calls, tokens and estimated provider cost from the hourly totals: a project's by model, unit, key, provider or day, or the organisation's by model this month. | | `list_api_tokens` | The organisation's API tokens: name, who made it, when it was last used. Never the token. | ### Write These need **Read and write** access. Each one is recorded in the project's activity as done by you through the client, for example `secret STRIPE_KEY set by alice via Claude Code (MCP)`. | Tool | What it does | | --- | --- | | `deploy` | Build the unit's repository and deploy it, or redeploy the live image with `source: redeploy`. | | `restart_unit` | Restart a unit on its live image, applying pending variable and secret changes. | | `rollback` | Make an earlier deployment live again. | | `set_variable` | Set a unit variable, or a shared variable when no unit is given. Values are [templates](https://kuiper.sh/docs/variables.md), like `${{secrets.API_KEY}}`. | | `unset_variable` | Remove a unit or shared variable. | | `set_secret` | Set a project secret. The value is encrypted and can't be read back, by the agent or anyone else. | | `delete_secret` | Delete a secret that no variable uses. | | `add_domain` | Add a custom domain to a unit and get the DNS records to create. | | `verify_domain` | Check a domain's DNS now. | | `remove_domain` | Remove a custom domain. | | `create_environment` | Create an environment, such as staging, as a copy of another. | | `sync_environment` | Bring chosen differences from one environment into another. | | `create_api_token` | Create an organisation API token. The token is shown once, in the answer. | | `revoke_api_token` | Revoke an API token. The last active one can't be revoked. | Kuiper also offers each project as an MCP resource (`kuiper://projects/`) and a `diagnose_unit` prompt that walks an agent through a unit's status, logs and traffic. ## Seeing and revoking access Open **Access**, then **MCP** or **API tokens**, to see your connected agents: the client's name, the organisation, the access, when it was connected and when it was last used. Owners and admins also see everyone's agents in the organisation, and can revoke them. **Revoke** disconnects an agent at once. Its next request fails and it has to be approved again. An agent also loses access when the person who approved it leaves the organisation. From the client's side, removing the server (for example `claude mcp remove kuiper`) forgets it locally. Revoke it in the dashboard too, to be sure. ## Security - **Short-lived tokens.** An access token lasts an hour. The refresh token that renews it lasts 30 days and works once: each use returns a new one. If an old refresh token is ever used again, Kuiper assumes it was copied and disconnects that agent. - **Bound to one place.** A token works only for this Kuiper server's `/mcp`, only for the organisation and person who approved it, and only with the access they chose. Kuiper stores hashes of tokens, never the tokens. - **PKCE and checked return addresses.** Sign-in uses the authorization code flow with PKCE (S256). Kuiper sends you back only to an address the client registered: an `https://` address, `http://` on your own computer (`127.0.0.1`, `localhost` or `[::1]`, any port), or an app's own link. - **The approval page can't be embedded** in another site, and approving needs the dashboard's own request headers, so another site can't approve for you. - **Secrets stay write-only.** No tool returns a secret value, a database password or an API token after it was created. - **Think about what the agent reads.** Logs, network rows and build output are written by your apps and by whoever calls them. An agent that reads them can be misled by text planted there. Give write access to agents you supervise, and have them ask before destructive changes. - **Write access is your access.** An agent with write access can do what you can, including creating API tokens that act for the whole organisation. Prefer read only unless the agent needs to act. --- # Deploying > Deploy from a Git repository, from a folder or from an image, and use kuiper.json when you need to change what Kuiper detects. Web page: https://kuiper.sh/docs/deploying/ ## Three ways to deploy Every deployment starts from some source. Kuiper builds it into an image, starts the image as a unit and waits for it to be healthy before it takes traffic. | Source | Use it when | Command | | --- | --- | --- | | Git repository | You want every push to deploy. | `kuiper link`, then `kuiper build` | | Folder | You want to ship what is on your disk, with no repository. | `kuiper deploy --source` | | Image | You already build images elsewhere. | `kuiper deploy --image` | In the dashboard, all three are tabs on the **New project** page. A unit's page offers them under **Source** and **Advanced**. ## From Git Kuiper builds a unit from a repository and deploys on every push to the branch you choose. A public repository needs no setup. A private one needs a credential, and the easiest is the Kuiper GitHub App. ### Connect GitHub Open **Access**, then **Git**, and choose **Connect GitHub**. The same button appears on the **New project** page the first time. 1. GitHub asks where to install the app and which repositories it may read. 2. GitHub asks you to authorise Kuiper. Kuiper links the installation only if your GitHub account can see it. 3. You land back in the dashboard with your account named. The connection shows up as a credential called `github-`. Each build gets its own token. The token reads one repository, is read-only and expires within an hour. Pushes build automatically, with no webhook to add. ### Pick a repository Open **New project** and stay on the **Git repository** tab. Kuiper lists the repositories it can read. Pick one with **Import**. ![The New project page with a list of repositories and an Import button on each.](https://kuiper.sh/docs/img/dashboard-new-git.webp "New project. Pick a repository, name the project and the unit, then deploy.") Kuiper then inspects the repository through the GitHub API, without cloning it. It shows what it found: - the framework, Foundry, Station or another Node app; - the package manager; - the directory to deploy, with a dropdown when a monorepo holds several apps; - the install, build and start commands, and the port; - the environment variable names the code reads. Check the values, fill in the **Project** and **Unit name**, choose a **Type** and press **Deploy**. Values that match what Kuiper detects are not saved as overrides, so detection keeps up when the repository changes. A repository missing from the list was not granted to the app. Use **Grant Kuiper access to it on GitHub** to add it. ### How detection works Kuiper recognises two frameworks. - **Glove Foundry** from a `foundry.config.ts` (or `.mts`, `.js`, `.mjs`) or a `glove-foundry` dependency. See [Glove Foundry units](https://kuiper.sh/docs/foundry.md). - **Station** from a `station.config.*` file or a `station-daemon` dependency. See [Station units](https://kuiper.sh/docs/station.md). Both deploy with no Dockerfile and no `kuiper.json`. For any other Node app, give Kuiper a start command and a port in **Build and run settings** and it writes a minimal manifest for you. ### Push to deploy When you link a repository, Kuiper tells you how pushes reach it. | Mode | What it means | | --- | --- | | `github_app` | Pushes arrive through the Kuiper GitHub App. Nothing to do. | | `created` | Kuiper added a webhook to the repository with your token. | | `manual` | Add the webhook Kuiper shows you: content type `application/json`, push events only. The secret is shown once. | A push builds the unit when its branch matches the linked branch. Kuiper reports each build back to GitHub as a commit status named `kuiper//`, and a build that deploys also creates a GitHub deployment. Open a pull request and you see the status next to it. These are commit statuses and deployment records, not GitHub Actions runs. No logs, variables or secrets are sent to GitHub. ### From the CLI Create the unit, link the repository and queue the first build: ```sh kuiper units create web kuiper link -u web --repo https://github.com/acme/web --branch main kuiper build -u web ``` `kuiper build` also takes a Git ref, and can build without deploying. ```sh kuiper build -u web --ref v1.2.0 kuiper build -u web --no-deploy kuiper builds -u web ``` `kuiper link` takes `--root-dir` for a monorepo package, `--build-env NAME=VALUE` for variables the build needs, and `--credential` to name the credential to clone with. Build variables are never secrets. Runtime secrets are not available to the build. If you have more than one credential, the first match wins: 1. the credential named on the link, with `--credential`; 2. the GitHub App installation on the repository's owner; 3. your organisation's only token for that host; 4. none, which clones anonymously. ### Build and run settings When detection guesses wrong, change the settings per unit. They apply from the next build. The running deployment does not change until then. | Setting | Replaces | | --- | --- | | Root directory | The repository root, for example `apps/agent` in a monorepo. | | Install command | The detected install, for example `pnpm install --filter agent...`. | | Build command | The detected build, for example `pnpm build`. | | Start command | The manifest's entrypoint. It runs with `sh -c`, for example `node dist/server.js`. | | Port | The manifest's first port. | | Dashboard link | Nothing. It adds an **Open dashboard** button to the unit. | They live under **Build and run settings** on the unit page, and in the CLI: ```sh kuiper settings -u web kuiper settings -u web --root-dir apps/agent --start-command "node dist/server.js" --port 3000 kuiper settings -u web --build-command "" kuiper build -u web ``` An empty value clears a setting. The install and build commands apply only when Kuiper generates the Dockerfile, that is when your repository has none. ## From a folder You do not need a repository. Kuiper can build what is on your disk. In the dashboard, open **New project**, choose **Upload a folder** and drop a folder or a `.tar.gz` on it. On an existing unit, use **Upload and deploy** under **Source**. ![The Upload a folder tab with a drop zone.](https://kuiper.sh/docs/img/dashboard-new-upload.webp "Upload a folder.") From the CLI: ```sh kuiper deploy --source . -u web --wait ``` The CLI packs the directory and uploads it. A builder builds it exactly as it would a repository: your Dockerfile if there is one, otherwise the generated one for Foundry and Station. `--wait` streams the build log and returns when the deployment is live, or fails. What gets uploaded: - **In a Git work tree:** tracked files, plus untracked files that `.gitignore` does not exclude. Uncommitted changes go too, and the build is labelled `@+dirty`. - **Anywhere else:** everything except `.git`, `node_modules` and `target`. - **Never:** `.env` and `.env.*` files. Put secrets in `kuiper secrets` so they never end up in an image. Files like `.env.example` are kept. - Symlinks are left out. An upload is limited to 200 MiB compressed. ## From an image If you build images yourself, deploy one directly. Kuiper needs the image and its manifest. In the dashboard, choose the **Image** tab and fill in the image and the manifest. ![The Image tab with an image reference and a manifest editor.](https://kuiper.sh/docs/img/dashboard-new-image.webp "Deploy an image with its manifest.") From the CLI, the manifest is the `kuiper.json` on disk: ```sh kuiper deploy --image ghcr.io/acme/agent:1.4.0 -u web --manifest kuiper.json --wait ``` `--manifest` defaults to `kuiper.json` in the current directory. For a private image, give Kuiper credentials for the registry once: ```sh kuiper registry add ghcr.io --username your-github-user kuiper registry list ``` The command prompts for the token. For GitHub's registry, use a token with `read:packages`. Credentials belong to one project. They are sealed on write and never shown again. ## kuiper.json `kuiper.json` is the manifest. It says what the code needs: which ports to open, which variable names it reads, how much CPU and memory to reserve, and how to check that it is healthy. It never holds values. Where values come from is set in the project, see [Variables and secrets](https://kuiper.sh/docs/variables.md). Foundry and Station apps do not need one. The builder writes the same file it would infer, and uses a committed `kuiper.json` as it is. Write one by hand for anything else, or to change a default. ```json kuiper.json { "schemaVersion": 1, "project": "my-agent", "runtime": { "entrypoint": ["node", "dist/server.js"] }, "ports": [{ "name": "http", "port": 8080, "public": true }], "env": { "required": ["DATABASE_URL", "ANTHROPIC_API_KEY"], "optional": ["LOG_LEVEL"] }, "resources": { "request": { "cpu": 0.5, "memoryMiB": 512 }, "limit": { "cpu": 1, "memoryMiB": 1024 }, "volumeGiB": 5 }, "health": { "http": "/healthz", "intervalSec": 10 } } ``` | Field | What it does | | --- | --- | | `schemaVersion` | Must be `1`. | | `project` | A name for the app. It is informational. You pick the Kuiper project when you deploy. | | `runtime.entrypoint` | The program to run, as a list, for example `["node", "dist/server.js"]`. | | `ports` | Named ports. A port with `"public": true` gets an address on the apps domain. | | `env.required` and `env.optional` | The variable names the code reads. A deploy fails if a required name has no value. | | `resources.request` | What Kuiper reserves for the unit. It drives placement and billing. | | `resources.limit` | The hard ceiling. It must be at least the request. | | `resources.volumeGiB` | The size of the unit's persistent disk, mounted at `/data`. | | `health` | An HTTP path to probe, or `"tcp": true` to check that the port accepts connections. | | `capabilities` | `sandbox` and `browser`, for apps that start child containers. | | `placement.region` | The region the unit prefers. | Unknown keys are rejected, so a typo is an error and not a silent no-op. Three CLI commands work on the file. They run offline. ```sh kuiper manifest init kuiper manifest validate kuiper manifest schema ``` - `kuiper manifest init` writes the `kuiper.json` the builder would infer for a Foundry or Station app. Pass `--foundry` or `--station` when nothing is detected, `--out` to choose the file, and `--force` to overwrite an existing one. Edit the file, then commit it. - `kuiper manifest validate [PATH]` checks a file and prints what it declares. - `kuiper manifest schema` prints the JSON Schema, which you can point your editor at. > **One replica when there is a volume** > > A unit with a volume runs as one instance. On a deploy, Kuiper stops the old instance before it starts the new one, so the unit is briefly absent. A unit without a volume starts the new instance first and switches traffic when it is healthy. ## Next Your unit is running. [Give it variables and secrets](https://kuiper.sh/docs/variables.md), [read its logs](https://kuiper.sh/docs/operations.md), or [add a staging environment](https://kuiper.sh/docs/environments.md) that builds from its own branch. --- # Variables and secrets > Give a unit its settings. Values are templates that can point at a secret, a database or another unit, and a restart applies them. Web page: https://kuiper.sh/docs/variables/ ## How variables work A variable is a setting your code reads from its environment, like `DATABASE_URL` or `LOG_LEVEL`. You choose every name. Kuiper never injects fixed names that your app has to adopt. If your code reads `PG_CONN`, you set `PG_CONN`. The value of a variable is a **template**. It can be plain text, or it can point at something else in the project. Kuiper fills in the pointers when it deploys the unit. | Template | Where the value comes from | | --- | --- | | `info` | Plain text, such as the value in `LOG_LEVEL=info`. | | `${{secrets.ANTHROPIC_API_KEY}}` | A project secret. | | `${{shared.LOG_LEVEL}}` | A shared project variable. | | `${{main.DATABASE_URL}}` | An output of a managed database called `main`. | | `${{worker.INTERNAL_URL}}` | An output of another unit called `worker`. | | `${{neon.DATABASE_URL}}` | An output of an external resource called `neon`. | | `${{browsers.API_KEY}}` | A key for Kuiper's [browsers](https://kuiper.sh/docs/browsers.md), made for each running copy of the unit. `${{sandboxes.API_KEY}}` and `${{models.API_KEY}}` do the same for [sandboxes](https://kuiper.sh/docs/sandboxes.md) and [models](https://kuiper.sh/docs/models.md). | | `postgres://${{db.PGUSER}}:${{db.PGPASSWORD}}@${{db.PGHOST}}:5432/app` | Text and pointers mixed, to build your own string. | The pattern is always `${{.}}`. The name is the slug of a unit, a database, a bucket or an external resource, or one of the words `secrets`, `shared`, `models`, `browsers` and `sandboxes`. The output is the value it exports. Shell-style `${VAR}` passes through untouched. To write a literal `${{`, double the dollar sign: `$${{`. Templates keep your config stable. When a database moves or a secret rotates, the pointer stays the same and Kuiper resolves it to the new value. ## Set variables In the dashboard, open **Variables**, choose a unit, type a name and a value, and press **Save**. The lock switch next to the value stores it as a secret. **Import .env** takes a pasted `.env` file. ![The Environment variables page showing a unit's variables, some stored as secrets.](https://kuiper.sh/docs/img/dashboard-env.webp "Environment variables. Secrets show a lock and are never displayed.") Kuiper also reads your source for the variable names it uses, and lists the ones the unit does not have yet as **Found in your code** chips. Click a chip to fill in its name. Names that look sensitive, such as `…KEY`, `…TOKEN`, `…SECRET` and `…PASSWORD`, default to secrets. From the CLI, `kuiper env set` takes `NAME=TEMPLATE` pairs. ```sh kuiper env set -u web LOG_LEVEL=info kuiper env set -u web 'DATABASE_URL=${{main.DATABASE_URL}}' 'REDIS_URL=${{cache.REDIS_URL}}' kuiper env list -u web kuiper env unset -u web LOG_LEVEL ``` > **Quote templates** > > Wrap any value that contains `${{` in single quotes. Without them your shell may try to expand it before Kuiper sees it. `kuiper env list` shows each variable's template and whether it is secret. It never shows a secret's value. ## Secrets A secret is a value that Kuiper stores sealed. It is write-only: you can replace it or remove it, but nothing ever shows it again. Logs and build output redact secret values. ```sh printf %s "$ANTHROPIC_API_KEY" | kuiper secrets set ANTHROPIC_API_KEY=- kuiper env set -u web 'ANTHROPIC_API_KEY=${{secrets.ANTHROPIC_API_KEY}}' ``` `NAME=-` reads the value from standard input, which keeps it out of your shell history. You can also pass `NAME=VALUE`, which does not. ```sh kuiper secrets list kuiper secrets rm ANTHROPIC_API_KEY ``` `kuiper secrets list` shows each secret's version, when it was updated and which variables reference it. A variable whose template contains a secret is itself a secret. > **Keep secrets out of files** > > `.env` files are never uploaded when you deploy a folder. Put secrets in `kuiper secrets` so they never end up in an image. ## Shared variables A shared variable is a plain value that several units use. Set it once for the project and reference it from each unit. ```sh kuiper shared set LOG_LEVEL=info kuiper env set -u web 'LOG_LEVEL=${{shared.LOG_LEVEL}}' kuiper env set -u worker 'LOG_LEVEL=${{shared.LOG_LEVEL}}' ``` `kuiper shared` has the same `list`, `set` and `rm` as `kuiper secrets`, and the dashboard shows them under **Advanced** on the variables page. ## Reference another unit Every unit exports a few values on its own. | Output | What it is | | --- | --- | | `INTERNAL_HOST` | The unit's private hostname, `..internal`. | | `INTERNAL_URL` | The private URL, for example `http://worker.my-agent.internal:4400`. | | `PORT_` | The number of each named port, for example `PORT_HTTP`. | | `PUBLIC_URL` | The public address, only for units with a public port. | Units in the same project reach each other on the private network. Point one at another with a template. ```sh kuiper env set -u web 'WORKER_ENDPOINT=${{worker.INTERNAL_URL}}/jobs' kuiper outputs -u worker ``` `kuiper outputs` lists a unit's outputs and the reference that selects each one. ## External services Anything that lives outside Kuiper, such as a Neon database, an Upstash Redis or your own RDS, can be a resource too. Add it with its named outputs, and mark the secret ones. ```sh kuiper resources add neon DATABASE_URL=postgres://user:pass@host/db --secret DATABASE_URL kuiper env set -u web 'PG_CONN=${{neon.DATABASE_URL}}' kuiper resources list ``` Your app's variables do not change if you later swap the external service for a managed one. See [Databases, Redis and buckets](https://kuiper.sh/docs/databases.md). ## Models, browsers and sandboxes `models`, `browsers` and `sandboxes` are built in. They need no setup: point a variable at one and every running copy of the unit gets the address of its region's gateway and a key of its own. ```sh kuiper env set -u agent \ 'OPENAI_BASE_URL=${{models.BASE_URL}}' \ 'OPENAI_API_KEY=${{models.API_KEY}}' \ 'STEEL_BASE_URL=${{browsers.BASE_URL}}' \ 'STEEL_API_KEY=${{browsers.API_KEY}}' \ 'SANDBOX_URL=${{sandboxes.BASE_URL}}' \ 'SANDBOX_KEY=${{sandboxes.API_KEY}}' ``` `models` also exports `ANTHROPIC_BASE_URL`, the base URL for Anthropic's SDKs. Calls go to your organisation's own provider keys; see [Models](https://kuiper.sh/docs/models.md). A variable that uses `API_KEY` is secret. The key is replaced on every redeploy and revoked when the old copy stops, so it never has to be rotated by hand. See [Gateway keys](https://kuiper.sh/docs/gateway-keys.md). ## Apply changes A running unit does not see a change until it restarts. After you set a variable or a secret, the unit is marked **pending restart**. The units table shows a **restart needed** tag, and the unit's page offers a **Restart now** button. ```sh kuiper restart -u web ``` `kuiper restart` applies the pending changes. It restarts the unit at a turn boundary. Setting a secret marks every unit that references it as pending restart, and `kuiper secrets set` tells you how many. If you would rather not do it by hand, turn on **Auto-restart** in the project's **Settings**. Kuiper then restarts affected units for you. To see exactly what a deployment received, open it in the dashboard, or run: ```sh kuiper deployment dep_… ``` It prints the resolved environment of that deployment, with secrets masked. ## Variables for the build Some builds need a variable, for example a registry token for a private package. Build variables are separate from runtime variables, and they are never secrets. Set them when you link a repository. ```sh kuiper link -u web --repo https://github.com/acme/web --build-env NPM_CONFIG_REGISTRY=https://registry.example.com ``` Runtime secrets are never available to the build. ## Rename and delete References point at resources, not at names. If you rename an external resource with `kuiper resources rename`, every template that mentions it is rewritten. Kuiper refuses to delete a database or a resource that a variable still references, and lists the variables that do. --- # Databases, Redis and buckets > Run Postgres and Redis next to your units, store files in S3-compatible buckets, and keep state on a volume. All of it is private to the project. Web page: https://kuiper.sh/docs/databases/ ## Postgres and Redis Kuiper runs Postgres 17 and Redis (Valkey 8.1) for you. Each one is a private service inside your project, with a persistent disk and CPU and memory reserved for it. Nothing is published on the internet. Only your units can reach a database. ![The Databases page listing a Redis cache and a Postgres database, with a form to create another.](https://kuiper.sh/docs/img/dashboard-databases.webp "Databases. A Redis called cache and a Postgres called main.") Create one in the dashboard from **Data**, then **Databases**, or from the CLI: ```sh kuiper db create postgres main -p my-agent kuiper db create redis cache -p my-agent kuiper db list -p my-agent kuiper db show main -p my-agent ``` Then give a unit the connection string. The variable name is your choice, and the template selects the database output: ```sh kuiper env set -p my-agent -u web 'DATABASE_URL=${{main.DATABASE_URL}}' kuiper env set -p my-agent -u web 'REDIS_URL=${{cache.REDIS_URL}}' kuiper restart -p my-agent -u web ``` Postgres exports `DATABASE_URL`, `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD` and `PGDATABASE`. Redis exports `REDIS_URL`, `REDIS_HOST`, `REDIS_PORT` and `REDIS_PASSWORD`. See [Variables and secrets](https://kuiper.sh/docs/variables.md) for how templates work. Hostnames look like `..internal` and resolve only inside the project's private network. Postgres URLs use `sslmode=disable` because the traffic never leaves that network. > **Not reachable from your laptop** > > A database cannot be reached from outside the project, so you cannot point `psql` at it from your machine. Run migrations from a unit inside the project, for example at startup. ### Sizes and versions Choose a size with `--size`. The default is `small`. | Size | CPU | Memory | Disk | | --- | --- | --- | --- | | `small` | 0.5 | 512 MiB | 5 GiB | | `medium` | 1 | 2 GiB | 20 GiB | | `large` | 2 | 4 GiB | 50 GiB | ```sh kuiper db create postgres main --size medium ``` `--version 17` selects Postgres 17 and `--version 8.1` selects Redis 8.1. Those are the supported versions. Images are pinned, so a restart never upgrades the engine. Resizing, upgrades and password rotation are not available yet. A major-version move means a dump and restore into a fresh database. ### Credentials Passwords and URLs are sealed. The dashboard, the CLI and the API mask them by default. To read them, ask for it explicitly: ```sh kuiper db credentials main -p my-agent --reveal ``` ### Restart and delete ```sh kuiper db restart main -p my-agent kuiper db delete main -p my-agent --confirm main ``` A restart stops the database and starts it again on the same disk, with the same data and credentials. Deleting is permanent: it removes the credentials, the service and its disk. Remove the variables that reference the database first. Kuiper lists any that remain. > **No automatic backups yet** > > Kuiper does not back up databases for you yet. On a single-node installation the data lives on that node's disk, and losing the disk loses the database. Arrange your own backups before you store anything you cannot recreate. ## Buckets A bucket is private, S3-compatible object storage for files: reports, uploads, media. Create one from **Data**, then **Buckets**, or the CLI: ```sh kuiper buckets create reports -p my-agent kuiper buckets list -p my-agent kuiper buckets show reports -p my-agent ``` ![The Buckets page with one bucket, its object count, bytes used and quota.](https://kuiper.sh/docs/img/dashboard-buckets.webp "Buckets.") Each bucket has its own access key. Bind its values to a unit like any other output: ```sh kuiper env set -p my-agent -u web \ 'S3_ENDPOINT=${{reports.S3_ENDPOINT}}' \ 'S3_BUCKET=${{reports.S3_BUCKET}}' \ 'S3_REGION=${{reports.S3_REGION}}' \ 'S3_FORCE_PATH_STYLE=${{reports.S3_FORCE_PATH_STYLE}}' \ 'S3_ACCESS_KEY_ID=${{reports.S3_ACCESS_KEY_ID}}' \ 'S3_SECRET_ACCESS_KEY=${{reports.S3_SECRET_ACCESS_KEY}}' ``` Use path-style addressing with any S3 client. The endpoint is a public address, so the same bucket works from inside and outside Kuiper. ```sh kuiper buckets credentials reports -p my-agent --reveal kuiper buckets delete reports -p my-agent --confirm reports ``` A bucket holds up to 10 GiB and 1,000,000 objects by default, and an organisation can allocate 100 GiB in total. Deleting needs an empty bucket with no unfinished uploads and no variable that references it. ### Browser access (CORS) By default only your app's server can use a bucket. Browsers block a web page on another site from reading or uploading files in it. To let your website do that, for example to upload media straight from the browser with a signed link your app hands out, allow its address. In the dashboard, open **Data**, then **Buckets**, pick the bucket, and under **Browser access** choose **Allow websites**: 1. Add each website by the address people visit, like `https://app.example.com`, with no path. Kuiper suggests your project's own addresses and `http://localhost:3000` for local development. **Allow any website (\*)** suits public files. 2. Choose **Read files** (GET, HEAD) or **Read and upload** (also PUT, POST, DELETE). **Advanced** sets the request headers pages may send (`*` by default), the response headers scripts may read (`ETag` by default) and how long a browser caches the check (an hour). The status turns **On** within seconds. From the CLI: ```sh kuiper buckets cors media -p my-agent --origin https://app.example.com --uploads kuiper buckets cors media -p my-agent # show it kuiper buckets cors media -p my-agent --off ``` Every request still needs a signed link or the bucket's key; browser access only decides which websites a browser lets use them. Plain `` and `