Deploy on Kuiper
Kuiper is a deployment platform for agents, the way Vercel is for Next.js. It recognises a Foundry folder and deploys it with no Dockerfile and no config, keeps its state on a persistent disk, and reads its runs into the dashboard. This page is the Foundry side: what to change in your project, then how to deploy it.
What Kuiper takes off your hands
| A Foundry app needs | On your own servers | On Kuiper |
|---|---|---|
| Build and start | A Dockerfile, a process manager, the right flags | Detected from foundry.config.ts or the glove-foundry dependency; runs glove foundry start --host 0.0.0.0 --port 4141 under tini |
| Durable state | A volume, and making sure only one copy runs | A disk at /data on every Foundry unit; a unit with a disk always runs one copy, so schedules fire once |
| An authorization boundary | Generate, store and rotate a token | Warns before the build when no requestAuthorization exists; generates FOUNDRY_TOKEN and the inspector login for you |
| Model calls | Provider keys in every service, your own usage tracking | The model gateway: your provider keys stay at the organisation; every call recorded with tokens and estimated cost |
| Email for the agent | A mail provider, inbound parsing, webhook verification | A mailbox per agent and Glove tools in @kuiper-sh/inbox/glove |
| Browsers and sandboxes | Chrome and a container fleet | Gateway browsers and sandboxes, or a Docker socket Kuiper polices for Station's own containers |
| A database | Provision it, copy the URL into config | kuiper db create postgres main, referenced as ${{main.DATABASE_URL}} |
| Seeing production | Expose the inspector, ship logs | The Inspect card and Agents page read runs through the node; logs, network logs, usage and budgets |
Kuiper's own guide, Glove on Kuiper, maps every part of a Glove agent to the service under it and has steps for personal agents, support desks, coding agents and web or voice fronts.
1. Keep state on /data
Foundry has four kinds of state: its runtime data, conversation transcripts, memory and working-environment files. All four must be outside the image, or a redeploy loses them. The starter templates write to .data/ beside the code; on Kuiper, point them at the unit's disk:
import { join } from "node:path";
import { FileFoundryDataAdapter, defineApplication } from "glove-foundry";
// ".data" locally; set to /data on the unit.
export const dataDir = process.env.AGENT_DATA_DIR ?? ".data";
export const data = new FileFoundryDataAdapter({ file: join(dataDir, "foundry.json") });
export default defineApplication({
name: "concierge",
data,
// requestAuthorization: step 2
});- Transcripts. Open the template's
FileStoreunder the same directory:FileStore.open(conversationId, join(dataDir, "transcripts")). - Memory.
createSqliteMemoryAdapters({ file: join(dataDir, "memory.db"), namespace, schema })fromglove-memory/sqlite. - Working environment.
foundryDataEnvironmentPersistence()keeps VFS snapshots in the data adapter, so they land on the disk too. - One copy. These are single-host adapters. Kuiper runs one copy of a unit with a disk and replaces it on deploy without overlap, which is also what keeps each schedule firing once. To run several copies, move to database-backed adapters on a managed Postgres.
2. Add an authorizer
Foundry won't bind 0.0.0.0 without requestAuthorization. This one accepts a bearer token from API clients and the Kuiper dashboard, and a username and password from browsers, so you can open the inspector:
import { timingSafeEqual } from "node:crypto";
import { Effect } from "effect";
const token = process.env.FOUNDRY_TOKEN;
const dashboardUser = process.env.FOUNDRY_DASHBOARD_USERNAME ?? "admin";
const dashboardPassword = process.env.FOUNDRY_DASHBOARD_PASSWORD;
const same = (a: string, b: string) => {
const x = Buffer.from(a);
const y = Buffer.from(b);
return x.length === y.length && timingSafeEqual(x, y);
};
const allowed = (authorization?: string) => {
if (!authorization) return false;
if (token && authorization.startsWith("Bearer ")) return same(authorization.slice(7), token);
if (dashboardPassword && authorization.startsWith("Basic ")) {
const decoded = Buffer.from(authorization.slice(6), "base64").toString("utf8");
const i = decoded.indexOf(":");
return i > 0 && same(decoded.slice(0, i), dashboardUser) && same(decoded.slice(i + 1), dashboardPassword);
}
return false;
};
export default defineApplication({
name: "concierge",
data,
requestAuthorization: {
identifier: "kuiper",
challenge: 'Basic realm="Foundry", charset="UTF-8"',
authorize: ({ path, authorization }) => Effect.succeed(path === "/health" || allowed(authorization)),
},
});On Kuiper, open the unit and choose Foundry inspector → Turn on (or kuiper dashboard -u concierge --enable). Kuiper generates the password, stores it as a project secret and sets both variables. The unit's Inspect card offers Generate token and restart for FOUNDRY_TOKEN, which it then uses to read runs through the node. Your browser never sees it.
3. Call models through the gateway
import { Kuiper } from "@kuiper-sh/sdk";
import { createAdapter } from "glove-core/models/providers";
import { defineAgent } from "glove-foundry";
const kuiper = new Kuiper(); // reads KUIPER_GATEWAY_KEY
export default defineAgent({
description: "Answers for Acme",
model: () =>
createAdapter({ provider: "anthropic", model: "claude-sonnet-4-20250514", ...kuiper.models.glove("anthropic") }),
// store, systemPrompt, tools, …
});The gateway serves each provider's own API, so Glove keeps its provider and model and only changes the base URL and key. Without the SDK, pass baseURL from ${{models.BASE_URL}} (or ${{models.ANTHROPIC_BASE_URL}} for the anthropic provider) and apiKey from ${{models.API_KEY}}. See Kuiper's models page.
4. Deploy
curl -fsSL https://kuiper.sh/install | sh
kuiper login
kuiper projects create concierge && kuiper use concierge
kuiper units create concierge
kuiper providers add anthropic --key - # once per organisation
kuiper env set -u concierge AGENT_DATA_DIR=/data \
'KUIPER_GATEWAY_KEY=${{models.API_KEY}}'
kuiper deploy --source . -u concierge --wait # or: kuiper link -u concierge --repo … --branch main
kuiper dashboard -u concierge --enable # inspector login
kuiper logs -u concierge -fThe builder uses Node 22 and your package manager, runs your build script if you have one (Foundry needs none), and starts glove foundry start. The default manifest asks for 0.5 vCPU and 768 MiB, allows up to 2 vCPU and 2 GiB, adds a 5 GiB disk at /data, and lists the names in .env.example as optional variables. Run kuiper manifest init --foundry to write that kuiper.json out and tune it.
Health checks
The default manifest checks TCP: the unit is healthy once Foundry accepts connections. From glove-foundry 0.5.2, /health answers 503 when the execution daemon or scheduler fails its ping, so if your authorizer lets /health through (the one above does), use it as a real readiness check:
{ "health": { "http": "/health" } }Email, browsers and sandboxes
import { Inbox } from "@kuiper-sh/inbox";
import { inboxTools } from "@kuiper-sh/inbox/glove";
import { defineSchedule } from "glove-foundry";
// The key is KUIPER_GATEWAY_KEY, as for models. The unit's key only opens the
// services its variables reference, so the unit must also reference the inbox.
const inbox = new Inbox({ gatewayUrl: process.env.INBOX_URL });
const checkMail = defineSchedule({
name: "check-mail",
timing: { kind: "every", interval: "5m" },
message: "Check your mailbox with check_email and answer each new email once.",
});
// in defineAgent:
// tools: inboxTools(inbox, { mailbox: "concierge" }),
// schedules: [checkMail],Bind the inbox once, so the unit's gateway key opens it: kuiper env set -u concierge 'INBOX_URL=${{inbox.BASE_URL}}'. kuiper inbox create concierge prints the address. The tools are check_email, read_email, get_email_attachment, send_email and reply_email; allowRecipient limits who the agent may write to. Foundry's API sits behind your authorizer, so to answer the moment mail arrives, point the mailbox's webhook at a small unit that forwards to Foundry, as in the webhook recipe. Kuiper's email agent example builds one end to end.
Browsers and sandboxes
glove-execution's adapters drive browsers and sandboxes that a Station hosts. There is no adapter for Kuiper's gateway services yet, so pick one of these:
- Station on the unit. Host Station's container sandboxes and browsers with stationDaemon and mount them with
mountSandbox/mountBrowser. When your app depends onstation-sandboxorstation-browser-use, Kuiper adds the Docker CLI and gives the unit a Docker socket it serves and polices; every container runs under gVisor within the unit's limits. Kuiper's ready-made images, such askuiper/coding, start by name. - Kuiper's services from your own tools.
kuiper.sandboxes.create({ image: "kuiper/coding" })andkuiper.browsers.create()from@kuiper-sh/sdkrun on separate machines, are billed by the second and are held to your budgets. Wrap them in Glove tools, or implement aSandboxBackendforcreateSandboxAdapter.
Next.js and Foundry
An app that runs Next.js and Foundry as two processes deploys as two units from one repository. Point the web unit at the Foundry unit's private address, which needs no public route:
kuiper env set -u web 'FOUNDRY_URL=${{concierge.INTERNAL_URL}}'The generated lib/foundry.ts client reads FOUNDRY_URL; give it the same bearer token through its authorization option. See Next.js integration.
Checklist
- Data adapter, transcripts, memory and VFS snapshots under
AGENT_DATA_DIR, set to/data. requestAuthorizationthat acceptsBearer $FOUNDRY_TOKENand lets/healththrough.- Models through the gateway; provider keys added with
kuiper providers add, never set on the unit. - Secrets set with
kuiper secrets setand bound with${{secrets.NAME}}templates. - Inspector login turned on;
FOUNDRY_TOKENgenerated from the Inspect card. - A budget on the project if the agent can start browsers or sandboxes.
Running somewhere else? Deploy & security covers glove foundry start, TLS, the execution daemon and a container recipe for any host.