Shipwright Harness

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:

  1. Confirm that ~/.claude.json is actually a symlink: ls -la ~/.claude.json
  2. Verify it points to $AGENT_HOME/claude.json and that the target file exists
  3. 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:

  1. A PersistentVolumeClaim for the agent’s home directory (AGENT_HOME)
  2. A Secret carrying the scoped per-agent API token (and optional task-store token)
  3. 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.