| name | glorp |
| description | Guide for downstream coding agents driving Glorp — especially Glorp Garage (the remote multi-session control plane) and its sandboxes (Garage-provisioned workspaces). Use when programmatically creating sessions, running coding agents over the REST/WebSocket API, the @porkytheblack/glorp-client SDK, or the @porkytheblack/glorp-mcp MCP server, provisioning tenant namespaces, sandboxes from templates, exchanging files, or wiring Glorp into CI/orchestration. Also use for the higher-level Task API — the black-box surface where you submit a typed job (make a video, build a deck, review a PR, fix a bug) and poll one object for the result, without managing sessions, templates, or the event stream. |
Driving Glorp & Glorp Garage
You are an agent working with Glorp programmatically — not the Glorp TUI user. This
skill tells you how to start a Garage, create sessions, run coding agents inside
sandboxes, and collect their results, safely and without deadlocking.
Glorp is a coding agent (read/write/edit/bash/glob/grep/ls/web_fetch, subagents,
a mesh for fan-out). You can run it three ways:
| Mode | Command | Use when |
|---|
| TUI | glorp | A human is driving interactively. Not your path. |
| Headless one-shot | glorp -p "…" | One prompt, one workspace, exits when done. |
| Garage | glorp garage | You need many concurrent, long-lived, remotely-driven sessions. This is your path for orchestration. |
Don't conflate two subsystems. "Glorp Garage" (this skill) is the multi-session
HTTP/WS control plane. It is not the station-signal fleet runner (a separate async
fan-out framework). Garage was formerly named "Station", so older notes may use that name.
Two surfaces — pick the altitude you need
Garage exposes the same runtime at two levels. Choose deliberately:
| Surface | You manage | Reach for it when |
|---|
| Sessions (most of this skill) | sessions, workspaces, permission modes, the event stream | you want fine-grained control — multi-turn conversations, live streaming, custom orchestration |
| Task API (jump) | one object — submit an input, poll a result | you just want a typed job done (a deck, a video, a PR fix) and a deliverable back, with no session/stream bookkeeping |
Tasks are built on top of sessions (a task is a worker session under the hood), so the two coexist — you can start with the Task API and drop to the session layer only if you outgrow it.
Core model: sessions, workspaces, sandboxes
- A session is one running Glorp agent with its own conversation, plan, tasks, and
permission state. State:
provisioning → idle ⇄ busy → (error) → destroyed.
- A workspace is the directory the agent reads and edits. Two kinds:
- Caller-supplied workspace — an existing host directory you point at
(
"workspace": "/abs/path"). Garage never deletes these.
- Sandbox — a workspace Garage provisions itself under
workspaceRoot
(default <data-dir>/workspaces). Created when you omit workspace, or when you
provision from a template. Garage may clean these up. Sandboxes are the safe
target for bypass/auto unattended runs and disposable work.
The distinction matters for cleanup: DELETE /sessions/:id?workspace=true removes the
folder only if it is a Garage-provisioned sandbox under workspaceRoot that no other
session references. Caller-supplied and shared workspaces are always kept. So you can
safely cascade-delete a sandbox without risking a user's real repo.
Set up glorp
Needs Bun ≥ 1.3. Build the binary (or run from source), then give it
a model provider key:
git clone https://github.com/porkytheblack/glorp && cd glorp
bun install
bun run build
export ANTHROPIC_API_KEY=sk-ant-…
The provider key is what the agents run on; the Garage API key (below) is separate and
gates access to the control plane. Persistent config lives in a garage.json under the
data dir (CLI flags override it). Full setup guide: docs/garage-usage.md.
Start a Garage
glorp garage
glorp garage --host 0.0.0.0
glorp garage --workspace-root /srv/sandboxes
- Loopback (
127.0.0.1) binds are open; any non-loopback bind requires an API key.
Force either way with GLORP_GARAGE_AUTH=required|off.
- Credentials for the agents themselves come from
~/.glorp config or env vars
(ANTHROPIC_API_KEY, OPENAI_API_KEY, …).
- Persistent config: a
garage.json in the data dir (CLI flags override it).
- Every endpoint is served at both
/api/v1/… (stable) and the bare root. Prefer
/api/v1 for anything durable.
- Back-compat:
glorp station still works as a hidden alias for glorp garage, and
the legacy GLORP_STATION_* env vars and a station.json config file are still honored.
Auth (non-loopback)
glorp garage keys add ci-bot --scopes admin
glorp garage keys list
glorp garage keys revoke <id>
Send it as Authorization: Bearer glsk_… on REST, or ?api_key=glsk_… on the WebSocket
(browsers can't set WS headers). Scopes: admin > run > read. /health is always open.
Recommended: the typed SDK
Prefer @porkytheblack/glorp-client over raw
curl when you're in a TS/JS context — it handles framing, reconnects, and errors.
import { configure, run } from "@porkytheblack/glorp-client";
configure({ endpoint: "http://127.0.0.1:4271", apiKey: "glsk_…" });
const h = await run({
workspace: "/srv/projects/acme",
prompt: "Add a /health endpoint and a test for it.",
});
const { text, status } = await h.result();
Run handle: h.sessionId, await h.status(), for await (const ev of h.events()) (live
stream), await h.result({ timeoutMs }), await h.abort().
Full client for multi-turn / admin work:
import { createClient } from "@porkytheblack/glorp-client";
const glorp = createClient({ endpoint, apiKey });
const ws = await glorp.workspaces.create("/srv/projects/acme");
const s = await glorp.sessions.createInWorkspace(ws.id, { permissionMode: "auto" });
const { text } = await glorp.sessions.sendMessageAndWait(s.id, "Refactor the auth module.");
Non-2xx throws a typed GlorpRemoteError with .status / .code (e.g. 401 = bad key).
Raw REST (when you can't use the SDK)
Session lifecycle, the loop you'll write most often:
BASE=http://127.0.0.1:4271/api/v1
H='content-type: application/json'
ID=$(curl -s -X POST $BASE/sessions -H "$H" \
-d '{"permissionMode":"auto"}' | jq -r .id)
curl -s -X POST $BASE/sessions/$ID/messages -H "$H" \
-d '{"text":"Run the test suite and fix any failures"}'
curl -s -X POST $BASE/sessions/$ID/messages -H "$H" \
-d '{"text":"Run the tests and fix failures","wait":true,"timeout_ms":600000}' | jq .
curl -s $BASE/sessions/$ID | jq '{state,busy,turn_count}'
curl -s $BASE/sessions/$ID/result | jq '{status,busy,text,error}'
curl -s -X POST $BASE/sessions/$ID/abort
curl -s -X DELETE "$BASE/sessions/$ID?workspace=true"
Key endpoints (full table in docs/garage-usage.md,
contract in docs/openapi.yaml):
| Method | Path | Purpose |
|---|
POST | /sessions / /workspaces/:id/sessions | Create session (body below) |
GET | /sessions · /sessions/:id | List · detail (poll state/busy here) |
POST | /sessions/:id/messages | Send a prompt (wait for sync) |
GET | /sessions/:id/result | Latest answer + run status, one call |
POST | /sessions/:id/abort | Stop the current turn |
DELETE | /sessions/:id[?workspace=true] | Destroy (optionally rm the sandbox) |
GET/POST/DELETE | /sessions/:id/files[/:path] | The uploads/ file exchange |
POST/DELETE | /sessions/:id/credentials | Per-session key (in-memory only) |
GET/POST | /workspaces[…] | Register / list / inspect workspaces |
GET | /templates · /health | Setup templates · liveness (open) |
POST /sessions body (all optional):
{
"workspace": "/home/dev/my-app",
"workspaceId": "ws_…",
"template": "next-app",
"params": { "repo": "me/my-repo" },
"provider": "anthropic", "model": "claude-sonnet-4-20250514",
"permissionMode": "normal",
"credentials": { "provider":"anthropic", "apiKey":"sk-ant-…", "model":"…" }
}
Sandboxes from templates
Templates provision a sandbox before the agent starts — clone a repo, install deps,
drop in config. Put JSON files in <data-dir>/templates/; the filename is the template name.
{
"description": "Clone a repo and install deps",
"steps": [
{ "type": "git-clone", "repo": "https://{env:GH_TOKEN}@github.com/{param:repo}.git", "ref": "main" },
{ "type": "shell", "command": "bun install" },
{ "type": "copy", "from": "/home/dev/templates/glorp.json", "to": "glorp.json" }
]
}
- Step types:
git-clone (repo, optional dest/ref), shell (command), copy (from,to).
- Interpolation:
{param:NAME} from the request params, {env:VAR} from Garage's env.
Interpolated values are scrubbed from error messages (secret-safe).
- Steps run sequentially. On any failure the sandbox is torn down and creation fails —
you never get a half-provisioned workspace. (Only a Garage-created dir is removed; a
pre-existing caller dir is left intact.)
curl -s -X POST $BASE/sessions -H "$H" \
-d '{"template":"next-app","params":{"repo":"me/my-repo"},"permissionMode":"bypass"}'
Permission modes — pick deliberately
Unattended runs deadlock if a tool asks for permission and nobody answers.
| Mode | Behavior | Use for |
|---|
normal | Prompts on risky ops; you must resolve each slot | A human/agent actively watching the stream |
auto | Auto-approves safe ops, prompts on risky ones | Default for unattended runs (SDK run() uses this) |
bypass | No prompts at all | Disposable sandboxes only — never a user's real repo |
If you stay in normal, you must answer permission slots as they arrive:
- REST:
POST /sessions/:id/slots/:slotId → { "action": "approve" } (or deny/resolve+value/reject+reason).
- WS command:
{ "type": "resolve_permission", "slot_id":"…", "allow": true }.
- Or in
wait mode pass "auto_approve": true to clear prompts for that one turn.
File exchange (uploads/)
Each session has an uploads/ folder inside its workspace — the hand-off point. Drop inputs
in for the agent; the agent writes deliverables (.pptx, .zip, exports) back for you.
Paths are confined to the folder (.. traversal → 400).
curl -s -F file=@./brief.pdf $BASE/sessions/$ID/files
curl -s $BASE/sessions/$ID/files | jq .
curl -s $BASE/sessions/$ID/files/deck.pptx -o deck.pptx
SDK: client.sessions.uploadFile/files/downloadFile/deleteFile.
Streaming over WebSocket
Connect GET /sessions/:id/events (auth via ?api_key=). You get a session_hydrate
snapshot, then a live stream. Each frame: { "sessionId", "seq", "event": {…} }. seq is
monotonic per client — a gap means you missed events; send { "type":"resync" } or
reconnect to re-hydrate. Commands you can push back: send_message, abort,
resolve_permission, set_permission_mode, resync. SDK: h.events() / streamSession().
Per-session credentials
POST /sessions/:id/credentials swaps a session onto its own provider key; DELETE reverts.
The key is in memory only — never persisted, logged, or returned (responses show provider
- last-4). Re-supply after a Garage restart.
Multi-tenant namespaces
Provision an isolated namespace per user so their sessions/workspaces/sandboxes/
credentials can't see each other (data lives under <dataDir>/namespaces/<id>/,
sandboxes under <workspaceRoot>/<id>/). Requires auth on. Admin-only control plane:
curl -sX POST $EP/api/v1/namespaces -H "authorization: Bearer $ADMIN" \
-H 'content-type: application/json' -d '{"name":"acme"}'
curl -sX POST $EP/api/v1/namespaces/ns_acme/keys -H "authorization: Bearer $ADMIN" \
-H 'content-type: application/json' -d '{"name":"acme-bot"}'
curl -sX POST $EP/api/v1/sessions -H "authorization: Bearer $TENANT" \
-H 'content-type: application/json' -d '{"permissionMode":"auto"}'
curl -s $EP/api/v1/sessions -H "authorization: Bearer $ADMIN" -H 'x-glorp-namespace: ns_acme'
curl -sX DELETE "$EP/api/v1/namespaces/ns_acme?data=true" -H "authorization: Bearer $ADMIN"
Same flow with the SDK (needs an admin key):
const glorp = createClient({ endpoint, apiKey: ADMIN_KEY });
const ns = await glorp.namespaces.create("acme");
await glorp.namespaces.createKey(ns.id, "acme-bot");
Requests with no namespace resolve to the built-in default namespace (the legacy
single-tenant layout) — existing setups keep working unchanged. Each namespace can
hold its own model credentials, falling back to the garage's defaults when unset — and
its own template library (see Companions, below).
Per-namespace template libraries. Beyond the garage-global catalog in
<dataDir>/templates/, a namespace can have its own templates two ways: on-disk files
under its subtree (<dataDir>/namespaces/<id>/templates/*.json), and/or its own
companion registry. Resolution is inherit-and-override, newest-wins:
tenant disk > tenant companion > garage disk > garage companion. So a tenant's
GET /templates, /templates/:name, and /tasks/types all reflect its effective
catalog. A namespace with neither its own dir nor companion inherits the garage catalog
unchanged. The companion route is covered next.
Companions
A companion is a small external service Garage talks to for two things it
deliberately doesn't own (full wire spec: docs/companion-service-spec.md):
- Git tokens — it holds the GitHub App key and mints short-lived install tokens;
Garage pulls one when a template clone needs auth (never stores it).
- A template registry — it hosts Template v2 documents; Garage only GETs them
and provisions workspaces from them. The service can generate/rotate templates however
it likes.
Garage only ever issues GETs with static headers, and the companion is optional at
runtime: if it's down, Garage serves the last-known-good catalog and keeps working.
Global companion — one registry for the whole Garage. Set it via env (or garage.json):
export GLORP_GARAGE_TEMPLATE_REGISTRY_URL=https://companion.example/v1/templates
export GLORP_GARAGE_TEMPLATE_REGISTRY_HEADERS='{"authorization":"Bearer <garage-key>"}'
Per-namespace companion — give a tenant its own registry (its own key → its own
library) at provision time. This is how each client gets a different catalog:
curl -sX POST $EP/api/v1/namespaces -H "authorization: Bearer $ADMIN" -H 'content-type: application/json' -d '{
"name": "acme",
"template_registry": {
"url": "https://companion.example/v1/templates",
"headers": { "authorization": "Bearer <acme-tenant-key>" }
}
}'
await glorp.namespaces.create("acme", undefined, {
url: "https://companion.example/v1/templates",
headers: { authorization: "Bearer <acme-tenant-key>" },
});
- The
url must be http(s). The headers (the tenant's key) are stored server-side
and never returned — GET /namespaces/:id exposes only template_registry_url.
- The tenant's companion templates layer in as
tenant disk > **tenant companion** > garage disk > garage companion, so they beat the garage catalog but yield to the tenant's own
on-disk overrides. Everything the tenant reads (/templates, /tasks/types) reflects this.
- Companion templates inline their skill files, so a tenant needs no on-disk assets — the
whole catalog can live in the companion.
MCP server (for MCP-capable agents)
If you're an MCP client (Claude Desktop/Code, Cursor, a custom orchestrator), you
can drive a Garage as tools instead of raw REST: run
@porkytheblack/glorp-mcp, which wraps the kit and speaks
MCP over stdio (default) or streamable HTTP (--http, POST /mcp).
GLORP_ENDPOINT=$EP GLORP_API_KEY=$KEY npx @porkytheblack/glorp-mcp
GLORP_ENDPOINT=$EP GLORP_API_KEY=$KEY npx @porkytheblack/glorp-mcp --http
Tools (glorp_*): namespaces (list/get/create/delete, mint_namespace_key,
list_namespace_keys), workspaces (list/create/delete), sessions (run, list,
get, send_message, session_result, abort, destroy), and the agent roster
(list_agents, add_agent, switch_agent, remove_agent). Each session/workspace/
agent tool takes an optional namespace for admin proxying. Admin tools 403
cleanly if the configured key isn't admin. Full guide: docs/mcp.md.
Task API — typed jobs, one object to poll
The black-box surface: hand Garage a job and get a result, without touching
sessions, templates, or the event stream. A task is one object — an input
you submit and a result you poll (or receive via webhook) until it's
completed/failed. A task type is a template name, so the catalog is
self-extending: a new capability is a new template server-side, no app change.
import { createClient } from "@porkytheblack/glorp-client";
const glorp = createClient({ endpoint, apiKey });
const { id } = await glorp.tasks.create({
type: "slide-deck",
input: { prompt: "A 5-slide deck on our Q3 results" },
});
const task = await glorp.tasks.wait(id, {
onQuestion: (q) => (q.kind === "confirm" ? true : q.options?.[0]?.value ?? ""),
onProgress: (note) => console.log("…", note),
});
if (task.status === "completed") {
console.log(task.result.summary);
for (const f of task.result.files) {
await fs.writeFile(f.path, await glorp.tasks.downloadFile(id, f.path));
}
} else console.error("task failed:", task.error);
Lifecycle (projected live on every read, never stored):
queued → working ⇄ needs_input → completed | failed, with failed reachable
from any state. A defer_start submit adds a staged step —
queued → staged → (upload inputs, then start) → working — so you can attach
input files before the first turn runs.
What makes tasks different from a raw session:
- Deliverable contracts gate completion. A task type whose template declares
a
required deliverable never reads completed on a text reply alone — it
stays working until the agent produces an artifact that satisfies the
contract (right file type, exists, passes a structural magic-byte check, passes
any verify command). This is the model-independent lever that stops a
"make a video" task from handing back a JSON storyboard or finishing with no
file. Nudge a stuck task with tasks.message(id, "…").
- A cumulative usage meter rides every read (
task.usage: tokens_in/out/total,
cost_usd, cost_known). It's monotonic and survives context compaction,
so you can poll it, diff against your last reading, and bill the delta.
cost_known: false means an unpriced model was used — treat cost_usd as a floor.
The methods you'll actually call:
| Call | Does |
|---|
tasks.types() | list submittable types + their typed inputs (build a form / validate) |
tasks.create({ type, input:{ prompt, params? }, permission_mode?, callback_url?, defer_start? }) | submit; returns { id } |
tasks.wait(id, { onQuestion, onProgress }) | poll to a terminal state, answering questions |
tasks.get(id) / tasks.list() | read one / all task objects (own the loop yourself if you prefer) |
tasks.answer(id, questionId, value) | answer a needs_input question (choice→value, confirm→bool, text→string, info→null) |
tasks.message(id, "now fix X") | follow up; the task keeps its workspace + context and re-delivers |
tasks.files(id) / tasks.downloadFile(id, path) | list / fetch deliverables (in uploads/) |
tasks.createWithInputs(input, files) | the files-first path: create deferred, upload into inputs/, start — one call |
tasks.delete(id) | cancel + remove the session and workspace |
Webhooks (skip polling): pass callback_url at submit time and Garage POSTs
the full task object on every transition into needs_input/completed/failed.
Delivery is fire-and-forget (5s timeout, not retried), so keep tasks.get()
as the source of truth and reconcile on a timer.
Operator note: infra secrets (a render key/URL) are managed params set
once on the host (GLORP_GARAGE_TASK_PARAM_<NAME>=… or taskParams in
garage.json). They're authoritative, applied to every task, and hidden from
tasks.types(), so a submitting app never sees or supplies them. To hand a task
runtime env vars, declare a template env map ({env}/{param}
interpolated, isolated per task).
Raw REST mirrors all of this under /tasks… (Authorization: Bearer <key>,
optional X-Glorp-Namespace). Full walkthrough — happy path, the task object,
input files, answering questions, webhooks, and the REST table — is bundled at
docs/tasks.md.
Gotchas & safety
- Don't
bypass against a real repo. Reserve bypass for sandboxes you'll delete.
- Default to
auto for anything unattended or your run hangs on the first prompt.
- Poll
state/busy (GET /sessions/:id) or stream — don't assume a turn is done.
Use GET /sessions/:id/result to fetch the latest answer without re-prompting.
wait:true needs a real timeout_ms for long jobs (e.g. test suites: 600000).
- Cleanup is guarded:
?workspace=true only removes disposable sandboxes; it will not
delete a caller-supplied or shared workspace. Safe to call on sandboxes; a no-op-on-folder
for real repos.
- Capture API keys at creation — the raw
glsk_… is shown exactly once.
/health is unauthenticated — use it for liveness probes.
Where to read more
Reference docs are bundled with this skill under ./docs/: