sark
Getting Started

Deploy

The three things a real deployment needs, and what breaks without each.

Without TEMPLATE_BOX_ID every thread gets a fresh box: empty, no repos, nothing installed. With one, threads fork a snapshot that already has your stack, your repos, and your MCP settings, so a thread is useful the second it starts.

box new                      # install your stack, clone repos
box stop <id>                # the snapshot IS the template
# put TEMPLATE_BOX_ID=<id> in .deploy.env

Keep the template stopped. To publish a new version: resume → update → stop.

fork() does not accept a TTL, so sark sets ttlSeconds (and a readable box name, slack <threadId>) with a follow-up PATCH right after the fork. A failure there is logged and non-fatal.

2. Set the secrets

npx wrangler secret put BOX_API_KEY        # box_... from the Box dashboard
npx wrangler secret put MCP_TOKEN_SECRET   # any long random string
npx wrangler secret put API_TOKEN          # guards /api
SecretIf unset
BOX_API_KEYNothing works; every box call fails.
MCP_TOKEN_SECRETTokens cannot be minted or verified; the agent can never reply.
API_TOKENEvery /api route returns 503. This is deliberate: it fails closed rather than open.

Rotating MCP_TOKEN_SECRET invalidates every token currently sitting inside a live sandbox. Those threads recover on their next turn, when ensureMcp re-registers.

3. Set PUBLIC_URL and deploy

Your settings live in .deploy.env, which is gitignored:

.deploy.env
WORKER_NAME=sark
PUBLIC_URL=https://<your-worker>.workers.dev
ALLOWED_CHANNELS=
ALLOWED_USERS=
ALLOWED_TEAMS=
TEMPLATE_BOX_ID=
CLOUDFLARE_ACCOUNT_ID=
npm run deploy

Don't put these in wrangler.jsonc. It is the public template — placeholder origin, empty allowlists, no account_id — and npm run deploy injects your values as wrangler CLI overrides instead. Overrides rather than a second config file, because wrangler does not merge configs and a local copy would silently miss any binding added to the tracked one.

npm run check-config fails if real values reach the tracked file, and runs in CI. Wire it up locally too:

ln -s ../../scripts/check-config-clean.sh .git/hooks/pre-commit

There is a chicken-and-egg here: PUBLIC_URL has to be the Worker's own origin, which you can't know until it exists. So the first deploy runs without it (npx wrangler deploy --name sark), you read the origin off the output, and the second deploy has it right.

This is the one var that must be right. It is baked into each box's environment as SLACK_MCP_URL (${PUBLIC_URL}/mcp) at fork time, and it is how the agent finds its way back. If it points somewhere unreachable, every run falls through to the watchdog instead of the agent speaking for itself.

Because box env is fixed at fork time, changing PUBLIC_URL does not update boxes that already exist. Threads pick it up when their box is next recreated.

See Configuration for the rest of the vars.

4. Verify

curl -s https://<your-worker>.workers.dev/health
# {"ok":true,"slack":true,"template":"box_...","provider":"claude-code"}

npm run drive -- --url https://<your-worker>.workers.dev --thread smoke "say hi"

A real end-to-end run against the deployed origin is the only way to prove the MCP callback path works, because that path requires a publicly reachable PUBLIC_URL.

Deploying and in-flight threads

The state machine is driven by persisted Durable Object alarms, not by inline work in a request. A deploy that resets a Durable Object mid-step therefore doesn't strand its queue: the alarm is still in storage, and ensureLiveness() re-arms one if a thread has outstanding work with no alarm scheduled. Reading GET /api/threads/{id} also re-arms a stranded thread, so a poller heals it for free.

Slack

Slack is optional and comes last. See Connect Slack.

On this page