The Agent
The Shipwright agent (artifact C) is a thin autonomous runner: it picks the next ready task from the queue, builds it, ships a PR, and forwards metrics — all without manual intervention. It has a Prisma-backed PostgreSQL store and four HTTP surfaces: a machine-polled runtime API, a human-facing admin CRUD API, a server-rendered admin UI, and a public read-only task board.
Run modes
There are three ways to run the agent depending on your context:
| Mode | Entry point | Transport | Entrypoint behavior | When to use |
|---|---|---|---|---|
| Pi / bare-metal | agent/src/index.ts |
Slack Socket Mode | Runs directly on the host | Running directly on a host with a local .env file |
| K8s container | agent/src/entrypoint-main.ts |
Slack Socket Mode | Dockerfile ENTRYPOINT is bun run agent/src/entrypoint-main.ts, which starts the health server in-process on SHIPWRIGHT_HEALTH_PORT (default 3459) before the startup sequence so Kubernetes liveness probes are reachable during init |
Deployed via the Dockerfile (validates vars, fetches config, installs plugins, spawns runner) |
| Local dev | task stack |
Chat poll loop → admin Chat UI | Runs the agent in Docker via the agent pane | Testing Claude locally without a Slack workspace — chat via the admin console’s Chat tab |
The admin service Dockerfile (admin/Dockerfile) ENTRYPOINT is bun run admin/src/main.ts,
which runs migrations, constructs all services, and mounts all admin and runtime routes.
Running the agent locally
Run the full local dev stack:
task stack
This launches every service (metrics, admin, task-store, chat service, and the agent in Docker)
in a single tmux session, seeds dev tokens, and opens the admin console in your browser. Chat
with the agent from the console’s Chat tab (/admin/chat) — the agent’s chat poll loop
claims each message, runs it through Claude, and posts the reply back into the thread.
Admin CRUD API
The admin CRUD API is the human-facing control plane for agents. It is mounted at /agents/*.
Auth is checked in this order: env API keys (SHIPWRIGHT_ADMIN_API_KEYS), then per-agent DB
bearer tokens, then session cookies. Admin keys bypass all per-agent checks; scoped bearer tokens
are restricted to their own agent ID.
| Resource | Endpoints |
|---|---|
| Agents | POST /agents, GET /agents, GET /agents/:id, PATCH /agents/:id, POST /agents/:id/provision |
| Envs | POST / GET / PATCH /agents/:id/envs, DELETE /agents/:id/envs/:key |
| Crons | POST /agents/:id/crons, PATCH / DELETE /agents/:id/crons/:cronId, POST /agents/:id/crons/reconcile |
| Tools | POST / GET /agents/:id/tools, PATCH / DELETE /agents/:id/tools/:toolId |
| Tokens | POST / GET /agents/:id/tokens, DELETE /agents/:id/tokens/:tokenId |
| Plugins | POST / GET / PATCH /agents/:id/plugins, DELETE /agents/:id/plugins |
Token creation returns the raw token once at creation. Only its SHA-256 hash is persisted — token validation is an O(1) hash-index lookup with no plaintext at rest.
Data model
The agent store uses nine Prisma models on a dedicated database (DATABASE_URL_SHIPWRIGHT_ADMIN):
| Model | What it owns |
|---|---|
Agent |
Runner identity — name, slackId, selfHosted, repos, encrypted slackBotToken / anthropicApiKey |
AgentEnv |
Key/value env store — key, value (AES-256-GCM encrypted); unique per [agentId, key] |
AgentCronJob |
Scheduled prompts — schedule, prompt, channel or user, silent, enabled, preCheck |
AgentCronRun |
Cron execution history — startedAt, completedAt, outcome, inputTokens, outputTokens, costUsd, model |
AgentTool |
Allowed tool patterns — pattern (e.g. Read, Bash), enabled |
AgentToken |
Scoped API tokens — token (SHA-256 hash), label, revokedAt |
AgentPlugin |
Installed Claude Code plugins — name, version, enabled |
AgentMember |
Authorized human members — email; unique per [agentId, email] |
AgentChatTokenUsageDaily |
Daily chat token usage aggregates — date, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, costUsd; unique per [agentId, date] |
All child models cascade-delete with their parent Agent.
Secrets at rest (env values, Slack/Anthropic keys) are AES-256-GCM encrypted at the service layer.
Set SHIPWRIGHT_ENCRYPTION_KEY to a 64-char hex string (32 bytes) in any real deployment — if
unset, secrets are stored in plain text with a logged warning.
Default system crons
See Cron Jobs for the full list of default system crons, their schedules, and what each one does.
Required environment variables
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL_SHIPWRIGHT_ADMIN |
always | Postgres connection string for the admin service |
SHIPWRIGHT_AGENT_ID |
entrypoint | The agent’s ID in the platform |
SHIPWRIGHT_API_URL |
entrypoint | Base URL of the admin API for config fetch at startup |
SHIPWRIGHT_AGENT_API_KEY |
entrypoint | Bearer token for the config fetch |
SHIPWRIGHT_ENCRYPTION_KEY |
production | 64-char hex for AES-256-GCM secrets at rest |
SLACK_BOT_TOKEN |
Slack mode | Slack bot user OAuth token (xoxb-...) |
SLACK_APP_TOKEN |
Slack mode | Slack app-level token for Socket Mode (xapp-...) |
SLACK_SIGNING_SECRET |
Slack mode | Verifies incoming Slack request signatures |
See configuration.md for the full environment variable reference.
Adding an MCP server
Register an MCP server on a Shipwright agent the same way you would in any Claude Code session:
claude mcp add <name> <command> [args...]
# or, for an HTTP/SSE server:
claude mcp add --transport http <name> <url>
This writes the registration into ~/.claude.json. On a Shipwright agent, ~/.claude.json
is not a real file — it’s a symlink to $AGENT_HOME/claude.json, created by
ensureDotClaudeSymlink() in agent/src/setup.ts at every agent startup. The same function
also symlinks ~/.claude → $AGENT_HOME/dot-claude. Both live on the agent’s persistent
volume (PVC), which survives pod restarts; the container’s ephemeral filesystem does not.
This matters because ~/.claude.json is where MCP server registrations live — outside
~/.claude/ as a sibling file — so it needs its own symlink to persist across pod restarts.
Without it, a server you register with claude mcp add would be written into the ephemeral
image copy and silently vanish the next time the pod restarts. Claude Code would then read
the fresh image copy instead of the PVC’s persisted registration.
Troubleshooting: MCP server “disappeared”
If a registered MCP server vanishes after a pod restart:
- Confirm that
~/.claude.jsonis actually a symlink:ls -la ~/.claude.json - Verify it points to
$AGENT_HOME/claude.jsonand that the target file exists - Check that the target file contains your MCP server registration:
cat $AGENT_HOME/claude.json
Security note
Avoid putting secrets directly in an MCP server registration command line if possible —
prefer an environment variable reference instead. This prevents the secret from appearing
in claude mcp list output or process listings.
Kubernetes provisioning
When SHIPWRIGHT_K8S_PROVISIONING=enabled, the admin service acts as a Kubernetes provisioner.
POST /agents/:id/provision creates:
- A PersistentVolumeClaim for the agent’s home directory (
AGENT_HOME) - A Secret carrying the scoped per-agent API token (and optional task-store token)
- A Deployment running the agent container, referencing both
The PVC is retained on deletion (data safety policy) — cluster admins clean it up manually.
Self-hosted agents (selfHosted: true) skip provisioning; they manage their own workload.
Next steps
Ready to configure how much the agent does on its own? See Configuring Autonomy next, then Deploying to Cloud when you’re ready to ship.