Operate / Deploy on Kuiper

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.

Kuiper projectKuiper gatewayPeople · appsyour domainFoundry unitglove foundry start/data diskdata · transcripts · VFSPostgresRedis · bucketsSecretssealedModelsyour provider keysBrowsersreal ChromeSandboxeskuiper/coding …Inboxemail in and outDashboard · CLI · MCPlogs · runs · model calls · usage · budgetsHTTPSreads · writesvariablesone keyreads runs and logs
A Foundry agent on Kuiper. The unit is built from your folder or repository and served over HTTPS; Foundry's data, transcripts and memory live on its /data disk; databases and secrets are wired in by variable templates; one gateway key reaches the model providers, browsers, sandboxes and inbox. The dashboard reads the agent's runs through the node, without your app's token leaving Kuiper.

What Kuiper takes off your hands

A Foundry app needsOn your own serversOn Kuiper
Build and startA Dockerfile, a process manager, the right flagsDetected from foundry.config.ts or the glove-foundry dependency; runs glove foundry start --host 0.0.0.0 --port 4141 under tini
Durable stateA volume, and making sure only one copy runsA disk at /data on every Foundry unit; a unit with a disk always runs one copy, so schedules fire once
An authorization boundaryGenerate, store and rotate a tokenWarns before the build when no requestAuthorization exists; generates FOUNDRY_TOKEN and the inspector login for you
Model callsProvider keys in every service, your own usage trackingThe model gateway: your provider keys stay at the organisation; every call recorded with tokens and estimated cost
Email for the agentA mail provider, inbound parsing, webhook verificationA mailbox per agent and Glove tools in @kuiper-sh/inbox/glove
Browsers and sandboxesChrome and a container fleetGateway browsers and sandboxes, or a Docker socket Kuiper polices for Station's own containers
A databaseProvision it, copy the URL into configkuiper db create postgres main, referenced as ${{main.DATABASE_URL}}
Seeing productionExpose the inspector, ship logsThe 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:

foundry.application.tstypescript
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 FileStore under the same directory: FileStore.open(conversationId, join(dataDir, "transcripts")).
  • Memory. createSqliteMemoryAdapters({ file: join(dataDir, "memory.db"), namespace, schema }) from glove-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:

foundry.application.tstypescript
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

agents/concierge/agent.tstypescript
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

terminalbash
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 -f

The 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:

kuiper.json (excerpt)json
{ "health": { "http": "/health" } }

Email, browsers and sandboxes

Email

agents/concierge/agent.tstypescript
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 on station-sandbox or station-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 as kuiper/coding, start by name.
  • Kuiper's services from your own tools. kuiper.sandboxes.create({ image: "kuiper/coding" }) and kuiper.browsers.create() from @kuiper-sh/sdk run on separate machines, are billed by the second and are held to your budgets. Wrap them in Glove tools, or implement a SandboxBackend for createSandboxAdapter.

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:

terminalbash
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.
  • requestAuthorization that accepts Bearer $FOUNDRY_TOKEN and lets /health through.
  • Models through the gateway; provider keys added with kuiper providers add, never set on the unit.
  • Secrets set with kuiper secrets set and bound with ${{secrets.NAME}} templates.
  • Inspector login turned on; FOUNDRY_TOKEN generated 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.