Put it in production
Production is four more questions. Where does it run and stay running? Where does state live across redeploys? How does it reach models, browsers, sandboxes and mail without keys in every unit? How do you see what it did? Kuiper answers all four for a Foundry folder, with no Dockerfile.
Three changes before you deploy
1. Put state on the disk
Every Foundry unit on Kuiper gets a persistent disk at /data. The starter template writes its state next to the code, which is inside the image, so a redeploy would wipe it. Point Foundry's data, the transcripts and any SQLite memory at one directory you can set per environment:
import { join } from "node:path";
// ".data" on your machine; set it to /data on Kuiper.
export const dataDir = process.env.AGENT_DATA_DIR ?? ".data";
export const dataFile = (name: string) => join(dataDir, name);import { FileFoundryDataAdapter, defineApplication } from "glove-foundry";
import { dataFile } from "./lib/paths.js";
export const data = new FileFoundryDataAdapter({ file: dataFile("foundry.json") });
export default defineApplication({ name: "release-desk", data /* , requestAuthorization (below) */ });store: ({ conversationId }) => FileStore.open(conversationId, dataFile("transcripts")),These adapters are single-host, so the unit runs one replica. Kuiper guarantees that for a unit with a disk: a deploy replaces the copy and never overlaps it. That matters for schedules too, because every runtime arms the schedules it finds. For several hosts, use a database-backed data adapter and store; Kuiper's managed Postgres is one command away.
2. Put an authorizer in front
Foundry refuses to listen on a public interface unless foundry.application.ts defines requestAuthorization. Otherwise the inspector, the API and the event streams would be open to anyone. The smallest one accepts a bearer token and lets the health check through:
import { timingSafeEqual } from "node:crypto";
import { Effect } from "effect";
const token = process.env.FOUNDRY_TOKEN;
const same = (a: string, b: string) =>
a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
export default defineApplication({
name: "release-desk",
data,
requestAuthorization: {
identifier: "release-desk-token",
challenge: "Bearer",
authorize: ({ path, authorization }) =>
Effect.succeed(
path === "/health" ||
(!!token && !!authorization?.startsWith("Bearer ") && same(authorization.slice(7), token)),
),
},
});Browsers can't send a bearer token, so to open the inspector from one, add a Basic branch; Kuiper's Foundry inspector switch then manages the username and password for you. The Deploy on Kuiper page has that version.
3. Call models through the gateway
Kuiper's model gateway serves each provider's own API at one address and sends calls to your provider keys, which never reach the unit. It records every call with its tokens and estimated cost. Glove keeps its provider and model; it only changes base URL and key:
import { Kuiper } from "@kuiper-sh/sdk";
const kuiper = new Kuiper(); // reads KUIPER_GATEWAY_KEY
model: () => createAdapter({
provider: "anthropic",
model: "claude-sonnet-4-20250514",
...kuiper.models.glove("anthropic"), // { baseURL, apiKey } for this provider
}),Deploy
curl -fsSL https://kuiper.sh/install | sh
kuiper login
kuiper projects create release-desk
kuiper use release-desk
kuiper units create release
# Provider keys live at the organisation, never in the unit
kuiper providers add anthropic --key -
# Secrets and variables, as templates
openssl rand -hex 24 | kuiper secrets set FOUNDRY_TOKEN=-
kuiper env set -u release \
AGENT_DATA_DIR=/data \
'FOUNDRY_TOKEN=${{secrets.FOUNDRY_TOKEN}}' \
'KUIPER_GATEWAY_KEY=${{models.API_KEY}}'
kuiper deploy --source . -u release --wait
kuiper logs -u release -fKuiper sees a Foundry app (a foundry.config.ts or a glove-foundry dependency) and builds it on Node 22 with your package manager. It starts glove foundry start on port 4141 with a health check, a 5 GiB disk at /data, and the names in .env.example as optional variables. kuiper link --repo … makes every push to a branch deploy, and every deployment is numbered, so kuiper rollback brings any earlier one back.
See it run
- Inspect card and Agents page. The unit's runs, conversations, events, approvals and activations, read through the node with the unit's own
FOUNDRY_TOKEN. Your browser never holds it. If the unit has no token yet, the card offers to generate one. - Model calls.
kuiper models calls --unit releaselists every call with tokens, latency and estimated cost; turn on logging to keep prompts and tool calls too. - Logs survive crashes and redeploys, with secret values redacted.
- Budgets notify or block at a limit you set, per organisation, project or key.
- Your coding agent can read all of it over Kuiper's MCP server:
claude mcp add --transport http kuiper https://app.kuiper.sh/mcp.
Browsers, sandboxes and email
The same gateway key reaches the rest of Kuiper's services once the unit's variables reference them. Inbox gives the agent an email address, with Glove tools from @kuiper-sh/inbox/glove and signed webhooks straight to the unit. Browsers are real Chrome behind Steel's API. Sandboxes come from ready-made images such as kuiper/coding; a Foundry app that hosts Station's own container sandboxes declares capabilities.sandbox in kuiper.json and gets a Docker socket Kuiper polices. Deploy on Kuiper covers each.
Already built for you
| Primitive | What you get | Instead of building |
|---|---|---|
| Zero-config deploysKuiper | Foundry and Station apps detected and built with no Dockerfile: Node 22, the right start command, port and health check.kuiper deploy --source . -u UNIT --wait · kuiper link -u UNIT --repo … (push to deploy) | a Dockerfile, a process manager and a deploy script |
| Deployments and rollbacksKuiper | Numbered deployments, one live at a time; any earlier one redeploys with its manifest.kuiper deployments · kuiper rollback DEPLOYMENT | image tags and a rollback runbook |
| Domains and environmentsKuiper | HTTPS on every unit, custom domains, and staging environments from their own branches.kuiper domains add HOST · kuiper environments create NAME | certificates, DNS and a second copy of the stack |
| Persistent diskKuiper | Where FileFoundryDataAdapter, transcripts, SQLite memory and VFS snapshots live across restarts and redeploys./data on every Foundry unit | mounting and backing up a volume |
| Managed databases and bucketsKuiper | Postgres, Redis and S3-compatible buckets inside the project, wired in by variable templates.kuiper db create postgres main · ${{main.DATABASE_URL}} | provisioning a database and copying its URL into config |
| Secrets and variablesKuiper | Sealed values bound into units, never in the image, redacted from logs.kuiper secrets set NAME=- · kuiper env set 'NAME=${{secrets.NAME}}' | a secrets manager integration |
| Model gatewayKuiper | Each provider's own API at one address with your keys, limits and every call recorded with tokens and cost.createAdapter({ provider, model, ...kuiper.models.glove(provider) }) | provider keys in every unit and your own usage tracking |
| Kuiper browsersKuiper | Real Chrome with profiles, hibernation and recordings, billed by the second.Steel-compatible API at ${{browsers.BASE_URL}} | running headless Chrome next to your agent |
| Kuiper sandboxesKuiper | Remote computers from ready-made images with a terminal, files and ports. An app that depends on station-sandbox gets a Docker socket Kuiper polices, for Station's own containers.kuiper.sandboxes.create({ image: "kuiper/coding" }) · capabilities.sandbox in kuiper.json | a container fleet for untrusted code |
| Kuiper inboxKuiper | An email address per agent that receives, sends, threads and calls your unit's webhook.inboxTools(inbox, { mailbox }) from @kuiper-sh/inbox/glove | an email provider, inbound parsing and webhook verification |
| Agent inspectionKuiper | Foundry runs, conversations, events and approvals read into the dashboard through the node, once requestAuthorization accepts Bearer $FOUNDRY_TOKEN. The app's token never reaches a browser.the unit's Inspect card · the project's Agents page | exposing your inspector publicly to see production |
| Logs and network logsKuiper | Logs kept across crashes and redeploys with secrets redacted, plus requests, DNS and connections per unit.kuiper logs -u UNIT -f · kuiper network summary | a log shipper and a proxy to see outbound calls |
| Usage and budgetsKuiper | Spend by resource and project, with limits that notify or block.kuiper usage · kuiper budgets create --scope project --limit 50 | spreadsheets reconciling provider bills |
| Kuiper over MCPKuiper | Your coding agent reads projects, logs and usage and, if you allow it, deploys.claude mcp add --transport http kuiper https://app.kuiper.sh/mcp | copy-pasting logs into your coding agent |
Where you are now
You have built every layer: a harness with all seven parts, a Foundry system around it, and production on Kuiper. Most of it was configuration. You wrote tools, prompts and a few adapters; the loop, the state, the queue, the approvals, the API, the inspector, the deploy and the gateway already existed. That ratio is the point. From here:
- Pick a harness blueprint close to what you are building.
- Keep the primitives map open while you build.
- Give your coding agent the skills so it knows all this too.