← Blog

From glove foundry dev to production

A Foundry agent that works on your laptop is three changes away from production: keep its state on a disk, put a check in front of its API, and point its model at a gateway. This walkthrough makes those changes to the minimal starter and deploys it on Kuiper, with no Dockerfile and no config file.

Start from the minimal template

bash
npx glove-foundry init assistant --template minimal
cd assistant
pnpm install
pnpm dev

Open http://127.0.0.1:4141, start a run, and ask the assistant what time it is. The inspector shows the run, the model call and the current_time tool call. That is a whole agent system already: an instance, a conversation, a run queue, an HTTP API and an inspector.

Two files matter for production. foundry.application.ts holds the process-level setup, and agents/assistant/agent.ts is the agent. The file path is the agent's id.

1. Keep state on a disk you choose

Foundry runs every message in a fresh process. Anything the agent needs on the next message has to be stored outside that process. The starter already does this under .data/: FileFoundryDataAdapter keeps instances, conversations, schedules and workspace data, and lib/file-store.ts keeps each conversation's transcript.

The only change is to make the directory configurable, so the same code writes to .data/ on your laptop and to Kuiper's persistent disk in production:

lib/paths.tstypescript
import { fileURLToPath } from "node:url";

/** The persistent disk in production; .data/ beside the project locally. */
export const dataDir = process.env.AGENT_DATA_DIR
  ?? fileURLToPath(new URL("../.data/", import.meta.url));

Point both adapters at it. The transcript store takes a directory as its second argument:

agents/assistant/agent.tstypescript
store: ({ conversationId }) =>
  FileStore.open(conversationId, join(dataDir, "transcripts")),

If the agent uses glove-memory or a working environment, put their files under the same directory. The four things to persist separately are runtime data, transcripts, memory and the VFS; see Persistence.

2. Put a check in front of the API

Locally Foundry listens on loopback. On a public interface it refuses to start unless foundry.application.ts defines requestAuthorization. Without it, the API, the inspector and the event streams would be open to anyone.

The smallest useful check accepts one bearer token and lets health checks through:

foundry.application.tstypescript
import { timingSafeEqual } from "node:crypto";
import { join } from "node:path";
import { Effect } from "effect";
import { FileFoundryDataAdapter, defineApplication } from "glove-foundry";
import { dataDir } from "./lib/paths.js";

const token = process.env.FOUNDRY_TOKEN;

function bearerMatches(authorization?: string): boolean {
  if (!token || !authorization?.startsWith("Bearer ")) return false;
  const given = Buffer.from(authorization.slice(7));
  const expected = Buffer.from(token);
  return given.length === expected.length && timingSafeEqual(given, expected);
}

export const data = new FileFoundryDataAdapter({ file: join(dataDir, "foundry.json") });

export default defineApplication({
  name: "assistant",
  data,
  accounts: [],
  routes: [],
  bindings: [],
  requestAuthorization: {
    identifier: "foundry-token",
    challenge: "Bearer",
    authorize: ({ path, authorization }) =>
      Effect.succeed(path === "/health" || bearerMatches(authorization)),
  },
});

The adapter only returns yes or no. Foundry never keeps the credential, and it never reaches prompts, events or the inspector. For a browser login to the inspector, add an HTTP Basic branch; Glove Foundry units has the version that accepts both.

3. Call models through a gateway

The starter reads an OpenRouter key from the environment. In production you would rather not hand provider keys to the running process at all. Kuiper's model gateway serves each provider's own API at one address, sends each call to your organisation's provider key, and records every call with its tokens and estimated cost. Glove's createAdapter only needs a different base URL and key, and @kuiper-sh/sdk supplies both:

bash
pnpm add @kuiper-sh/sdk
agents/assistant/agent.tstypescript
import { join } from "node:path";
import { Kuiper } from "@kuiper-sh/sdk";
import { createAdapter } from "glove-core/models/providers";
import { defineAgent } from "glove-foundry";
import { z } from "zod";
import { FileStore } from "../../lib/file-store.js";
import { dataDir } from "../../lib/paths.js";

export default defineAgent({
  description: "A general-purpose Glove assistant",

  // new Kuiper() reads its gateway key from KUIPER_GATEWAY_KEY.
  model: () => {
    const kuiper = new Kuiper();
    return createAdapter({
      provider: "anthropic",
      model: "claude-sonnet-5-5",
      ...kuiper.models.glove("anthropic"),
    });
  },

  store: ({ conversationId }) =>
    FileStore.open(conversationId, join(dataDir, "transcripts")),

  systemPrompt: "You are a precise, practical assistant.",

  tools: [{
    name: "current_time",
    description: "Return the current ISO timestamp",
    inputSchema: z.object({}),
    async do() {
      return { status: "success", data: new Date().toISOString() };
    },
  }],
});

kuiper.models.glove(provider) returns the baseURL and apiKey for that provider: the Anthropic path for anthropic, the shared one for every other provider. A provider's own model names, like claude-… or gpt-…, find your key of that provider. Add the provider key once for the organisation:

bash
kuiper providers add anthropic --key -

For local development, make a gateway key for your laptop and put it in .env.local as KUIPER_GATEWAY_KEY. Your laptop and production then take the same code path:

bash
kuiper keys create laptop -p assistant --services models

4. Deploy

Kuiper detects Foundry from foundry.config.ts or the glove-foundry dependency. It builds a Node 22 image with your package manager, starts glove foundry start --host 0.0.0.0 --port 4141, gives the unit a public HTTPS address, a health check and a 5 GiB disk at /data.

bash
kuiper projects create assistant
kuiper use assistant
kuiper units create assistant

openssl rand -hex 24 | kuiper secrets set FOUNDRY_TOKEN=-
kuiper env set -u assistant \
  'FOUNDRY_TOKEN=${{secrets.FOUNDRY_TOKEN}}' \
  'KUIPER_GATEWAY_KEY=${{models.API_KEY}}' \
  AGENT_DATA_DIR=/data

kuiper deploy --source . -u assistant --wait

The variable names are the ones this code reads: FOUNDRY_TOKEN by the authorizer, KUIPER_GATEWAY_KEY by the SDK, AGENT_DATA_DIR by lib/paths.ts. Their values are templates. ${{secrets.FOUNDRY_TOKEN}} reads a sealed project secret, which never enters the image and is redacted from logs. ${{models.API_KEY}} is a gateway key made for each running copy of the unit, revoked when that copy stops. Because the unit references models, its key opens the model gateway and nothing else.

The deploy uploads the folder, streams the build log, and prints each step of the rollout until the deployment is live. .env files are never uploaded.

bash
kuiper status -u assistant
kuiper logs -u assistant -f
kuiper outputs -u assistant
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.

5. Call it from your app

kuiper outputs lists the unit's PUBLIC_URL. The typed client takes it and a header function:

typescript
import { createFoundryClient } from "glove-foundry/client";
import type { FoundryRoutes } from "./.foundry/routes.js";

const foundry = createFoundryClient<FoundryRoutes>({
  baseUrl: assistantUrl, // the unit's PUBLIC_URL
  authorization: {
    identifier: "foundry-token",
    headers: () => ({ authorization: `Bearer ${foundryToken}` }),
  },
});

const me = await foundry.agent("assistant").create();
const thread = await foundry.createConversation(me.id);
const run = await foundry.send(me.id, thread.id, "What time is it?");
const done = await run.wait();
console.log(done.output?.value);

The same instance answers on /v1/chat/completions and /v1/responses, so an OpenAI SDK or an existing chat UI can talk to it with the instance id as the model. A web app deployed as another unit in the same project can reach it privately through ${{assistant.INTERNAL_URL}}.

6. Watch it

Every Foundry unit has an Inspect card, and every project an Agents page. They show runs, conversations, events, approvals, activations and health, read from the app's own API through the node it runs on. The browser never sees the token, and only reads are possible. The authorizer above is what makes that work: Kuiper calls the API with the unit's FOUNDRY_TOKEN.

bash
kuiper models calls --unit assistant
kuiper models usage --by model

The first lists the unit's model calls, newest first. The second sums calls, tokens and estimated provider cost.

What to know before you rely on it

What you did not write

ConcernWithout KuiperOn Kuiper
Build and startA Dockerfile, a process managerDetected from the Foundry project
Disk for stateA volume, mounted and backed upA disk at /data on every Foundry unit
Provider keysCopied into every serviceHeld by the gateway; units get a per-copy key
SecretsA secrets manager integrationSealed, bound by template, redacted from logs
HTTPSCertificates and a proxyAn address per unit, custom domains on request
Seeing runs in productionExposing the inspectorInspect card and Agents page, read-only
Model spendReconciling provider billsEvery call recorded with tokens and cost
Rolling backImage tags and a runbookkuiper rollback DEPLOYMENT
More. Deploy on Kuiper is the reference for everything here, including browsers, sandboxes and email for agents. Glove Foundry units on kuiper.sh covers the dashboard login and the inspector mount path.