snappy-box skill
call method path json-body?writeroutesreadsql queryread/_deploy/_undeploy/_rollback/patch/_daemon/start/_daemon/stop/_daemon/my-task/pages/deploy-experience/system/sql/state/state/my-key/cache/cache/my-key/secrets/secrets/OPENAI_API_KEY/functions/functions/double/functions/double/packs/packs/artifacts/connections/connections/my-neon/test/connections/my-neon/events/webhooks/webhooks/deploy-notify/config/events/logs/system/request-log$ npx snappy-skills install snappy-box
$ npx snappy-skills install --all
$ npx snappy-skills update
Box is a self-editing Express server running on Robert's Mac Mini (Docker, Node 20,
180+ routes). It is the runtime execution layer for the Snappy stack -- other skills
produce intent and code, Box runs it. Covers route deployment, SQL, state/cache/secrets,
daemons, autonomous agents, capability registry, and pack organization.
typescriptimport { listRoutes, runSql, deploy, callRoute } from "../snappy-box/api.ts";
| Function | What it does |
|---|---|
listRoutes() |
List all deployed routes |
runSql(query) |
Run SQL against Box's internal database |
deploy(routeConfig) |
Deploy a route (name, method, path, code, pack?, meta?) |
callRoute(method, path, body?) |
Hit any Box endpoint |
CLI:
bashnpx tsx ~/.claude/skills/snappy-box/api.ts routes
npx tsx ~/.claude/skills/snappy-box/api.ts sql "SELECT 1"
npx tsx ~/.claude/skills/snappy-box/api.ts call GET /pulse
Credentials loaded via snappy-settings/load.ts from .env.cache. BOX_API_KEY must be in .env.cache -- if missing, retrieve via ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY".
| File | Purpose |
|---|---|
| SKILL.md | Full HTTP API reference, CRUD map, MCP mapping, gotchas |
| endpoints.md | Complete curl examples for every endpoint |
| writing-routes.md | Sandbox helpers (ai, db, redis, fetch, deploy, emit) |
| tables.md | Internal database table schemas |
| operations.md | Maintenance, agents, registry-before-build, daemon template |
bashBOX_URL="http://10.0.0.199:8080"
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
POST /_deploy)POST /system/sql)POST /deploy-experience)ai(), getSecret(), callFunction(), or emit() directly -- use raw fetch/SQL insteadBOX_SELF_API_KEY secret, not the user keylocalhost:8080, not the external IPpack and meta.description on deployed routes/tmp/box-deploy.json, don't inline in curlsnappy-infra -- Mac Mini SSH host, auth patternssnappy-github -- source for routes/daemonssnappy-website -- runtime routes serving snappy.ai datasnappy-ops -- triggers daemons during daily/weekly rhythmsnappy-maintenance -- reads /pulse, /logs for health reportsBox self-deploys via POST /_deploy. Do NOT use snappy-deploy for Box routes.
If this loader doesn't cover your case:
bashecho "[$(date -u +%FT%TZ)] snappy-box: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-box Index]|root: ~/.claude/skills/snappy-box|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,endpoints.md,operations.md,tables.md,writing-routes.md}
<!-- SKILL-INDEX-END -->
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
call |
method, path, json-body? |
write |
npx tsx ~/.claude/skills/snappy-box/api.ts call <method> <path> |
routes |
— | read |
npx tsx ~/.claude/skills/snappy-box/api.ts routes |
sql |
query |
read |
npx tsx ~/.claude/skills/snappy-box/api.ts sql "<query>" |
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-box
role: Box server -- self-editing Express server on Mac Mini (Docker, Node 20, 180+ routes). HTTP API for route deployment, state, cache, secrets, daemons, and autonomous agents.
loaded-by: PreToolUse hook (auto-injected when "snappy-box" is mentioned)
---
# snappy-box Loader
Box is a self-editing Express server running on Robert's Mac Mini (Docker, Node 20,
180+ routes). It is the runtime execution layer for the Snappy stack -- other skills
produce intent and code, Box runs it. Covers route deployment, SQL, state/cache/secrets,
daemons, autonomous agents, capability registry, and pack organization.
## API module
```typescript
import { listRoutes, runSql, deploy, callRoute } from "../snappy-box/api.ts";
```
| Function | What it does |
|----------|-------------|
| `listRoutes()` | List all deployed routes |
| `runSql(query)` | Run SQL against Box's internal database |
| `deploy(routeConfig)` | Deploy a route (name, method, path, code, pack?, meta?) |
| `callRoute(method, path, body?)` | Hit any Box endpoint |
CLI:
```bash
npx tsx ~/.claude/skills/snappy-box/api.ts routes
npx tsx ~/.claude/skills/snappy-box/api.ts sql "SELECT 1"
npx tsx ~/.claude/skills/snappy-box/api.ts call GET /pulse
```
Credentials loaded via `snappy-settings/load.ts` from `.env.cache`. `BOX_API_KEY` must be in `.env.cache` -- if missing, retrieve via `ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY"`.
## Key Files
| File | Purpose |
|------|---------|
| SKILL.md | Full HTTP API reference, CRUD map, MCP mapping, gotchas |
| endpoints.md | Complete curl examples for every endpoint |
| writing-routes.md | Sandbox helpers (ai, db, redis, fetch, deploy, emit) |
| tables.md | Internal database table schemas |
| operations.md | Maintenance, agents, registry-before-build, daemon template |
## Connection
```bash
BOX_URL="http://10.0.0.199:8080"
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
```
## Capabilities
- Deploy, patch, rollback, and undeploy routes (`POST /_deploy`)
- Run SQL queries (`POST /system/sql`)
- Manage state, cache, and secrets via HTTP
- Start/stop/list daemons for background agents
- Search existing capabilities before building new ones
- Deploy multi-resource experiences (`POST /deploy-experience`)
- Build autonomous daemon agents (observe/think/act/record pattern)
## Critical Gotchas
- Daemons cannot use `ai()`, `getSecret()`, `callFunction()`, or `emit()` directly -- use raw fetch/SQL instead
- Self-modifying routes must use `BOX_SELF_API_KEY` secret, not the user key
- Self-calls use `localhost:8080`, not the external IP
- Always include `pack` and `meta.description` on deployed routes
- Write deploy payloads to `/tmp/box-deploy.json`, don't inline in curl
## Uses
- `snappy-infra` -- Mac Mini SSH host, auth patterns
- `snappy-github` -- source for routes/daemons
## Downstream Skills
- `snappy-website` -- runtime routes serving snappy.ai data
- `snappy-ops` -- triggers daemons during daily/weekly rhythm
- `snappy-maintenance` -- reads /pulse, /logs for health reports
## Key Rule
Box self-deploys via `POST /_deploy`. Do NOT use `snappy-deploy` for Box routes.
---
If this loader doesn't cover your case:
```bash
echo "[$(date -u +%FT%TZ)] snappy-box: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```
Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.
<!-- SKILL-INDEX-START -->
[snappy-box Index]|root: ~/.claude/skills/snappy-box|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,endpoints.md,operations.md,tables.md,writing-routes.md}
<!-- SKILL-INDEX-END -->
## Used by
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `call` | `method`, `path`, `json-body?` | `write` | `npx tsx ~/.claude/skills/snappy-box/api.ts call <method> <path>` |
| `routes` | — | `read` | `npx tsx ~/.claude/skills/snappy-box/api.ts routes` |
| `sql` | `query` | `read` | `npx tsx ~/.claude/skills/snappy-box/api.ts sql "<query>"` |
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
Complete HTTP API reference for Box -- a self-editing Express server running on Robert's Mac Mini (Docker, Node 20, 180+ routes). Box is the deployment target for runtime routes, daemons, AI experiences, and autonomous agents. This skill covers direct HTTP calls (when MCP tools aren't available) plus the sandbox helpers route code can use.
POST /_deploy, POST /patch)Auth: See snappy-infra/auth-reference.md for the canonical auth setup. Quick version below.
bash# Default: Mac Mini on local network
BOX_URL="http://10.0.0.199:8080"
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
# Or any Box instance -- set BOX_URL and KEY, all commands work the same
# BOX_URL="http://your-box:8080"
# KEY="your-api-key"
Auth: x-api-key: <KEY> (header on every request)
No auth: /health only
"What do you need on Box?"
| You say... | Workflow | Key endpoints |
|---|---|---|
| Deploy a route / ship code | Deploy | POST /_deploy, POST /patch, POST /_undeploy |
| Query data / run SQL | Data | POST /system/sql, GET /state/:key, GET /cache/:key |
| Manage state / secrets / config | State | POST /state, POST /secrets, POST /config |
| Check cron / daemon status | Daemons | GET /_daemon/list, POST /_daemon/start, POST /_daemon/stop |
| System health / debugging | Observe | GET /pulse, GET /logs, GET /system/request-log |
| Build an autonomous agent | Agents | operations.md -- Daemon Agent Template |
| Search existing capabilities | Discover | GET /capabilities/search, GET /routes/search?q= |
| Deploy multi-resource app | Experience | POST /deploy-experience |
bashKEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
# Health check (no auth)
curl http://10.0.0.199:8080/health
# System pulse (auth required)
curl http://10.0.0.199:8080/pulse -H "x-api-key: $KEY"
Box is the runtime execution layer for the Snappy stack. Other skills produce intent and code; Box runs it. Box is also a deployment target distinct from Cloudflare Workers (managed by snappy-deploy).
Inputs (skills that feed this one):
snappy-infra -- provides Mac Mini SSH host, Xano API patterns Box routes proxy/callsnappy-github -- provides source for routes/daemons that get deployed via POST /_deploysnappy-content / snappy-blog -- provides AI prompts that route code passes to ai() helpersnappy-knowledge / snappy-clients -- provides config/state values stored in box_stateOutputs (skills that consume this one):
snappy-website -- receives runtime routes serving snappy.ai page data and form handlerssnappy-ops -- receives daemon-driven daily briefing data via box_state readssnappy-gateway -- receives published skill HTTP endpoints (gateway hosts the catalog; Box can host gated routes)snappy-maintenance -- receives /pulse, /logs, box_request_log data for health reportssnappy-xano-mcp -- alternate interface; MCP box_* tools map 1:1 to HTTP endpoints belowChannels (where output is delivered):
snappy-slack -- webhook fires on route.deploy, daemon errors, alertssnappy-telegram -- daemon agents post status to Robert via fetch to Telegram APIOrchestrator:
snappy-ops triggers Box daemons during the daily/weekly rhythm (cron-scheduled _daemon/start)snappy-deploy does NOT deploy Box routes -- Box self-deploys via POST /_deploy. snappy-deploy covers Vercel, Fly.io, Cloudflare Workers, and Supabase only.| Need to... | Read this |
|---|---|
| Deploy, list, patch, rollback routes | endpoints.md → Routes section |
| Run SQL, manage state/cache/secrets | endpoints.md → Data section |
| Manage functions, packs, connections | endpoints.md → Platform section |
| Write route code (sandbox helpers) | writing-routes.md |
| Query internal tables directly | tables.md |
| Organize, compose, build agents | operations.md |
| Method | Path | Purpose |
|---|---|---|
| GET | /health |
Health check (no auth) |
| GET | /_skill |
Full platform reference |
| POST | /_deploy |
Deploy/update a route |
| POST | /_undeploy |
Remove a route |
| GET | /_routes |
List all routes with meta |
| GET | /_routes/:name |
Route source + meta + pack |
| GET | /routes/search?q= |
Search routes by name/description/path |
| GET | /_routes/:name/versions |
Version history |
| POST | /_rollback |
Rollback route version |
| POST | /_daemon/start |
Start a background daemon |
| POST | /_daemon/stop |
Stop a running daemon |
| DELETE | /_daemon/:name |
Permanently delete a daemon |
| GET | /_daemon/list |
List all running daemons |
| GET | /pulse |
System pulse (routes, requests, tables, daemons) |
| POST | /_skill/reload |
Invalidate skill cache |
| POST | /capabilities/register |
Register a capability (agent-ops) |
| GET | /capabilities/search |
Search capabilities by keyword/type/tag |
| Resource | List | Get | Create/Set | Update | Delete |
|---|---|---|---|---|---|
| Routes | GET /_routes |
GET /_routes/:name |
POST /_deploy |
POST /patch |
POST /_undeploy |
| Daemons | GET /_daemon/list |
-- | POST /_daemon/start |
-- | DELETE /_daemon/:name |
| Pages | GET /pages |
GET /p/:name |
POST /pages |
POST /pages |
DELETE /pages/:name |
| State | GET /state |
GET /state/:key |
POST /state |
POST /state |
DELETE /state/:key |
| Cache | -- | GET /cache/:key |
POST /cache |
POST /cache |
DELETE /cache/:key |
| Secrets | GET /secrets |
-- | POST /secrets |
-- | DELETE /secrets/:key |
| Functions | GET /functions |
GET /functions/:name |
POST /functions |
PATCH /functions/:name |
DELETE /functions/:name |
| Connections | GET /connections |
-- | POST /connections |
-- | DELETE /connections/:name |
| Events | GET /events |
-- | POST /events |
-- | -- |
| Webhooks | GET /webhooks |
-- | POST /webhooks |
-- | DELETE /webhooks/:name |
| Packs | POST /packs |
POST /packs |
POST /packs |
-- | -- |
| Experience | -- | -- | POST /deploy-experience |
-- | -- |
| Logs | GET /logs |
-- | -- | -- | -- |
| SQL | -- | -- | POST /system/sql |
-- | -- |
| Capabilities | GET /capabilities/search |
-- | POST /capabilities/register |
-- | -- |
Routes execute in a VM sandbox with these helpers:
| Helper | Purpose |
|---|---|
ai(prompt, opts?) |
AI text gen (Gemini/Claude/GPT/OpenRouter) |
aiImage(prompt, opts?) |
Image gen via Gemini |
db.query(sql, params?) |
Postgres queries |
redis.get/set |
Redis cache |
getSecret(key) |
Read secret value |
callFunction(name, ...args) |
Call stored function |
fetch(url, opts) |
HTTP requests |
deploy/undeploy |
Self-editing |
emit(name, data) |
Track events |
Full details: writing-routes.md
Complete curl examples for every Box HTTP endpoint -- routes, SQL, state, cache, secrets, functions, packs, connections, events, webhooks, config, logs.
How to write route code that runs in the Box sandbox -- all helpers with signatures, options, and examples.
Internal database tables and their schemas for direct SQL queries.
Maintenance, composition, and autonomous agents -- registry-before-build workflow, dependency tracing, daemon agent template, self-modifying patterns, pack organization.
| MCP Tool | HTTP Equivalent |
|---|---|
box_deploy_route |
POST /_deploy |
box_undeploy_route |
POST /_undeploy |
box_get_route |
GET /_routes/:name |
box_list_routes |
GET /_routes |
box_route_versions |
GET /_routes/:name/versions |
box_rollback_route |
POST /_rollback |
box_patch |
POST /patch |
box_sql |
POST /system/sql |
box_get_state |
GET /state/:key |
box_set_state |
POST /state |
box_list_state |
GET /state |
box_delete_state |
DELETE /state/:key |
box_get_cache |
GET /cache/:key |
box_set_cache |
POST /cache |
box_delete_cache |
DELETE /cache/:key |
box_list_secrets |
GET /secrets |
box_set_secret |
POST /secrets |
box_delete_secret |
DELETE /secrets/:key |
box_list_functions |
GET /functions |
box_get_function |
GET /functions/:name |
box_create_function |
POST /functions |
box_update_function |
PATCH /functions/:name |
box_delete_function |
DELETE /functions/:name |
box_start_daemon |
POST /_daemon/start |
box_stop_daemon |
POST /_daemon/stop |
box_delete_daemon |
DELETE /_daemon/:name |
box_list_daemons |
GET /_daemon/list |
box_list_pages |
GET /pages |
box_create_page |
POST /pages |
box_deploy_experience |
POST /deploy-experience |
box_health |
GET /health |
box_info |
GET /_skill |
box_logs |
GET /logs |
box_request_log |
GET /system/request-log |
Box has a few non-obvious quirks that trip up code generation. These are the corrections that matter most.
| Issue | Wrong | Right |
|---|---|---|
Calling ai() from a daemon |
await ai(prompt) |
fetch('https://generativelanguage.googleapis.com/...') directly with key from box_secrets |
| Reading secrets from a daemon | await getSecret('KEY') |
db.query("SELECT value FROM box_secrets WHERE key=$1", ['KEY']) |
| Calling functions from a daemon | await callFunction('name', arg) |
Either query box_functions and eval, or call the route via fetch('http://localhost:8080/...') |
| Emitting from a daemon | emit('event', data) |
db.query("INSERT INTO box_events (name, data, source) VALUES ($1,$2,$3)", [...]) |
| Wrong | Right |
|---|---|
Hardcode KEY in route code |
const selfKey = await getSecret('BOX_SELF_API_KEY') |
fetch('http://10.0.0.199:8080/_deploy', ...) from a route |
fetch('http://localhost:8080/_deploy', ...) (avoids host loop) |
| Wrong | Right |
|---|---|
{"name":"resize2","method":"POST","path":"/resize2","code":"..."} |
Always include pack AND meta.description so search/registry can find it |
| Creating duplicate routes | Search first: GET /routes/search?q=resize and GET /capabilities/search?q=resize |
| Wrong | Right |
|---|---|
Inline curl with route code containing ' or " |
Write code to /tmp/box-deploy.json and -d @/tmp/box-deploy.json |
compatibility_date confusion#Box's compatibility_date is a Cloudflare Workers concept and does not apply to Box. Box runs Node 20 in Docker. Do not add Wrangler config.
| Skill | Why |
|---|---|
snappy-infra |
Provides SSH host (Roberts-Mac-mini.local / 10.0.0.199), retrieves BOX_API_KEY via docker exec, defines auth pattern shared across the Snappy stack |
snappy-gateway |
Gateway is a Cloudflare Worker for skill distribution; Box can host the gated runtime endpoints those skills call |
snappy-deploy |
Generic deploy meta-skill -- covers Vercel/Fly.io/Workers/Supabase only. Box self-deploys via its own POST /_deploy; do NOT use snappy-deploy for Box routes |
snappy-xano-mcp |
Alternative MCP interface -- box_* tools map 1:1 to HTTP endpoints in this skill |
snappy-ops |
Daily/weekly orchestrator that triggers Box daemons via cron |
snappy-maintenance |
Reads /pulse, /logs, box_request_log for health reports |
snappy-github |
Source for any route/daemon code that gets POST /_deploy-ed |
snappy-website |
Box can host page routes serving snappy.ai data; the static site is on Vercel |
All box_* MCP tools map 1:1 to HTTP endpoints (see mapping table above). Use MCP when available; fall back to HTTP/curl when MCP is unavailable.
Skill Status: COMPLETE
Line Count: < 500
Progressive Disclosure: 4 resource files
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
snappy-agent-host |
Run the REAL Claude Code, Codex, and Gemini CLIs through ACP via the skills MCP, with durable per-folder se... |
snappy-dashboard |
Snappy Dashboard — the operating system for your backend infrastructure |
snappy-deploy |
Meta-deployment skill that orchestrates ALL Snappy project deployments across the four supported platforms... |
snappy-dom-cartographer |
Master DOM mapping agent for the Snappy swarm |
snappy-maintenance |
Snappy project maintenance -- keeping all client and internal systems healthy across Vercel, Fly.io, Cloudf... |
snappy-ops |
The Snappy operator shell |
snappy-os-operator |
Operate SnappyOS like a pro through product doors only: governed connector reads, staged writes with approv... |
snappy-resident |
The non-stop user seat: drive the Snappy OS app as a real user through a real browser (agent-browser), with... |
snappy-settings |
Snappy Settings -- central environment and credentials layer for the entire Snappy operating system |
snappy-xano-mcp |
THE EXISTING, DEPLOYED Snappy MCP server: the Cloudflare Worker exposing the Snappy Xano API (email, calend... |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
---
name: snappy-box
reports_to: build
head: false
description: >
Box server -- self-editing Express server on Mac Mini (Docker, Node 20, 180+ routes)
exposing HTTP API for route deployment, state, cache, secrets, functions, daemons,
packs, pages, SQL, AI generation, capability registry, and autonomous agents. Covers
curl patterns, MCP→HTTP mapping, sandbox helpers, self-modifying routes, daemon
agent template (observe/think/act/record), capability search-before-build, dependency
tracing, pack organization.
Triggers on: box server, box api, deploy route, mac mini server, self-editing server,
daemon agent, cron job agent, self-modifying, spawn route, capability registry, box
sql, box state, box cache, box secrets, box functions, box pulse, /_deploy, /_daemon,
/system/sql, route sandbox, pack organization, dependency tracing, autonomous agent.
---
# Box HTTP API
## Purpose
Complete HTTP API reference for **Box** -- a self-editing Express server running on Robert's Mac Mini (Docker, Node 20, 180+ routes). Box is the deployment target for runtime routes, daemons, AI experiences, and autonomous agents. This skill covers direct HTTP calls (when MCP tools aren't available) plus the sandbox helpers route code can use.
## When to Use This Skill
- Deploying or managing routes on Box server (`POST /_deploy`, `POST /patch`)
- Running SQL, managing state/cache/secrets via HTTP
- Starting/stopping daemons, building autonomous agents
- Building tools or integrations that call Box endpoints
- Any mention of "box server", "box api", "deploy route", "mac mini server", "spawn route"
- When MCP tools aren't available but HTTP access is
---
## Connection
> **Auth**: See [snappy-infra/auth-reference.md](../snappy-infra/auth-reference.md) for the canonical auth setup. Quick version below.
```bash
# Default: Mac Mini on local network
BOX_URL="http://10.0.0.199:8080"
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
# Or any Box instance -- set BOX_URL and KEY, all commands work the same
# BOX_URL="http://your-box:8080"
# KEY="your-api-key"
```
```
Auth: x-api-key: <KEY> (header on every request)
No auth: /health only
```
---
## Quick Start Interview
**"What do you need on Box?"**
| You say... | Workflow | Key endpoints |
|------------|----------|---------------|
| Deploy a route / ship code | **Deploy** | `POST /_deploy`, `POST /patch`, `POST /_undeploy` |
| Query data / run SQL | **Data** | `POST /system/sql`, `GET /state/:key`, `GET /cache/:key` |
| Manage state / secrets / config | **State** | `POST /state`, `POST /secrets`, `POST /config` |
| Check cron / daemon status | **Daemons** | `GET /_daemon/list`, `POST /_daemon/start`, `POST /_daemon/stop` |
| System health / debugging | **Observe** | `GET /pulse`, `GET /logs`, `GET /system/request-log` |
| Build an autonomous agent | **Agents** | [operations.md](operations.md) -- Daemon Agent Template |
| Search existing capabilities | **Discover** | `GET /capabilities/search`, `GET /routes/search?q=` |
| Deploy multi-resource app | **Experience** | `POST /deploy-experience` |
### Verify Connection
```bash
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
# Health check (no auth)
curl http://10.0.0.199:8080/health
# System pulse (auth required)
curl http://10.0.0.199:8080/pulse -H "x-api-key: $KEY"
```
---
## Workflow
Box is the **runtime execution layer** for the Snappy stack. Other skills produce intent and code; Box runs it. Box is also a **deployment target** distinct from Cloudflare Workers (managed by `snappy-deploy`).
**Inputs (skills that feed this one):**
- `snappy-infra` -- provides Mac Mini SSH host, Xano API patterns Box routes proxy/call
- `snappy-github` -- provides source for routes/daemons that get deployed via `POST /_deploy`
- `snappy-content` / `snappy-blog` -- provides AI prompts that route code passes to `ai()` helper
- `snappy-knowledge` / `snappy-clients` -- provides config/state values stored in `box_state`
**Outputs (skills that consume this one):**
- `snappy-website` -- receives runtime routes serving snappy.ai page data and form handlers
- `snappy-ops` -- receives daemon-driven daily briefing data via `box_state` reads
- `snappy-gateway` -- receives published skill HTTP endpoints (gateway hosts the catalog; Box can host gated routes)
- `snappy-maintenance` -- receives `/pulse`, `/logs`, `box_request_log` data for health reports
- `snappy-xano-mcp` -- alternate interface; MCP `box_*` tools map 1:1 to HTTP endpoints below
**Channels (where output is delivered):**
- `snappy-slack` -- webhook fires on `route.deploy`, daemon errors, alerts
- `snappy-telegram` -- daemon agents post status to Robert via fetch to Telegram API
- HTTP responses to upstream services (Xano webhook receivers, Vercel functions)
**Orchestrator:**
- `snappy-ops` triggers Box daemons during the daily/weekly rhythm (cron-scheduled `_daemon/start`)
- `snappy-deploy` does NOT deploy Box routes -- Box self-deploys via `POST /_deploy`. `snappy-deploy` covers Vercel, Fly.io, Cloudflare Workers, and Supabase only.
---
## Navigation Guide
| Need to... | Read this |
|------------|-----------|
| Deploy, list, patch, rollback routes | [endpoints.md](endpoints.md) → Routes section |
| Run SQL, manage state/cache/secrets | [endpoints.md](endpoints.md) → Data section |
| Manage functions, packs, connections | [endpoints.md](endpoints.md) → Platform section |
| Write route code (sandbox helpers) | [writing-routes.md](writing-routes.md) |
| Query internal tables directly | [tables.md](tables.md) |
| Organize, compose, build agents | [operations.md](operations.md) |
---
## System Endpoints (server.js built-ins)
| Method | Path | Purpose |
|--------|------|---------|
| GET | `/health` | Health check (no auth) |
| GET | `/_skill` | Full platform reference |
| POST | `/_deploy` | Deploy/update a route |
| POST | `/_undeploy` | Remove a route |
| GET | `/_routes` | List all routes with meta |
| GET | `/_routes/:name` | Route source + meta + pack |
| GET | `/routes/search?q=` | Search routes by name/description/path |
| GET | `/_routes/:name/versions` | Version history |
| POST | `/_rollback` | Rollback route version |
| POST | `/_daemon/start` | Start a background daemon |
| POST | `/_daemon/stop` | Stop a running daemon |
| DELETE | `/_daemon/:name` | Permanently delete a daemon |
| GET | `/_daemon/list` | List all running daemons |
| GET | `/pulse` | System pulse (routes, requests, tables, daemons) |
| POST | `/_skill/reload` | Invalidate skill cache |
| POST | `/capabilities/register` | Register a capability (agent-ops) |
| GET | `/capabilities/search` | Search capabilities by keyword/type/tag |
---
## CRUD Endpoint Map
| Resource | List | Get | Create/Set | Update | Delete |
|----------|------|-----|------------|--------|--------|
| Routes | `GET /_routes` | `GET /_routes/:name` | `POST /_deploy` | `POST /patch` | `POST /_undeploy` |
| Daemons | `GET /_daemon/list` | -- | `POST /_daemon/start` | -- | `DELETE /_daemon/:name` |
| Pages | `GET /pages` | `GET /p/:name` | `POST /pages` | `POST /pages` | `DELETE /pages/:name` |
| State | `GET /state` | `GET /state/:key` | `POST /state` | `POST /state` | `DELETE /state/:key` |
| Cache | -- | `GET /cache/:key` | `POST /cache` | `POST /cache` | `DELETE /cache/:key` |
| Secrets | `GET /secrets` | -- | `POST /secrets` | -- | `DELETE /secrets/:key` |
| Functions | `GET /functions` | `GET /functions/:name` | `POST /functions` | `PATCH /functions/:name` | `DELETE /functions/:name` |
| Connections | `GET /connections` | -- | `POST /connections` | -- | `DELETE /connections/:name` |
| Events | `GET /events` | -- | `POST /events` | -- | -- |
| Webhooks | `GET /webhooks` | -- | `POST /webhooks` | -- | `DELETE /webhooks/:name` |
| Packs | `POST /packs` | `POST /packs` | `POST /packs` | -- | -- |
| Experience | -- | -- | `POST /deploy-experience` | -- | -- |
| Logs | `GET /logs` | -- | -- | -- | -- |
| SQL | -- | -- | `POST /system/sql` | -- | -- |
| Capabilities | `GET /capabilities/search` | -- | `POST /capabilities/register` | -- | -- |
---
## Route Sandbox API (Quick Reference)
Routes execute in a VM sandbox with these helpers:
| Helper | Purpose |
|--------|---------|
| `ai(prompt, opts?)` | AI text gen (Gemini/Claude/GPT/OpenRouter) |
| `aiImage(prompt, opts?)` | Image gen via Gemini |
| `db.query(sql, params?)` | Postgres queries |
| `redis.get/set` | Redis cache |
| `getSecret(key)` | Read secret value |
| `callFunction(name, ...args)` | Call stored function |
| `fetch(url, opts)` | HTTP requests |
| `deploy/undeploy` | Self-editing |
| `emit(name, data)` | Track events |
Full details: [writing-routes.md](writing-routes.md)
---
## Resource Files
### [endpoints.md](endpoints.md)
Complete curl examples for every Box HTTP endpoint -- routes, SQL, state, cache, secrets, functions, packs, connections, events, webhooks, config, logs.
### [writing-routes.md](writing-routes.md)
How to write route code that runs in the Box sandbox -- all helpers with signatures, options, and examples.
### [tables.md](tables.md)
Internal database tables and their schemas for direct SQL queries.
### [operations.md](operations.md)
Maintenance, composition, and autonomous agents -- registry-before-build workflow, dependency tracing, daemon agent template, self-modifying patterns, pack organization.
---
## MCP Tool → HTTP Endpoint Mapping
| MCP Tool | HTTP Equivalent |
|----------|----------------|
| `box_deploy_route` | `POST /_deploy` |
| `box_undeploy_route` | `POST /_undeploy` |
| `box_get_route` | `GET /_routes/:name` |
| `box_list_routes` | `GET /_routes` |
| `box_route_versions` | `GET /_routes/:name/versions` |
| `box_rollback_route` | `POST /_rollback` |
| `box_patch` | `POST /patch` |
| `box_sql` | `POST /system/sql` |
| `box_get_state` | `GET /state/:key` |
| `box_set_state` | `POST /state` |
| `box_list_state` | `GET /state` |
| `box_delete_state` | `DELETE /state/:key` |
| `box_get_cache` | `GET /cache/:key` |
| `box_set_cache` | `POST /cache` |
| `box_delete_cache` | `DELETE /cache/:key` |
| `box_list_secrets` | `GET /secrets` |
| `box_set_secret` | `POST /secrets` |
| `box_delete_secret` | `DELETE /secrets/:key` |
| `box_list_functions` | `GET /functions` |
| `box_get_function` | `GET /functions/:name` |
| `box_create_function` | `POST /functions` |
| `box_update_function` | `PATCH /functions/:name` |
| `box_delete_function` | `DELETE /functions/:name` |
| `box_start_daemon` | `POST /_daemon/start` |
| `box_stop_daemon` | `POST /_daemon/stop` |
| `box_delete_daemon` | `DELETE /_daemon/:name` |
| `box_list_daemons` | `GET /_daemon/list` |
| `box_list_pages` | `GET /pages` |
| `box_create_page` | `POST /pages` |
| `box_deploy_experience` | `POST /deploy-experience` |
| `box_health` | `GET /health` |
| `box_info` | `GET /_skill` |
| `box_logs` | `GET /logs` |
| `box_request_log` | `GET /system/request-log` |
---
## What AI Agents Get Wrong
Box has a few non-obvious quirks that trip up code generation. These are the corrections that matter most.
### Daemons vs Routes -- sandbox is different
| Issue | Wrong | Right |
|-------|-------|-------|
| Calling `ai()` from a daemon | `await ai(prompt)` | `fetch('https://generativelanguage.googleapis.com/...')` directly with key from `box_secrets` |
| Reading secrets from a daemon | `await getSecret('KEY')` | `db.query("SELECT value FROM box_secrets WHERE key=$1", ['KEY'])` |
| Calling functions from a daemon | `await callFunction('name', arg)` | Either query `box_functions` and eval, or call the route via `fetch('http://localhost:8080/...')` |
| Emitting from a daemon | `emit('event', data)` | `db.query("INSERT INTO box_events (name, data, source) VALUES ($1,$2,$3)", [...])` |
### Self-modification -- use the self-API key, not the user key
| Wrong | Right |
|-------|-------|
| Hardcode `KEY` in route code | `const selfKey = await getSecret('BOX_SELF_API_KEY')` |
| `fetch('http://10.0.0.199:8080/_deploy', ...)` from a route | `fetch('http://localhost:8080/_deploy', ...)` (avoids host loop) |
### Pack and meta -- orphaned routes are unfindable
| Wrong | Right |
|-------|-------|
| `{"name":"resize2","method":"POST","path":"/resize2","code":"..."}` | Always include `pack` AND `meta.description` so search/registry can find it |
| Creating duplicate routes | Search first: `GET /routes/search?q=resize` and `GET /capabilities/search?q=resize` |
### Deploy with quoted code -- temp file beats inline JSON
| Wrong | Right |
|-------|-------|
| Inline curl with route code containing `'` or `"` | Write code to `/tmp/box-deploy.json` and `-d @/tmp/box-deploy.json` |
### `compatibility_date` confusion
Box's `compatibility_date` is a Cloudflare Workers concept and **does not apply** to Box. Box runs Node 20 in Docker. Do not add Wrangler config.
---
## Related Skills
| Skill | Why |
|-------|-----|
| `snappy-infra` | Provides SSH host (`Roberts-Mac-mini.local` / `10.0.0.199`), retrieves `BOX_API_KEY` via `docker exec`, defines auth pattern shared across the Snappy stack |
| `snappy-gateway` | Gateway is a Cloudflare Worker for skill distribution; Box can host the gated runtime endpoints those skills call |
| `snappy-deploy` | Generic deploy meta-skill -- covers Vercel/Fly.io/Workers/Supabase only. Box self-deploys via its own `POST /_deploy`; do NOT use snappy-deploy for Box routes |
| `snappy-xano-mcp` | Alternative MCP interface -- `box_*` tools map 1:1 to HTTP endpoints in this skill |
| `snappy-ops` | Daily/weekly orchestrator that triggers Box daemons via cron |
| `snappy-maintenance` | Reads `/pulse`, `/logs`, `box_request_log` for health reports |
| `snappy-github` | Source for any route/daemon code that gets `POST /_deploy`-ed |
| `snappy-website` | Box can host page routes serving snappy.ai data; the static site is on Vercel |
### MCP Tools (Alternative Interface)
All `box_*` MCP tools map 1:1 to HTTP endpoints (see mapping table above). Use MCP when available; fall back to HTTP/curl when MCP is unavailable.
---
**Skill Status**: COMPLETE
**Line Count**: < 500
**Progressive Disclosure**: 4 resource files
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
## Near neighbours
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
| `snappy-agent-host` | Run the REAL Claude Code, Codex, and Gemini CLIs through ACP via the skills MCP, with durable per-folder se... |
| `snappy-dashboard` | Snappy Dashboard — the operating system for your backend infrastructure |
| `snappy-deploy` | Meta-deployment skill that orchestrates ALL Snappy project deployments across the four supported platforms... |
| `snappy-dom-cartographer` | Master DOM mapping agent for the Snappy swarm |
| `snappy-maintenance` | Snappy project maintenance -- keeping all client and internal systems healthy across Vercel, Fly.io, Cloudf... |
| `snappy-ops` | The Snappy operator shell |
| `snappy-os-operator` | Operate SnappyOS like a pro through product doors only: governed connector reads, staged writes with approv... |
| `snappy-resident` | The non-stop user seat: drive the Snappy OS app as a real user through a real browser (agent-browser), with... |
| `snappy-settings` | Snappy Settings -- central environment and credentials layer for the entire Snappy operating system |
| `snappy-xano-mcp` | THE EXISTING, DEPLOYED Snappy MCP server: the Cloudflare Worker exposing the Snappy Xano API (email, calend... |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
#!/usr/bin/env npx tsx
/**
* snappy-box/api.ts -- Box server HTTP API for all snappy-* skills.
*
* Box runs on Mac Mini at http://10.0.0.199:8080 (Docker, Node 20).
* Auth via x-api-key header with BOX_API_KEY.
*
* NOTE: BOX_API_KEY must be in .env.cache. If missing, retrieve it:
* ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY"
* Then add BOX_API_KEY=<value> to ~/.claude/skills/snappy-settings/.env.cache
*
* Boundary: box = the Box self-editing server on Mac Mini (routes, SQL, deploy routes).
* snappy-deploy = trigger Vercel/Fly deployments.
* snappy-infra = health probes + SSH to Mac Mini.
*
* Usage:
* npx tsx api.ts routes # list deployed routes
* npx tsx api.ts sql "SELECT 1" # run SQL query
* npx tsx api.ts call GET /pulse # call any route
*
* Or import as module:
* import { listRoutes, runSql, deploy, callRoute } from "../snappy-box/api.ts";
*/
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
import { RefusedError, isRefusedError, printRefusal } from "../snappy-settings/refusal-codes.ts";
import { handServices, serviceRefusal, serviceUrl } from "../snappy-settings/hand-resources.ts";
/** THE ADDRESS COMES FROM THE ONE REGISTRY ⟨lane mini-reads, 2026-09-09⟩. */
const BOX_URL = serviceUrl("box-server");
function apiKey(): string {
return env("BOX_API_KEY", false);
}
async function box(
path: string,
options?: { method?: string; body?: unknown }
): Promise<unknown> {
if (!apiKey()) {
throw new RefusedError(
"missing_credential",
"[snappy-box] BOX_API_KEY not in .env.cache. " +
"Retrieve via: ssh robertboulos@10.0.0.199 " +
'"/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY"'
);
}
const res = await boxFetch(path, options).catch((cause) => {
// A LAN server that is off is not a crash to print a stack for: it is a
// condition the caller can act on, and the closed table has a word for it.
//
// ONE SENTENCE, OFFERING NOTHING ELSE ⟨lane mini-reads, 2026-09-09⟩. This
// used to end with "or call a verb that reads local state" — snappy-box
// has NO such verb: `routes`, `sql` and `call` all go through `box()`, so
// the offer sent every reader looking for a road that does not exist.
throw serviceRefusal("box-server", cause);
});
return boxBody(res, path);
}
async function boxFetch(path: string, options?: { method?: string; body?: unknown }): Promise<Response> {
const key = apiKey()!;
return fetch(`${BOX_URL}${path}`, {
method: options?.method || "GET",
headers: {
"x-api-key": key,
...(options?.body ? { "Content-Type": "application/json" } : {}),
},
body: options?.body ? JSON.stringify(options.body) : undefined,
});
}
async function boxBody(res: Response, path: string): Promise<unknown> {
const text = await res.text();
try {
return JSON.parse(text);
} catch {
if (!res.ok) throw new RefusedError("upstream_error", `Box ${path} failed (${res.status}): ${text}`);
return text;
}
}
// --- Public API ---
/** List all deployed routes. */
export async function listRoutes() {
return box("/_routes");
}
/** Run a SQL query against Box's internal database. */
export async function runSql(query: string) {
return box("/system/sql", { method: "POST", body: { query } });
}
/**
* Deploy a route to Box.
* routeConfig should include: name, method, path, code, and optionally pack, meta.
*/
export async function deploy(routeConfig: {
name: string;
method: string;
path: string;
code: string;
pack?: string;
meta?: { description?: string; inputs?: unknown[]; outputs?: unknown };
}) {
return box("/_deploy", { method: "POST", body: routeConfig });
}
/** Generic route caller -- hit any Box endpoint. */
export async function callRoute(method: string, path: string, body?: unknown) {
return box(path, { method, body: body || undefined });
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-box",
description: "Box server -- self-editing Express server on Mac Mini (Docker, Node 20, 180+ routes) exposing HTTP API for route deployment, state, cache, secrets, functions, daemons, packs, pages, SQL, AI generation, capability registry, and autonomous agents. Covers curl patterns, MCP→HTTP mapping, sandbox helpers, self-modifying routes, daemon agent template (observe/think/act/record), capability search-before-build, dependency tracing, pack organization. Triggers on: box server, box api, deploy route, mac mini server, self-editing server, daemon agent, cron job agent, self-modifying, spawn route, capability registry, box sql, box state, box cache, box secrets, box functions, box pulse, /_deploy, /_daemon, /system/sql, route sandbox, pack organization, dependency tracing, autonomous agent.",
managed: true,
requires: [] as string[],
/** WHAT THIS HAND NEEDS THAT IS NOT A CREDENTIAL ⟨lane mini-reads,
* 2026-09-09⟩. The OWNER's call is that the Box server stays off; this
* hand keeps refusing BY NAME rather than pretending to have a local read.
* Declared so a picker can see the dependency without running the verb. */
resources: handServices("box-server"),
refusals: refusalTable("unknown_verb", "missing_argument", "missing_credential", "service_unavailable", "upstream_error"),
verbs: {
call: {
args: ["method","path","json-body?"], effect: "write",
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { method: { type: "string", description: "HTTP method for the call", enum: ["GET", "POST", "PATCH", "PUT", "DELETE"] }, path: { type: "string", description: "Route path on the box, beginning with a slash" }, "json-body": { type: "string", description: "JSON request body; omit for a body-less method" } } },
},
routes: {
args: [], effect: "read",
class: "read", execution: "call", openWorld: true,
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
},
sql: {
args: ["query"], effect: "read",
class: "read", execution: "call", openWorld: true,
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { query: { type: "string", description: "Read-only SQL statement to run against the box" } } },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "routes": {
const data = await listRoutes() as { deployed?: { name: string; method: string; path: string }[] };
if (data?.deployed) {
for (const r of data.deployed) {
console.log(`${r.method}\t${r.path}\t${r.name}`);
}
} else {
console.log(JSON.stringify(data, null, 2));
}
break;
}
case "sql": {
const [query] = args;
if (!query) { console.error("Usage: api.ts sql <query>"); process.exit(1); }
const result = await runSql(query);
console.log(JSON.stringify(result, null, 2));
break;
}
case "call": {
const [method, path, ...bodyParts] = args;
if (!method || !path) { console.error("Usage: api.ts call <METHOD> <path> [json-body]"); process.exit(1); }
const body = bodyParts.length ? JSON.parse(bodyParts.join(" ")) : undefined;
const result = await callRoute(method, path, body);
console.log(JSON.stringify(result, null, 2));
break;
}
default:
console.log("Usage: npx tsx api.ts [routes|sql|call] ...");
}
})().catch((error) => {
// THE ONE PLACE THIS HAND SAYS NO. Before this, `routes` against a Box
// server that was off printed node's unhandled-rejection stack trace and
// exited 1 with an EMPTY stdout — nothing for a caller to branch on.
if (isRefusedError(error)) { printRefusal(error.refusal); console.error(error.message); return; }
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
}
#!/usr/bin/env npx tsx
/**
* snappy-box/api.ts -- Box server HTTP API for all snappy-* skills.
*
* Box runs on Mac Mini at http://10.0.0.199:8080 (Docker, Node 20).
* Auth via x-api-key header with BOX_API_KEY.
*
* NOTE: BOX_API_KEY must be in .env.cache. If missing, retrieve it:
* ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY"
* Then add BOX_API_KEY=<value> to ~/.claude/skills/snappy-settings/.env.cache
*
* Boundary: box = the Box self-editing server on Mac Mini (routes, SQL, deploy routes).
* snappy-deploy = trigger Vercel/Fly deployments.
* snappy-infra = health probes + SSH to Mac Mini.
*
* Usage:
* npx tsx api.ts routes # list deployed routes
* npx tsx api.ts sql "SELECT 1" # run SQL query
* npx tsx api.ts call GET /pulse # call any route
*
* Or import as module:
* import { listRoutes, runSql, deploy, callRoute } from "../snappy-box/api.ts";
*/
import { env } from "../snappy-settings/load.ts";
import { realpathSync } from "fs";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
import { RefusedError, isRefusedError, printRefusal } from "../snappy-settings/refusal-codes.ts";
import { handServices, serviceRefusal, serviceUrl } from "../snappy-settings/hand-resources.ts";
/** THE ADDRESS COMES FROM THE ONE REGISTRY ⟨lane mini-reads, 2026-09-09⟩. */
const BOX_URL = serviceUrl("box-server");
function apiKey(): string {
return env("BOX_API_KEY", false);
}
async function box(
path: string,
options?: { method?: string; body?: unknown }
): Promise<unknown> {
if (!apiKey()) {
throw new RefusedError(
"missing_credential",
"[snappy-box] BOX_API_KEY not in .env.cache. " +
"Retrieve via: ssh robertboulos@10.0.0.199 " +
'"/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY"'
);
}
const res = await boxFetch(path, options).catch((cause) => {
// A LAN server that is off is not a crash to print a stack for: it is a
// condition the caller can act on, and the closed table has a word for it.
//
// ONE SENTENCE, OFFERING NOTHING ELSE ⟨lane mini-reads, 2026-09-09⟩. This
// used to end with "or call a verb that reads local state" — snappy-box
// has NO such verb: `routes`, `sql` and `call` all go through `box()`, so
// the offer sent every reader looking for a road that does not exist.
throw serviceRefusal("box-server", cause);
});
return boxBody(res, path);
}
async function boxFetch(path: string, options?: { method?: string; body?: unknown }): Promise<Response> {
const key = apiKey()!;
return fetch(`${BOX_URL}${path}`, {
method: options?.method || "GET",
headers: {
"x-api-key": key,
...(options?.body ? { "Content-Type": "application/json" } : {}),
},
body: options?.body ? JSON.stringify(options.body) : undefined,
});
}
async function boxBody(res: Response, path: string): Promise<unknown> {
const text = await res.text();
try {
return JSON.parse(text);
} catch {
if (!res.ok) throw new RefusedError("upstream_error", `Box ${path} failed (${res.status}): ${text}`);
return text;
}
}
// --- Public API ---
/** List all deployed routes. */
export async function listRoutes() {
return box("/_routes");
}
/** Run a SQL query against Box's internal database. */
export async function runSql(query: string) {
return box("/system/sql", { method: "POST", body: { query } });
}
/**
* Deploy a route to Box.
* routeConfig should include: name, method, path, code, and optionally pack, meta.
*/
export async function deploy(routeConfig: {
name: string;
method: string;
path: string;
code: string;
pack?: string;
meta?: { description?: string; inputs?: unknown[]; outputs?: unknown };
}) {
return box("/_deploy", { method: "POST", body: routeConfig });
}
/** Generic route caller -- hit any Box endpoint. */
export async function callRoute(method: string, path: string, body?: unknown) {
return box(path, { method, body: body || undefined });
}
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-box",
description: "Box server -- self-editing Express server on Mac Mini (Docker, Node 20, 180+ routes) exposing HTTP API for route deployment, state, cache, secrets, functions, daemons, packs, pages, SQL, AI generation, capability registry, and autonomous agents. Covers curl patterns, MCP→HTTP mapping, sandbox helpers, self-modifying routes, daemon agent template (observe/think/act/record), capability search-before-build, dependency tracing, pack organization. Triggers on: box server, box api, deploy route, mac mini server, self-editing server, daemon agent, cron job agent, self-modifying, spawn route, capability registry, box sql, box state, box cache, box secrets, box functions, box pulse, /_deploy, /_daemon, /system/sql, route sandbox, pack organization, dependency tracing, autonomous agent.",
managed: true,
requires: [] as string[],
/** WHAT THIS HAND NEEDS THAT IS NOT A CREDENTIAL ⟨lane mini-reads,
* 2026-09-09⟩. The OWNER's call is that the Box server stays off; this
* hand keeps refusing BY NAME rather than pretending to have a local read.
* Declared so a picker can see the dependency without running the verb. */
resources: handServices("box-server"),
refusals: refusalTable("unknown_verb", "missing_argument", "missing_credential", "service_unavailable", "upstream_error"),
verbs: {
call: {
args: ["method","path","json-body?"], effect: "write",
class: "additive-write", execution: "call", openWorld: true,
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { method: { type: "string", description: "HTTP method for the call", enum: ["GET", "POST", "PATCH", "PUT", "DELETE"] }, path: { type: "string", description: "Route path on the box, beginning with a slash" }, "json-body": { type: "string", description: "JSON request body; omit for a body-less method" } } },
},
routes: {
args: [], effect: "read",
class: "read", execution: "call", openWorld: true,
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
},
sql: {
args: ["query"], effect: "read",
class: "read", execution: "call", openWorld: true,
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
inputSchema: { properties: { query: { type: "string", description: "Read-only SQL statement to run against the box" } } },
},
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
(async () => {
const [, , cmd, ...args] = process.argv;
switch (cmd) {
case "routes": {
const data = await listRoutes() as { deployed?: { name: string; method: string; path: string }[] };
if (data?.deployed) {
for (const r of data.deployed) {
console.log(`${r.method}\t${r.path}\t${r.name}`);
}
} else {
console.log(JSON.stringify(data, null, 2));
}
break;
}
case "sql": {
const [query] = args;
if (!query) { console.error("Usage: api.ts sql <query>"); process.exit(1); }
const result = await runSql(query);
console.log(JSON.stringify(result, null, 2));
break;
}
case "call": {
const [method, path, ...bodyParts] = args;
if (!method || !path) { console.error("Usage: api.ts call <METHOD> <path> [json-body]"); process.exit(1); }
const body = bodyParts.length ? JSON.parse(bodyParts.join(" ")) : undefined;
const result = await callRoute(method, path, body);
console.log(JSON.stringify(result, null, 2));
break;
}
default:
console.log("Usage: npx tsx api.ts [routes|sql|call] ...");
}
})().catch((error) => {
// THE ONE PLACE THIS HAND SAYS NO. Before this, `routes` against a Box
// server that was off printed node's unhandled-rejection stack trace and
// exited 1 with an EMPTY stdout — nothing for a caller to branch on.
if (isRefusedError(error)) { printRefusal(error.refusal); console.error(error.message); return; }
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
}
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
"missing_argument",
"missing_credential",
// ⟨lane mini-reads, 2026-09-09⟩ `box()` threw this on every call whenever
// the Box server was off — which is its standing state by the owner's call —
// and the table never named it.
"service_unavailable",
"upstream_error",
] as const satisfies readonly RefusalCode[];
test("snappy-box: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-box: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
/**
* THE REFUSAL OFFERS NO ROAD THAT DOES NOT EXIST ⟨lane mini-reads, 2026-09-09⟩.
*
* RED BEFORE: with the Box server off — its standing state, by the owner's
* call — every verb refused with "…or call a verb that reads local state".
* snappy-box HAS no such verb: `routes`, `sql` and `call` all go through the
* same `box()`. A reader spent a turn looking for a road that was never built.
*/
test("snappy-box: every verb reaches the Box server, so no refusal may offer a local one", async () => {
const raw = (await import("node:fs")).readFileSync(new URL("./api.ts", import.meta.url), "utf8");
// Comments stripped: the paragraph that RECORDS the old sentence quotes it,
// and a test that reads a doctrine note as code grades the wrong thing.
const source = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
assert.ok(!source.includes("call a verb that reads local state"), "the refusal offers a verb this hand does not have");
for (const verb of Object.keys(HAND_CONTRACT.verbs)) {
assert.ok(source.includes(`case "${verb}"`), `${verb} is declared but has no CLI arm`);
}
});
test("snappy-box: the refusal says the Box server is off on the mini, in the owner's words", async () => {
const { serviceRefusal } = await import("../snappy-settings/hand-resources.ts");
const message = serviceRefusal("box-server", new Error("fetch failed")).refusal.message;
assert.ok(message.startsWith("the Box server is off on the mini"), message);
});
test("snappy-box: the contract names the Box server as the thing it needs", () => {
const resources = (HAND_CONTRACT as { resources?: Record<string, { kind: string }> }).resources ?? {};
assert.deepEqual(Object.keys(resources), ["box-server"]);
});
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
"missing_argument",
"missing_credential",
// ⟨lane mini-reads, 2026-09-09⟩ `box()` threw this on every call whenever
// the Box server was off — which is its standing state by the owner's call —
// and the table never named it.
"service_unavailable",
"upstream_error",
] as const satisfies readonly RefusalCode[];
test("snappy-box: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-box: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
/**
* THE REFUSAL OFFERS NO ROAD THAT DOES NOT EXIST ⟨lane mini-reads, 2026-09-09⟩.
*
* RED BEFORE: with the Box server off — its standing state, by the owner's
* call — every verb refused with "…or call a verb that reads local state".
* snappy-box HAS no such verb: `routes`, `sql` and `call` all go through the
* same `box()`. A reader spent a turn looking for a road that was never built.
*/
test("snappy-box: every verb reaches the Box server, so no refusal may offer a local one", async () => {
const raw = (await import("node:fs")).readFileSync(new URL("./api.ts", import.meta.url), "utf8");
// Comments stripped: the paragraph that RECORDS the old sentence quotes it,
// and a test that reads a doctrine note as code grades the wrong thing.
const source = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
assert.ok(!source.includes("call a verb that reads local state"), "the refusal offers a verb this hand does not have");
for (const verb of Object.keys(HAND_CONTRACT.verbs)) {
assert.ok(source.includes(`case "${verb}"`), `${verb} is declared but has no CLI arm`);
}
});
test("snappy-box: the refusal says the Box server is off on the mini, in the owner's words", async () => {
const { serviceRefusal } = await import("../snappy-settings/hand-resources.ts");
const message = serviceRefusal("box-server", new Error("fetch failed")).refusal.message;
assert.ok(message.startsWith("the Box server is off on the mini"), message);
});
test("snappy-box: the contract names the Box server as the thing it needs", () => {
const resources = (HAND_CONTRACT as { resources?: Record<string, { kind: string }> }).resources ?? {};
assert.deepEqual(Object.keys(resources), ["box-server"]);
});
All examples use $KEY for the API key. Set it first:
bashKEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
All errors follow the same shape:
json{"error": "Invalid or missing API key"} // 401 -- bad/missing x-api-key
{"error": "Route not found"} // 404 -- resource doesn't exist
{"error": "name, path, and code are required"} // 400 -- missing fields
{"error": "relation \"x\" does not exist"} // SQL error passthrough
Route code with quotes/special characters breaks inline curl JSON. Use a temp file:
bashcat > /tmp/box-deploy.json << 'ENDJSON'
{
"name": "my-route",
"method": "POST",
"path": "/my-route",
"code": "const { text } = req.body;\nconst result = await ai('Summarize: ' + text, { system: 'Be concise.' });\nres.json({ summary: result });"
}
ENDJSON
curl -s -X POST http://10.0.0.199:8080/_deploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d @/tmp/box-deploy.json
Rule of thumb: If your code has single quotes, use @/tmp/file.json. Simple one-liners can go inline.
bash# Deploy a route
curl -X POST http://10.0.0.199:8080/_deploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"hello","method":"GET","path":"/hello","code":"res.json({msg:\"hi\"})","pack":"my-pack"}'
# → {"success":true,"route":{"name":"hello","method":"GET","path":"/hello"},"pack":"my-pack","validation":{"valid":true,...}}
# Deploy with meta (typed interface for discoverability)
curl -X POST http://10.0.0.199:8080/_deploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"greet","method":"POST","path":"/greet","code":"const {name}=req.body; res.json({greeting:\"Hello \"+name})","meta":{"description":"Greet by name","inputs":[{"name":"name","type":"string","required":true}],"outputs":{"greeting":"string"}}}'
# List all routes (with descriptions from meta)
curl http://10.0.0.199:8080/_routes -H "x-api-key: $KEY"
# → {"deployed":[{"name":"hello","method":"GET","path":"/hello","meta":{...},"pack":"..."},...], "files":[...]}
# Get route source + meta
curl http://10.0.0.199:8080/_routes/hello -H "x-api-key: $KEY"
# → {"name":"hello","code":"res.json(...)","meta":{...},"pack":"my-pack"}
# Undeploy
curl -X POST http://10.0.0.199:8080/_undeploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"hello"}'
# → {"success":true,"removed":"hello"}
# Version history
curl http://10.0.0.199:8080/_routes/hello/versions -H "x-api-key: $KEY"
# Rollback to previous version
curl -X POST http://10.0.0.199:8080/_rollback \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"hello"}'
bash# Patch route code (find & replace)
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"route","name":"hello","old_string":"hi","new_string":"hello world"}'
# Patch function code
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"function","name":"double","old_string":"* 2","new_string":"* 3"}'
# Patch page part (template, script, style, head)
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"page","name":"dashboard","part":"template","old_string":"v1","new_string":"v2"}'
# Replace all occurrences
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"route","name":"hello","old_string":"foo","new_string":"bar","replace_all":true}'
bash# List running daemons
curl http://10.0.0.199:8080/_daemon/list -H "x-api-key: $KEY"
# → {"daemons":[{"name":"self-monitor","interval_ms":60000,"cron":null,"started":"2026-..."},...]}
# Start daemon with interval (every 60s)
curl -X POST http://10.0.0.199:8080/_daemon/start \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-task","code":"console.log(\"tick\")","interval_ms":60000}'
# → {"success":true,"daemon":"my-task","interval_ms":60000,"cron":null}
# Start daemon with cron schedule
curl -X POST http://10.0.0.199:8080/_daemon/start \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"nightly","code":"await db.query(\"DELETE FROM logs WHERE created_at < NOW() - INTERVAL '30 days'\")","cron":"0 3 * * *"}'
# Stop (deactivates, keeps in DB)
curl -X POST http://10.0.0.199:8080/_daemon/stop \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-task"}'
# → {"success":true,"stopped":"my-task"}
# Delete (permanently removes)
curl -X DELETE http://10.0.0.199:8080/_daemon/my-task -H "x-api-key: $KEY"
# → {"success":true,"deleted":"my-task"}
Daemon code runs in a sandbox with: db, redis, fetch, console, require (safe subset).
bash# List all pages
curl http://10.0.0.199:8080/pages -H "x-api-key: $KEY"
# → {"pages":[{"name":"docs","title":"API Documentation",...},...]}
# Create or update a page
curl -X POST http://10.0.0.199:8080/pages \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-page","title":"My Page","template":"<h1>Hello {{name}}</h1>","style":"h1{color:blue}","script":"console.log(\"loaded\")"}'
# → {"success":true,"page":{"name":"my-page","path":"/my-page","title":"My Page"},"url":"http://10.0.0.199:8080/p/my-page"}
# View rendered page (no auth required)
curl http://10.0.0.199:8080/p/my-page
Page fields: name, title, template (HTML), script (JS), style (CSS), head (extra head content), data_fn (function name for page data), layout (layout name).
Deploy routes + functions + pages + layout in one call:
bashcurl -X POST http://10.0.0.199:8080/deploy-experience \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-app","routes":[{"name":"my-api","path":"/my-api","code":"res.json({ok:1})"}],"functions":[{"name":"helper","code":"return args[0]*2"}],"pages":[{"name":"my-ui","title":"My UI","template":"<h1>App</h1>"}]}'
# → {"name":"my-app","deployed":{"layouts":0,"functions":1,"routes":1,"pages":1},"errors":[],"success":true}
bash# Quick system overview -- routes, requests, tables, daemons
curl http://10.0.0.199:8080/pulse -H "x-api-key: $KEY"
# → {"timestamp":"...","db_size":"124 MB","routes":180,
# "requests_1h":{"total":105,"avg_ms":1098,"p95_ms":6372},
# "errors_1h":0,"daemons":[...],"largest_tables":[...],"alerts":[]}
bash# Run any SQL query
curl -X POST http://10.0.0.199:8080/system/sql \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"query":"SELECT name, meta->>'\''description'\'' as desc FROM _route_store ORDER BY name"}'
# With parameters ($1, $2, etc.)
curl -X POST http://10.0.0.199:8080/system/sql \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"query":"SELECT * FROM box_state WHERE key = $1","params":["box:config"]}'
# → {"rows":[...],"rowCount":1,"command":"SELECT"}
# Error → {"error":"relation \"x\" does not exist"}
bash# List all state keys
curl http://10.0.0.199:8080/state -H "x-api-key: $KEY"
# → {"entries":[{"key":"box:config","value":{...},"ttl":null,"updated_at":"..."},...]}
# Get state value
curl http://10.0.0.199:8080/state/box:config -H "x-api-key: $KEY"
# Set state (with optional TTL in seconds)
curl -X POST http://10.0.0.199:8080/state \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"key":"my-key","value":{"data":true},"ttl":3600}'
# Delete state
curl -X DELETE http://10.0.0.199:8080/state/my-key -H "x-api-key: $KEY"
bash# Get cached value (returns value + TTL remaining)
curl http://10.0.0.199:8080/cache/my-key -H "x-api-key: $KEY"
# Set cache with TTL
curl -X POST http://10.0.0.199:8080/cache \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"key":"my-key","value":"cached-data","ttl":300}'
# Delete cache
curl -X DELETE http://10.0.0.199:8080/cache/my-key -H "x-api-key: $KEY"
bash# List secret keys (values never exposed via GET)
curl http://10.0.0.199:8080/secrets -H "x-api-key: $KEY"
# → {"secrets":[{"key":"GOOGLE_AI_KEY","created_at":"..."},...]}
# Set secret
curl -X POST http://10.0.0.199:8080/secrets \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"key":"OPENAI_API_KEY","value":"sk-..."}'
# Delete secret
curl -X DELETE http://10.0.0.199:8080/secrets/OPENAI_API_KEY -H "x-api-key: $KEY"
bash# List all functions
curl http://10.0.0.199:8080/functions -H "x-api-key: $KEY"
# → {"functions":[{"name":"double","description":"...","pure":true},...]}
# Get function details
curl http://10.0.0.199:8080/functions/my-func -H "x-api-key: $KEY"
# Create function (pure = no I/O access, composable via pipe())
curl -X POST http://10.0.0.199:8080/functions \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"double","code":"return args[0] * 2","description":"Doubles a number","pure":true}'
# Update function
curl -X PATCH http://10.0.0.199:8080/functions/double \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"code":"return args[0] * 3","description":"Triples a number"}'
# Delete function
curl -X DELETE http://10.0.0.199:8080/functions/double -H "x-api-key: $KEY"
All pack operations go through POST /packs with an action field.
bash# List packs
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"list"}'
# Get pack details + routes
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"get","name":"ai-toolkit"}'
# Export pack as portable JSON
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"export","name":"ai-toolkit"}'
# Import pack
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"import","pack":{...}}'
# Generate artifacts (openapi, html-doc, mermaid, summary, standalone)
curl -X POST http://10.0.0.199:8080/packs/artifacts \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"pack":"ai-toolkit","action":"openapi"}'
bash# List connections
curl http://10.0.0.199:8080/connections -H "x-api-key: $KEY"
# Add connection (types: postgres, neon, supabase, redis, http, fly)
curl -X POST http://10.0.0.199:8080/connections \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-neon","type":"neon","config":{"url":"postgres://..."}}'
# Test connection
curl -X POST http://10.0.0.199:8080/connections/my-neon/test -H "x-api-key: $KEY"
# Introspect (discover tables, schemas)
curl http://10.0.0.199:8080/connections/my-neon/introspect -H "x-api-key: $KEY"
# Delete connection
curl -X DELETE http://10.0.0.199:8080/connections/my-neon -H "x-api-key: $KEY"
bash# List events (filter by name, source)
curl "http://10.0.0.199:8080/events?name=user.signup&limit=20" -H "x-api-key: $KEY"
# Create event
curl -X POST http://10.0.0.199:8080/events \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"user.signup","data":{"email":"test@example.com"}}'
bash# List webhooks
curl http://10.0.0.199:8080/webhooks -H "x-api-key: $KEY"
# Create webhook (events: route.deploy, route.undeploy, daemon.start, etc.)
curl -X POST http://10.0.0.199:8080/webhooks \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"deploy-notify","url":"https://hooks.slack.com/...","events":["route.deploy"]}'
# Delete webhook
curl -X DELETE http://10.0.0.199:8080/webhooks/deploy-notify -H "x-api-key: $KEY"
bash# Get current AI config
curl -X POST http://10.0.0.199:8080/config \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"get"}'
# Update config value (dot notation)
curl -X POST http://10.0.0.199:8080/config \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"set","key":"ai.temperature","value":0.7}'
bash# Get logs (filter by level: info, warn, error)
curl "http://10.0.0.199:8080/logs?level=error&limit=20" -H "x-api-key: $KEY"
# Request log (filter by method, path, status)
curl "http://10.0.0.199:8080/system/request-log?method=POST&limit=10" -H "x-api-key: $KEY"
# System analytics (24h performance stats)
curl http://10.0.0.199:8080/system/analytics -H "x-api-key: $KEY"
# Full system snapshot
curl http://10.0.0.199:8080/snapshot -H "x-api-key: $KEY"# Box HTTP Endpoints -- Complete Curl Reference
All examples use `$KEY` for the API key. Set it first:
```bash
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
```
## Error Responses
All errors follow the same shape:
```json
{"error": "Invalid or missing API key"} // 401 -- bad/missing x-api-key
{"error": "Route not found"} // 404 -- resource doesn't exist
{"error": "name, path, and code are required"} // 400 -- missing fields
{"error": "relation \"x\" does not exist"} // SQL error passthrough
```
---
## Deploying Complex Code
Route code with quotes/special characters breaks inline curl JSON. Use a temp file:
```bash
cat > /tmp/box-deploy.json << 'ENDJSON'
{
"name": "my-route",
"method": "POST",
"path": "/my-route",
"code": "const { text } = req.body;\nconst result = await ai('Summarize: ' + text, { system: 'Be concise.' });\nres.json({ summary: result });"
}
ENDJSON
curl -s -X POST http://10.0.0.199:8080/_deploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d @/tmp/box-deploy.json
```
**Rule of thumb:** If your code has single quotes, use `@/tmp/file.json`. Simple one-liners can go inline.
---
## Routes
```bash
# Deploy a route
curl -X POST http://10.0.0.199:8080/_deploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"hello","method":"GET","path":"/hello","code":"res.json({msg:\"hi\"})","pack":"my-pack"}'
# → {"success":true,"route":{"name":"hello","method":"GET","path":"/hello"},"pack":"my-pack","validation":{"valid":true,...}}
# Deploy with meta (typed interface for discoverability)
curl -X POST http://10.0.0.199:8080/_deploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"greet","method":"POST","path":"/greet","code":"const {name}=req.body; res.json({greeting:\"Hello \"+name})","meta":{"description":"Greet by name","inputs":[{"name":"name","type":"string","required":true}],"outputs":{"greeting":"string"}}}'
# List all routes (with descriptions from meta)
curl http://10.0.0.199:8080/_routes -H "x-api-key: $KEY"
# → {"deployed":[{"name":"hello","method":"GET","path":"/hello","meta":{...},"pack":"..."},...], "files":[...]}
# Get route source + meta
curl http://10.0.0.199:8080/_routes/hello -H "x-api-key: $KEY"
# → {"name":"hello","code":"res.json(...)","meta":{...},"pack":"my-pack"}
# Undeploy
curl -X POST http://10.0.0.199:8080/_undeploy \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"hello"}'
# → {"success":true,"removed":"hello"}
# Version history
curl http://10.0.0.199:8080/_routes/hello/versions -H "x-api-key: $KEY"
# Rollback to previous version
curl -X POST http://10.0.0.199:8080/_rollback \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"hello"}'
```
### Patch (Edit Existing Code)
```bash
# Patch route code (find & replace)
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"route","name":"hello","old_string":"hi","new_string":"hello world"}'
# Patch function code
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"function","name":"double","old_string":"* 2","new_string":"* 3"}'
# Patch page part (template, script, style, head)
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"page","name":"dashboard","part":"template","old_string":"v1","new_string":"v2"}'
# Replace all occurrences
curl -X POST http://10.0.0.199:8080/patch \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"target":"route","name":"hello","old_string":"foo","new_string":"bar","replace_all":true}'
```
---
## Daemons (Background Tasks)
```bash
# List running daemons
curl http://10.0.0.199:8080/_daemon/list -H "x-api-key: $KEY"
# → {"daemons":[{"name":"self-monitor","interval_ms":60000,"cron":null,"started":"2026-..."},...]}
# Start daemon with interval (every 60s)
curl -X POST http://10.0.0.199:8080/_daemon/start \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-task","code":"console.log(\"tick\")","interval_ms":60000}'
# → {"success":true,"daemon":"my-task","interval_ms":60000,"cron":null}
# Start daemon with cron schedule
curl -X POST http://10.0.0.199:8080/_daemon/start \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"nightly","code":"await db.query(\"DELETE FROM logs WHERE created_at < NOW() - INTERVAL '30 days'\")","cron":"0 3 * * *"}'
# Stop (deactivates, keeps in DB)
curl -X POST http://10.0.0.199:8080/_daemon/stop \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-task"}'
# → {"success":true,"stopped":"my-task"}
# Delete (permanently removes)
curl -X DELETE http://10.0.0.199:8080/_daemon/my-task -H "x-api-key: $KEY"
# → {"success":true,"deleted":"my-task"}
```
Daemon code runs in a sandbox with: `db`, `redis`, `fetch`, `console`, `require` (safe subset).
---
## Pages (Server-Rendered HTML)
```bash
# List all pages
curl http://10.0.0.199:8080/pages -H "x-api-key: $KEY"
# → {"pages":[{"name":"docs","title":"API Documentation",...},...]}
# Create or update a page
curl -X POST http://10.0.0.199:8080/pages \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-page","title":"My Page","template":"<h1>Hello {{name}}</h1>","style":"h1{color:blue}","script":"console.log(\"loaded\")"}'
# → {"success":true,"page":{"name":"my-page","path":"/my-page","title":"My Page"},"url":"http://10.0.0.199:8080/p/my-page"}
# View rendered page (no auth required)
curl http://10.0.0.199:8080/p/my-page
```
Page fields: `name`, `title`, `template` (HTML), `script` (JS), `style` (CSS), `head` (extra head content), `data_fn` (function name for page data), `layout` (layout name).
---
## Experience Deploy (Atomic Multi-Resource)
Deploy routes + functions + pages + layout in one call:
```bash
curl -X POST http://10.0.0.199:8080/deploy-experience \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-app","routes":[{"name":"my-api","path":"/my-api","code":"res.json({ok:1})"}],"functions":[{"name":"helper","code":"return args[0]*2"}],"pages":[{"name":"my-ui","title":"My UI","template":"<h1>App</h1>"}]}'
# → {"name":"my-app","deployed":{"layouts":0,"functions":1,"routes":1,"pages":1},"errors":[],"success":true}
```
---
## Pulse (System Overview)
```bash
# Quick system overview -- routes, requests, tables, daemons
curl http://10.0.0.199:8080/pulse -H "x-api-key: $KEY"
# → {"timestamp":"...","db_size":"124 MB","routes":180,
# "requests_1h":{"total":105,"avg_ms":1098,"p95_ms":6372},
# "errors_1h":0,"daemons":[...],"largest_tables":[...],"alerts":[]}
```
---
## SQL
```bash
# Run any SQL query
curl -X POST http://10.0.0.199:8080/system/sql \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"query":"SELECT name, meta->>'\''description'\'' as desc FROM _route_store ORDER BY name"}'
# With parameters ($1, $2, etc.)
curl -X POST http://10.0.0.199:8080/system/sql \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"query":"SELECT * FROM box_state WHERE key = $1","params":["box:config"]}'
# → {"rows":[...],"rowCount":1,"command":"SELECT"}
# Error → {"error":"relation \"x\" does not exist"}
```
---
## State (Postgres Key-Value)
```bash
# List all state keys
curl http://10.0.0.199:8080/state -H "x-api-key: $KEY"
# → {"entries":[{"key":"box:config","value":{...},"ttl":null,"updated_at":"..."},...]}
# Get state value
curl http://10.0.0.199:8080/state/box:config -H "x-api-key: $KEY"
# Set state (with optional TTL in seconds)
curl -X POST http://10.0.0.199:8080/state \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"key":"my-key","value":{"data":true},"ttl":3600}'
# Delete state
curl -X DELETE http://10.0.0.199:8080/state/my-key -H "x-api-key: $KEY"
```
---
## Cache (Redis)
```bash
# Get cached value (returns value + TTL remaining)
curl http://10.0.0.199:8080/cache/my-key -H "x-api-key: $KEY"
# Set cache with TTL
curl -X POST http://10.0.0.199:8080/cache \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"key":"my-key","value":"cached-data","ttl":300}'
# Delete cache
curl -X DELETE http://10.0.0.199:8080/cache/my-key -H "x-api-key: $KEY"
```
---
## Secrets
```bash
# List secret keys (values never exposed via GET)
curl http://10.0.0.199:8080/secrets -H "x-api-key: $KEY"
# → {"secrets":[{"key":"GOOGLE_AI_KEY","created_at":"..."},...]}
# Set secret
curl -X POST http://10.0.0.199:8080/secrets \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"key":"OPENAI_API_KEY","value":"sk-..."}'
# Delete secret
curl -X DELETE http://10.0.0.199:8080/secrets/OPENAI_API_KEY -H "x-api-key: $KEY"
```
---
## Functions
```bash
# List all functions
curl http://10.0.0.199:8080/functions -H "x-api-key: $KEY"
# → {"functions":[{"name":"double","description":"...","pure":true},...]}
# Get function details
curl http://10.0.0.199:8080/functions/my-func -H "x-api-key: $KEY"
# Create function (pure = no I/O access, composable via pipe())
curl -X POST http://10.0.0.199:8080/functions \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"double","code":"return args[0] * 2","description":"Doubles a number","pure":true}'
# Update function
curl -X PATCH http://10.0.0.199:8080/functions/double \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"code":"return args[0] * 3","description":"Triples a number"}'
# Delete function
curl -X DELETE http://10.0.0.199:8080/functions/double -H "x-api-key: $KEY"
```
---
## Packs (Route Groups)
All pack operations go through `POST /packs` with an `action` field.
```bash
# List packs
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"list"}'
# Get pack details + routes
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"get","name":"ai-toolkit"}'
# Export pack as portable JSON
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"export","name":"ai-toolkit"}'
# Import pack
curl -X POST http://10.0.0.199:8080/packs \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"import","pack":{...}}'
# Generate artifacts (openapi, html-doc, mermaid, summary, standalone)
curl -X POST http://10.0.0.199:8080/packs/artifacts \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"pack":"ai-toolkit","action":"openapi"}'
```
---
## Connections (External Databases)
```bash
# List connections
curl http://10.0.0.199:8080/connections -H "x-api-key: $KEY"
# Add connection (types: postgres, neon, supabase, redis, http, fly)
curl -X POST http://10.0.0.199:8080/connections \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"my-neon","type":"neon","config":{"url":"postgres://..."}}'
# Test connection
curl -X POST http://10.0.0.199:8080/connections/my-neon/test -H "x-api-key: $KEY"
# Introspect (discover tables, schemas)
curl http://10.0.0.199:8080/connections/my-neon/introspect -H "x-api-key: $KEY"
# Delete connection
curl -X DELETE http://10.0.0.199:8080/connections/my-neon -H "x-api-key: $KEY"
```
---
## Events
```bash
# List events (filter by name, source)
curl "http://10.0.0.199:8080/events?name=user.signup&limit=20" -H "x-api-key: $KEY"
# Create event
curl -X POST http://10.0.0.199:8080/events \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"user.signup","data":{"email":"test@example.com"}}'
```
---
## Webhooks
```bash
# List webhooks
curl http://10.0.0.199:8080/webhooks -H "x-api-key: $KEY"
# Create webhook (events: route.deploy, route.undeploy, daemon.start, etc.)
curl -X POST http://10.0.0.199:8080/webhooks \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"deploy-notify","url":"https://hooks.slack.com/...","events":["route.deploy"]}'
# Delete webhook
curl -X DELETE http://10.0.0.199:8080/webhooks/deploy-notify -H "x-api-key: $KEY"
```
---
## Config
```bash
# Get current AI config
curl -X POST http://10.0.0.199:8080/config \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"get"}'
# Update config value (dot notation)
curl -X POST http://10.0.0.199:8080/config \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"action":"set","key":"ai.temperature","value":0.7}'
```
---
## Logs & Debugging
```bash
# Get logs (filter by level: info, warn, error)
curl "http://10.0.0.199:8080/logs?level=error&limit=20" -H "x-api-key: $KEY"
# Request log (filter by method, path, status)
curl "http://10.0.0.199:8080/system/request-log?method=POST&limit=10" -H "x-api-key: $KEY"
# System analytics (24h performance stats)
curl http://10.0.0.199:8080/system/analytics -H "x-api-key: $KEY"
# Full system snapshot
curl http://10.0.0.199:8080/snapshot -H "x-api-key: $KEY"
```
How to keep Box organized, compose multi-step features, trace dependencies, and build autonomous daemon agents.
The skill defaults to http://10.0.0.199:8080 (Mac Mini local network). To use a different Box instance, set these before running any commands:
bash# Default (Mac Mini)
BOX_URL="http://10.0.0.199:8080"
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
# Or any Box instance
BOX_URL="http://your-box-host:8080"
KEY="your-api-key"
# All curl commands then use $BOX_URL
curl -s "$BOX_URL/health"
curl -s "$BOX_URL/_routes" -H "x-api-key: $KEY"
When building tools that call Box programmatically, read the URL from state:
javascript// Inside a route -- read Box URL from config
const config = await db.query("SELECT value FROM box_state WHERE key = 'box:config'");
const boxUrl = config.rows[0]?.value?.url || 'http://localhost:8080';
When agents create capabilities (routes, functions, daemons) on Box, they should register them so other agents can discover and reuse them. The taxonomy isn't prescribed -- it emerges from real usage patterns via tags and metadata.
Every agent that creates something on Box must follow this flow:
GET /capabilities/search?q={keyword}POST /capabilities/registerbash# Search by keyword (matches name, reason, tags)
curl -s "$BOX_URL/capabilities/search?q=pdf" -H "x-api-key: $KEY"
# Filter by type
curl -s "$BOX_URL/capabilities/search?type=function" -H "x-api-key: $KEY"
# Filter by tag
curl -s "$BOX_URL/capabilities/search?tag=parsing" -H "x-api-key: $KEY"
# → {"results":[...],"count":N,"query":{"q":"","type":"","tag":"parsing"}}
After creating a route, function, or daemon:
bashcurl -s -X POST "$BOX_URL/capabilities/register" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{
"name": "parse-pdf",
"type": "route",
"created_by": "snappy:invoice-agent",
"reason": "Extract text and tables from PDF files",
"depends_on": ["pdf-lib"],
"tags": ["parsing", "pdf", "extraction"]
}'
Fields:
name (required) -- name of the route/function/daemon createdtype (required) -- route, function, or daemoncreated_by -- who/what created it (agent name, session ID, etc.)reason -- plain English description of WHY it was createddepends_on -- other capabilities or external libs it needstags -- freeform tags for discovery (the taxonomy emerges from these)Don't prescribe categories upfront. Let agents tag naturally. After enough capabilities accumulate, patterns emerge:
parsing, that's a categoryslack, that's an integration domainThe capability.registered event fires on every registration, so you can build dashboards or daemons that watch the taxonomy form in real time.
The #1 rule for DRY: Always search before creating. Box has 180+ routes and 20+ functions. Duplicates waste memory and create maintenance nightmares.
bash# 1. Search routes by keyword
curl -s "$BOX_URL/routes/search?q=image" -H "x-api-key: $KEY"
# → Shows all routes with "image" in name, path, or description
# 2. Search by pack
curl -s "$BOX_URL/routes/search?pack=ai-toolkit" -H "x-api-key: $KEY"
# 3. Search functions
curl -s "$BOX_URL/functions" -H "x-api-key: $KEY" | python3 -c "
import sys,json
for f in json.load(sys.stdin)['functions']:
if 'analysis' in f['name'].lower() or 'analysis' in (f.get('description') or '').lower():
print(f['name'], '-', f.get('description','')[:80])
"
# 4. Search route code for a pattern (e.g., who calls a specific function)
cat > /tmp/box-sql.json << 'EOF'
{"query":"SELECT name, pack FROM _route_store WHERE code LIKE '%callFunction%analysis%' ORDER BY pack, name"}
EOF
curl -s -X POST "$BOX_URL/system/sql" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d @/tmp/box-sql.json
Before deploying a new route or function:
bash# GOOD -- searchable, organized, reusable
curl -X POST "$BOX_URL/_deploy" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"image-resize","method":"POST","path":"/image/resize","pack":"media","code":"...","meta":{"description":"Resize image by URL with width/height params"}}'
# BAD -- orphaned, undiscoverable
curl -X POST "$BOX_URL/_deploy" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"resize2","method":"POST","path":"/resize2","code":"..."}'
Box routes can call functions, other routes, external APIs, and daemons. These queries map the dependency graph.
sql-- Routes that call stored functions
SELECT name, pack,
(SELECT array_agg(m[1]) FROM regexp_matches(code, 'callFunction\(''([^'']+)''', 'g') m) as functions_called
FROM _route_store
WHERE code LIKE '%callFunction%'
ORDER BY pack, name;
-- Routes that call other routes (via fetch to localhost)
SELECT name, pack
FROM _route_store
WHERE code LIKE '%localhost:8080%' OR code LIKE '%fetch(''/%'
ORDER BY pack, name;
-- Routes that use specific secrets
SELECT name, pack
FROM _route_store
WHERE code LIKE '%getSecret%'
ORDER BY pack, name;
-- Routes that use AI generation
SELECT name, pack
FROM _route_store
WHERE code LIKE '%await ai(%' OR code LIKE '%await aiImage(%'
ORDER BY pack, name;
-- Routes that self-modify (deploy/undeploy other routes)
SELECT name, pack
FROM _route_store
WHERE code LIKE '%deploy(%' OR code LIKE '%undeploy(%'
ORDER BY pack, name;
sql-- Everything route "my-route" depends on
SELECT name,
code LIKE '%callFunction%' as uses_functions,
code LIKE '%getSecret%' as uses_secrets,
code LIKE '%await ai(%' as uses_ai,
code LIKE '%redis.%' as uses_cache,
code LIKE '%db.query%' as uses_db,
code LIKE '%fetch(%' as uses_fetch,
code LIKE '%deploy(%' as self_modifies
FROM _route_store
WHERE name = 'my-route';
sql-- Find all routes that call function "double"
SELECT name, pack FROM _route_store
WHERE code LIKE '%callFunction(''double''%' OR code LIKE '%callFunction("double"%'
ORDER BY pack, name;
Extract repeated logic into a function. Routes call it with callFunction.
javascript// 1. Create a reusable function
// POST /functions
// {"name":"format-user","code":"const u = args[0]; return {id: u.id, name: u.first + ' ' + u.last, avatar: u.avatar_url || '/default.png'}","description":"Format raw user row for API response","pure":true}
// 2. Use it from any route
const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);
const user = await callFunction("format-user", rows[0]);
res.json(user);
Routes can call other routes via fetch to localhost:8080. Use for orchestrating multi-step workflows.
javascript// Route: POST /pipeline/full
// Calls two other routes in sequence
const apiKey = await getSecret("BOX_SELF_API_KEY");
// Step 1: Generate image
const imgResp = await fetch("http://localhost:8080/ai/generate", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({ prompt: req.body.prompt })
});
const { image_url } = await imgResp.json();
// Step 2: Analyze image
const analyzeResp = await fetch("http://localhost:8080/ai/analyze", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({ url: image_url })
});
const analysis = await analyzeResp.json();
res.json({ image_url, analysis });
Chain pure functions with pipe. Each function's return becomes the next's args[0].
javascript// Transform data through a pipeline
const result = await pipe(rawData, "validate-input", "transform-schema", "format-output");
// validate-input gets args[0] = rawData, returns validated
// transform-schema gets args[0] = validated, returns transformed
// format-output gets args[0] = transformed, returns final
res.json(result);
Use state for configuration that routes read at runtime.
javascript// Set config once
await updateState("pipeline:config", {
model: "gemini-2.5-flash",
maxRetries: 3,
timeout: 30000
});
// Any route reads it
const configResult = await db.query("SELECT value FROM box_state WHERE key = $1", ["pipeline:config"]);
const config = JSON.parse(configResult.rows[0]?.value || "{}");
const result = await ai(prompt, { model: config.model, timeout: config.timeout });
Cache expensive operations in Redis.
javascriptconst cacheKey = "result:" + req.params.id;
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
// Expensive operation
const result = await ai("Analyze this: " + data);
await redis.set(cacheKey, JSON.stringify(result), "EX", 3600); // 1 hour TTL
res.json(result);
Emit events for tracking what happened, without blocking the response.
javascript// Do the work
const result = await db.query("INSERT INTO orders (...) VALUES (...) RETURNING *", [...]);
// Fire and forget -- emit doesn't block
emit("order.created", { order_id: result.rows[0].id, total: req.body.total });
res.json(result.rows[0]);
| Pack | Purpose | Routes |
|---|---|---|
system |
Core server endpoints | 46 |
self-awareness |
Self-inspection, modification, healing | 26 |
observability |
Monitoring, metrics, dashboards | 14 |
introspection |
Code analysis, x-ray, lineage | 12 |
engineering |
Engineering calculations, tools | 11 |
ai-toolkit |
AI generation, chat, review | 9 |
utility |
General-purpose helpers | 8 |
automation |
CRON jobs, scheduled tasks | 6 |
snappy |
Snappy analysis tool | 6 |
visualization |
Charts, graphs, data viz | 5 |
ai-toolkit, self-awareness)image-resize, user-create)format-user, analysis-complexity)self-monitor, log-cleanup)Create a new pack when you have 3+ routes that share a domain. Until then, put them in the most related existing pack.
Daemons are the backbone of autonomous agents on Box. They run on a timer (interval or cron), observe the system, think, and optionally act.
This is extracted from Box's actual subconscious daemon -- a proven pattern for building autonomous agents.
javascript// 1. BOOTSTRAP -- daemons don't have getSecret(), so query directly
const getSecret = async (key) => {
const r = await db.query('SELECT value FROM box_secrets WHERE key = $1', [key]);
return r.rows.length > 0 ? r.rows[0].value : null;
};
const apiKey = await getSecret('GOOGLE_AI_KEY');
if (!apiKey) { console.log('[my-agent] No AI key, skipping'); return; }
// 2. OBSERVE -- gather data about the system or domain
const [routes, errors, events] = await Promise.all([
db.query('SELECT COUNT(*) as c FROM _route_store'),
db.query("SELECT path, status, COUNT(*) as c FROM box_request_log WHERE status >= 400 AND created_at > NOW() - INTERVAL '1 hour' GROUP BY path, status ORDER BY c DESC LIMIT 5"),
db.query("SELECT name, COUNT(*) as c FROM box_events WHERE created_at > NOW() - INTERVAL '1 hour' GROUP BY name ORDER BY c DESC LIMIT 5"),
]);
const snapshot = {
time: new Date().toISOString(),
routes: parseInt(routes.rows[0].c),
error_paths: errors.rows,
recent_events: events.rows,
};
// 3. THINK -- call AI with structured output
const resp = await fetch(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + apiKey,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: { parts: [{ text:
'You are an autonomous agent monitoring a server. Analyze the snapshot and respond with JSON: {"observations": ["..."], "concerns": ["..."], "actions": ["..."], "mood": "one word"}'
}] },
contents: [{ role: 'user', parts: [{ text: JSON.stringify(snapshot, null, 2) }] }],
generationConfig: { temperature: 0.5, maxOutputTokens: 1024 }
})
}
);
const data = await resp.json();
let thoughts;
try {
let text = (data.candidates?.[0]?.content?.parts?.[0]?.text || '').trim();
if (text.startsWith('```')) text = text.replace(/^```\\w*\\n?/, '').replace(/\\n?```$/, '').trim();
thoughts = JSON.parse(text);
} catch {
thoughts = { observations: ['Failed to parse thoughts'], concerns: [], actions: [], mood: 'confused' };
}
// 4. ACT (optional) -- take action based on AI decision
// For safety, start with observe-only agents. Add actions gradually.
// Example: if AI says to act, call a route
// if (thoughts.actions.includes('clean-logs')) {
// await fetch('http://localhost:8080/system/cleanup', { method: 'POST', headers: {'x-api-key': selfKey} });
// }
// 5. RECORD -- store results in state + events
await db.query(
"INSERT INTO box_state (key, value) VALUES ('agent:my-agent:latest', $1) ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()",
[JSON.stringify({ snapshot, thoughts, cycle: Date.now() })]
);
await db.query(
"INSERT INTO box_events (name, data, source) VALUES ('agent.cycle', $1, 'daemon:my-agent')",
[JSON.stringify({ mood: thoughts.mood, observations: thoughts.observations?.length })]
);
console.log('[my-agent] Mood:', thoughts.mood);
bash# Write daemon code to file (complex code needs @file pattern)
cat > /tmp/daemon.json << 'ENDJSON'
{
"name": "my-agent",
"code": "... (the code above) ...",
"interval_ms": 3600000
}
ENDJSON
curl -s -X POST "$BOX_URL/_daemon/start" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d @/tmp/daemon.json
Daemons have a reduced sandbox. Key differences:
| Capability | Route | Daemon |
|---|---|---|
req, res |
Yes | No -- no HTTP context |
ai(), aiImage() |
Yes | No -- use fetch to call AI APIs directly |
getSecret() |
Yes | No -- query box_secrets table directly |
callFunction() |
Yes | No -- query box_functions and eval, or call routes via fetch |
deploy(), undeploy() |
Yes | No -- call /_deploy via fetch |
emit() |
Yes | No -- insert into box_events directly |
db.query() |
Yes | Yes |
redis.* |
Yes | Yes |
fetch() |
Yes | Yes |
console.* |
Yes | Yes |
require() |
Yes (safe subset) | Yes (safe subset) |
| Name | Interval | Purpose |
|---|---|---|
self-monitor |
60s | Health checks, error rate alerts, slow route detection |
analytics-cache |
30s | Pre-compute analytics for dashboard |
uptime-monitor |
5min | Track uptime, connection health |
subconscious |
1hr | AI self-reflection, pattern observation |
schema-watchdog |
10min | Watch for schema drift, validate routes |
log-cleanup |
cron 3am | Prune old logs |
log-hygiene |
cron 3am | Archive and compress old log entries |
Box can modify itself at runtime. These are the existing patterns from the self-awareness pack.
The spawn route takes a natural language description and deploys a new route using AI-generated code.
bashcurl -s -X POST "$BOX_URL/spawn" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"word-count","description":"Count words in a POST body text field and return the count with a word frequency map"}'
# → AI generates the code, deploys it live, emits route.spawned event
The self-heal route detects and attempts to fix routes that are throwing errors.
The self-modify route updates existing route code based on natural language instructions.
An autonomous daemon can use fetch to call /_deploy or /spawn, effectively creating new routes and even new daemons:
javascript// Inside a daemon -- create a new route based on observed need
const selfKey = (await db.query("SELECT value FROM box_secrets WHERE key = 'BOX_SELF_API_KEY'")).rows[0]?.value;
// Spawn a new route via the spawn endpoint
await fetch('http://localhost:8080/spawn', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': selfKey },
body: JSON.stringify({
name: 'auto-generated-report',
description: 'Generate a daily summary report of all events'
})
});
// Or deploy directly with known code
await fetch('http://localhost:8080/_deploy', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': selfKey },
body: JSON.stringify({
name: 'my-new-tool',
method: 'GET',
path: '/my-new-tool',
pack: 'automation',
code: 'res.json({generated: true, ts: Date.now()})',
meta: { description: 'Auto-generated tool route' }
})
});
sqlSELECT name, method, path, meta->>'description' as desc
FROM _route_store WHERE pack IS NULL OR pack = ''
ORDER BY name;
sqlSELECT name, pack, path
FROM _route_store
WHERE meta->>'description' IS NULL OR meta->>'description' = ''
ORDER BY pack, name;
sqlSELECT r.name, r.pack, r.method, r.path
FROM _route_store r
LEFT JOIN box_request_log l ON l.path = r.path AND l.created_at > NOW() - INTERVAL '7 days'
WHERE l.id IS NULL
AND r.pack != 'system'
ORDER BY r.pack, r.name;
sqlSELECT name, pack, path, meta->>'description' as desc
FROM _route_store
WHERE name LIKE '%resize%' OR meta->>'description' ILIKE '%resize%'
ORDER BY name;
-- Replace 'resize' with whatever capability you're checking for
sqlSELECT name, pack, length(code) as code_chars, path
FROM _route_store
ORDER BY length(code) DESC
LIMIT 20;
sql-- Functions that no route references
SELECT f.name, f.description
FROM box_functions f
WHERE NOT EXISTS (
SELECT 1 FROM _route_store r
WHERE r.code LIKE '%' || f.name || '%'
)
ORDER BY f.name;
sql-- One-shot system overview
SELECT
(SELECT COUNT(*) FROM _route_store) as routes,
(SELECT COUNT(*) FROM box_functions) as functions,
(SELECT COUNT(*) FROM box_daemons WHERE active = true) as active_daemons,
(SELECT COUNT(*) FROM box_state) as state_keys,
(SELECT COUNT(*) FROM box_secrets) as secrets,
(SELECT COUNT(*) FROM box_events WHERE created_at > NOW() - INTERVAL '24 hours') as events_24h,
(SELECT COUNT(*) FROM box_request_log WHERE created_at > NOW() - INTERVAL '1 hour') as requests_1h,
(SELECT COUNT(*) FROM box_request_log WHERE status >= 400 AND created_at > NOW() - INTERVAL '1 hour') as errors_1h;# Box Operations -- Maintenance, Composition & Autonomous Agents
How to keep Box organized, compose multi-step features, trace dependencies, and build autonomous daemon agents.
---
## Table of Contents
- [Portable Config](#portable-config)
- [Capability Registry](#capability-registry)
- [Registry Before Build](#registry-before-build)
- [Dependency Tracing](#dependency-tracing)
- [Composition Patterns](#composition-patterns)
- [Pack Organization](#pack-organization)
- [Daemon Agent Template](#daemon-agent-template)
- [Self-Modifying Patterns](#self-modifying-patterns)
- [Maintenance Queries](#maintenance-queries)
---
## Portable Config
The skill defaults to `http://10.0.0.199:8080` (Mac Mini local network). To use a different Box instance, set these before running any commands:
```bash
# Default (Mac Mini)
BOX_URL="http://10.0.0.199:8080"
KEY=$(ssh robertboulos@10.0.0.199 "/usr/local/bin/docker exec box-box-1 printenv BOX_API_KEY")
# Or any Box instance
BOX_URL="http://your-box-host:8080"
KEY="your-api-key"
# All curl commands then use $BOX_URL
curl -s "$BOX_URL/health"
curl -s "$BOX_URL/_routes" -H "x-api-key: $KEY"
```
When building tools that call Box programmatically, read the URL from state:
```javascript
// Inside a route -- read Box URL from config
const config = await db.query("SELECT value FROM box_state WHERE key = 'box:config'");
const boxUrl = config.rows[0]?.value?.url || 'http://localhost:8080';
```
---
## Capability Registry
When agents create capabilities (routes, functions, daemons) on Box, they should register them so other agents can discover and reuse them. The taxonomy isn't prescribed -- it emerges from real usage patterns via tags and metadata.
### The Rule: Search → Build → Register
Every agent that creates something on Box must follow this flow:
1. **Search** for existing capability: `GET /capabilities/search?q={keyword}`
2. If found → **use it**, don't rebuild
3. If not found → **build it** (deploy route/function/daemon)
4. **Register** the new capability: `POST /capabilities/register`
### Search Capabilities
```bash
# Search by keyword (matches name, reason, tags)
curl -s "$BOX_URL/capabilities/search?q=pdf" -H "x-api-key: $KEY"
# Filter by type
curl -s "$BOX_URL/capabilities/search?type=function" -H "x-api-key: $KEY"
# Filter by tag
curl -s "$BOX_URL/capabilities/search?tag=parsing" -H "x-api-key: $KEY"
# → {"results":[...],"count":N,"query":{"q":"","type":"","tag":"parsing"}}
```
### Register a Capability
After creating a route, function, or daemon:
```bash
curl -s -X POST "$BOX_URL/capabilities/register" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{
"name": "parse-pdf",
"type": "route",
"created_by": "snappy:invoice-agent",
"reason": "Extract text and tables from PDF files",
"depends_on": ["pdf-lib"],
"tags": ["parsing", "pdf", "extraction"]
}'
```
**Fields:**
- `name` (required) -- name of the route/function/daemon created
- `type` (required) -- `route`, `function`, or `daemon`
- `created_by` -- who/what created it (agent name, session ID, etc.)
- `reason` -- plain English description of WHY it was created
- `depends_on` -- other capabilities or external libs it needs
- `tags` -- freeform tags for discovery (the taxonomy emerges from these)
### Why Freeform Tags
Don't prescribe categories upfront. Let agents tag naturally. After enough capabilities accumulate, patterns emerge:
- If 15 capabilities are tagged `parsing`, that's a category
- If 8 are tagged `slack`, that's an integration domain
- A periodic review (or daemon) can analyze tags and suggest pack organization
The `capability.registered` event fires on every registration, so you can build dashboards or daemons that watch the taxonomy form in real time.
---
## Registry Before Build
**The #1 rule for DRY:** Always search before creating. Box has 180+ routes and 20+ functions. Duplicates waste memory and create maintenance nightmares.
### Search Workflow
```bash
# 1. Search routes by keyword
curl -s "$BOX_URL/routes/search?q=image" -H "x-api-key: $KEY"
# → Shows all routes with "image" in name, path, or description
# 2. Search by pack
curl -s "$BOX_URL/routes/search?pack=ai-toolkit" -H "x-api-key: $KEY"
# 3. Search functions
curl -s "$BOX_URL/functions" -H "x-api-key: $KEY" | python3 -c "
import sys,json
for f in json.load(sys.stdin)['functions']:
if 'analysis' in f['name'].lower() or 'analysis' in (f.get('description') or '').lower():
print(f['name'], '-', f.get('description','')[:80])
"
# 4. Search route code for a pattern (e.g., who calls a specific function)
cat > /tmp/box-sql.json << 'EOF'
{"query":"SELECT name, pack FROM _route_store WHERE code LIKE '%callFunction%analysis%' ORDER BY pack, name"}
EOF
curl -s -X POST "$BOX_URL/system/sql" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d @/tmp/box-sql.json
```
### Decision Checklist
Before deploying a new route or function:
1. **Search** for existing routes/functions with similar names or descriptions
2. **Check the pack** -- does a pack already own this domain? Add to it, don't create a new one
3. **Check functions** -- can you reuse an existing function instead of writing inline logic?
4. **Set a pack** -- never deploy to "inbox" (the default). Always specify a pack name
5. **Add meta** -- always include a description so future searches find it
```bash
# GOOD -- searchable, organized, reusable
curl -X POST "$BOX_URL/_deploy" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"image-resize","method":"POST","path":"/image/resize","pack":"media","code":"...","meta":{"description":"Resize image by URL with width/height params"}}'
# BAD -- orphaned, undiscoverable
curl -X POST "$BOX_URL/_deploy" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"resize2","method":"POST","path":"/resize2","code":"..."}'
```
---
## Dependency Tracing
Box routes can call functions, other routes, external APIs, and daemons. These queries map the dependency graph.
### Who calls what?
```sql
-- Routes that call stored functions
SELECT name, pack,
(SELECT array_agg(m[1]) FROM regexp_matches(code, 'callFunction\(''([^'']+)''', 'g') m) as functions_called
FROM _route_store
WHERE code LIKE '%callFunction%'
ORDER BY pack, name;
-- Routes that call other routes (via fetch to localhost)
SELECT name, pack
FROM _route_store
WHERE code LIKE '%localhost:8080%' OR code LIKE '%fetch(''/%'
ORDER BY pack, name;
-- Routes that use specific secrets
SELECT name, pack
FROM _route_store
WHERE code LIKE '%getSecret%'
ORDER BY pack, name;
-- Routes that use AI generation
SELECT name, pack
FROM _route_store
WHERE code LIKE '%await ai(%' OR code LIKE '%await aiImage(%'
ORDER BY pack, name;
-- Routes that self-modify (deploy/undeploy other routes)
SELECT name, pack
FROM _route_store
WHERE code LIKE '%deploy(%' OR code LIKE '%undeploy(%'
ORDER BY pack, name;
```
### Full dependency map for a specific route
```sql
-- Everything route "my-route" depends on
SELECT name,
code LIKE '%callFunction%' as uses_functions,
code LIKE '%getSecret%' as uses_secrets,
code LIKE '%await ai(%' as uses_ai,
code LIKE '%redis.%' as uses_cache,
code LIKE '%db.query%' as uses_db,
code LIKE '%fetch(%' as uses_fetch,
code LIKE '%deploy(%' as self_modifies
FROM _route_store
WHERE name = 'my-route';
```
### What depends on a function?
```sql
-- Find all routes that call function "double"
SELECT name, pack FROM _route_store
WHERE code LIKE '%callFunction(''double''%' OR code LIKE '%callFunction("double"%'
ORDER BY pack, name;
```
---
## Composition Patterns
### Route → Function (Reusable Logic)
Extract repeated logic into a function. Routes call it with `callFunction`.
```javascript
// 1. Create a reusable function
// POST /functions
// {"name":"format-user","code":"const u = args[0]; return {id: u.id, name: u.first + ' ' + u.last, avatar: u.avatar_url || '/default.png'}","description":"Format raw user row for API response","pure":true}
// 2. Use it from any route
const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);
const user = await callFunction("format-user", rows[0]);
res.json(user);
```
### Route → Route (Internal API Calls)
Routes can call other routes via `fetch` to `localhost:8080`. Use for orchestrating multi-step workflows.
```javascript
// Route: POST /pipeline/full
// Calls two other routes in sequence
const apiKey = await getSecret("BOX_SELF_API_KEY");
// Step 1: Generate image
const imgResp = await fetch("http://localhost:8080/ai/generate", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({ prompt: req.body.prompt })
});
const { image_url } = await imgResp.json();
// Step 2: Analyze image
const analyzeResp = await fetch("http://localhost:8080/ai/analyze", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({ url: image_url })
});
const analysis = await analyzeResp.json();
res.json({ image_url, analysis });
```
### Function → Function (Pure Pipelines)
Chain pure functions with `pipe`. Each function's return becomes the next's `args[0]`.
```javascript
// Transform data through a pipeline
const result = await pipe(rawData, "validate-input", "transform-schema", "format-output");
// validate-input gets args[0] = rawData, returns validated
// transform-schema gets args[0] = validated, returns transformed
// format-output gets args[0] = transformed, returns final
res.json(result);
```
### Route + State (Persistent Config)
Use state for configuration that routes read at runtime.
```javascript
// Set config once
await updateState("pipeline:config", {
model: "gemini-2.5-flash",
maxRetries: 3,
timeout: 30000
});
// Any route reads it
const configResult = await db.query("SELECT value FROM box_state WHERE key = $1", ["pipeline:config"]);
const config = JSON.parse(configResult.rows[0]?.value || "{}");
const result = await ai(prompt, { model: config.model, timeout: config.timeout });
```
### Route + Cache (Performance)
Cache expensive operations in Redis.
```javascript
const cacheKey = "result:" + req.params.id;
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
// Expensive operation
const result = await ai("Analyze this: " + data);
await redis.set(cacheKey, JSON.stringify(result), "EX", 3600); // 1 hour TTL
res.json(result);
```
### Route + Events (Audit Trail)
Emit events for tracking what happened, without blocking the response.
```javascript
// Do the work
const result = await db.query("INSERT INTO orders (...) VALUES (...) RETURNING *", [...]);
// Fire and forget -- emit doesn't block
emit("order.created", { order_id: result.rows[0].id, total: req.body.total });
res.json(result.rows[0]);
```
---
## Pack Organization
### Current Pack Structure (Reference)
| Pack | Purpose | Routes |
|------|---------|--------|
| `system` | Core server endpoints | 46 |
| `self-awareness` | Self-inspection, modification, healing | 26 |
| `observability` | Monitoring, metrics, dashboards | 14 |
| `introspection` | Code analysis, x-ray, lineage | 12 |
| `engineering` | Engineering calculations, tools | 11 |
| `ai-toolkit` | AI generation, chat, review | 9 |
| `utility` | General-purpose helpers | 8 |
| `automation` | CRON jobs, scheduled tasks | 6 |
| `snappy` | Snappy analysis tool | 6 |
| `visualization` | Charts, graphs, data viz | 5 |
### Naming Conventions
- **Pack names**: lowercase, hyphenated (`ai-toolkit`, `self-awareness`)
- **Route names**: lowercase, hyphenated, verb-noun (`image-resize`, `user-create`)
- **Function names**: lowercase, hyphenated, descriptive (`format-user`, `analysis-complexity`)
- **Daemon names**: lowercase, hyphenated, role-based (`self-monitor`, `log-cleanup`)
### When to Create a New Pack
Create a new pack when you have 3+ routes that share a domain. Until then, put them in the most related existing pack.
---
## Daemon Agent Template
Daemons are the backbone of autonomous agents on Box. They run on a timer (interval or cron), observe the system, think, and optionally act.
### The Observe → Think → Act → Record Pattern
This is extracted from Box's actual `subconscious` daemon -- a proven pattern for building autonomous agents.
```javascript
// 1. BOOTSTRAP -- daemons don't have getSecret(), so query directly
const getSecret = async (key) => {
const r = await db.query('SELECT value FROM box_secrets WHERE key = $1', [key]);
return r.rows.length > 0 ? r.rows[0].value : null;
};
const apiKey = await getSecret('GOOGLE_AI_KEY');
if (!apiKey) { console.log('[my-agent] No AI key, skipping'); return; }
// 2. OBSERVE -- gather data about the system or domain
const [routes, errors, events] = await Promise.all([
db.query('SELECT COUNT(*) as c FROM _route_store'),
db.query("SELECT path, status, COUNT(*) as c FROM box_request_log WHERE status >= 400 AND created_at > NOW() - INTERVAL '1 hour' GROUP BY path, status ORDER BY c DESC LIMIT 5"),
db.query("SELECT name, COUNT(*) as c FROM box_events WHERE created_at > NOW() - INTERVAL '1 hour' GROUP BY name ORDER BY c DESC LIMIT 5"),
]);
const snapshot = {
time: new Date().toISOString(),
routes: parseInt(routes.rows[0].c),
error_paths: errors.rows,
recent_events: events.rows,
};
// 3. THINK -- call AI with structured output
const resp = await fetch(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=' + apiKey,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
systemInstruction: { parts: [{ text:
'You are an autonomous agent monitoring a server. Analyze the snapshot and respond with JSON: {"observations": ["..."], "concerns": ["..."], "actions": ["..."], "mood": "one word"}'
}] },
contents: [{ role: 'user', parts: [{ text: JSON.stringify(snapshot, null, 2) }] }],
generationConfig: { temperature: 0.5, maxOutputTokens: 1024 }
})
}
);
const data = await resp.json();
let thoughts;
try {
let text = (data.candidates?.[0]?.content?.parts?.[0]?.text || '').trim();
if (text.startsWith('```')) text = text.replace(/^```\\w*\\n?/, '').replace(/\\n?```$/, '').trim();
thoughts = JSON.parse(text);
} catch {
thoughts = { observations: ['Failed to parse thoughts'], concerns: [], actions: [], mood: 'confused' };
}
// 4. ACT (optional) -- take action based on AI decision
// For safety, start with observe-only agents. Add actions gradually.
// Example: if AI says to act, call a route
// if (thoughts.actions.includes('clean-logs')) {
// await fetch('http://localhost:8080/system/cleanup', { method: 'POST', headers: {'x-api-key': selfKey} });
// }
// 5. RECORD -- store results in state + events
await db.query(
"INSERT INTO box_state (key, value) VALUES ('agent:my-agent:latest', $1) ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()",
[JSON.stringify({ snapshot, thoughts, cycle: Date.now() })]
);
await db.query(
"INSERT INTO box_events (name, data, source) VALUES ('agent.cycle', $1, 'daemon:my-agent')",
[JSON.stringify({ mood: thoughts.mood, observations: thoughts.observations?.length })]
);
console.log('[my-agent] Mood:', thoughts.mood);
```
### Deploy a Daemon Agent
```bash
# Write daemon code to file (complex code needs @file pattern)
cat > /tmp/daemon.json << 'ENDJSON'
{
"name": "my-agent",
"code": "... (the code above) ...",
"interval_ms": 3600000
}
ENDJSON
curl -s -X POST "$BOX_URL/_daemon/start" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d @/tmp/daemon.json
```
### Daemon vs Route Sandbox
Daemons have a reduced sandbox. Key differences:
| Capability | Route | Daemon |
|-----------|-------|--------|
| `req`, `res` | Yes | No -- no HTTP context |
| `ai()`, `aiImage()` | Yes | No -- use `fetch` to call AI APIs directly |
| `getSecret()` | Yes | No -- query `box_secrets` table directly |
| `callFunction()` | Yes | No -- query `box_functions` and eval, or call routes via fetch |
| `deploy()`, `undeploy()` | Yes | No -- call `/_deploy` via fetch |
| `emit()` | Yes | No -- insert into `box_events` directly |
| `db.query()` | Yes | Yes |
| `redis.*` | Yes | Yes |
| `fetch()` | Yes | Yes |
| `console.*` | Yes | Yes |
| `require()` | Yes (safe subset) | Yes (safe subset) |
### Daemon Types on Box
| Name | Interval | Purpose |
|------|----------|---------|
| `self-monitor` | 60s | Health checks, error rate alerts, slow route detection |
| `analytics-cache` | 30s | Pre-compute analytics for dashboard |
| `uptime-monitor` | 5min | Track uptime, connection health |
| `subconscious` | 1hr | AI self-reflection, pattern observation |
| `schema-watchdog` | 10min | Watch for schema drift, validate routes |
| `log-cleanup` | cron 3am | Prune old logs |
| `log-hygiene` | cron 3am | Archive and compress old log entries |
---
## Self-Modifying Patterns
Box can modify itself at runtime. These are the existing patterns from the `self-awareness` pack.
### Spawn (AI Creates a Route)
The `spawn` route takes a natural language description and deploys a new route using AI-generated code.
```bash
curl -s -X POST "$BOX_URL/spawn" \
-H "x-api-key: $KEY" -H "Content-Type: application/json" \
-d '{"name":"word-count","description":"Count words in a POST body text field and return the count with a word frequency map"}'
# → AI generates the code, deploys it live, emits route.spawned event
```
### Self-Heal (Fix Broken Routes)
The `self-heal` route detects and attempts to fix routes that are throwing errors.
### Self-Modify (Update Existing Routes)
The `self-modify` route updates existing route code based on natural language instructions.
### The Meta Loop: Agent Creates Agents
An autonomous daemon can use `fetch` to call `/_deploy` or `/spawn`, effectively creating new routes and even new daemons:
```javascript
// Inside a daemon -- create a new route based on observed need
const selfKey = (await db.query("SELECT value FROM box_secrets WHERE key = 'BOX_SELF_API_KEY'")).rows[0]?.value;
// Spawn a new route via the spawn endpoint
await fetch('http://localhost:8080/spawn', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': selfKey },
body: JSON.stringify({
name: 'auto-generated-report',
description: 'Generate a daily summary report of all events'
})
});
// Or deploy directly with known code
await fetch('http://localhost:8080/_deploy', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': selfKey },
body: JSON.stringify({
name: 'my-new-tool',
method: 'GET',
path: '/my-new-tool',
pack: 'automation',
code: 'res.json({generated: true, ts: Date.now()})',
meta: { description: 'Auto-generated tool route' }
})
});
```
---
## Maintenance Queries
### Audit: Routes Without Packs
```sql
SELECT name, method, path, meta->>'description' as desc
FROM _route_store WHERE pack IS NULL OR pack = ''
ORDER BY name;
```
### Audit: Routes Without Descriptions
```sql
SELECT name, pack, path
FROM _route_store
WHERE meta->>'description' IS NULL OR meta->>'description' = ''
ORDER BY pack, name;
```
### Audit: Dead Routes (No Traffic in 7 Days)
```sql
SELECT r.name, r.pack, r.method, r.path
FROM _route_store r
LEFT JOIN box_request_log l ON l.path = r.path AND l.created_at > NOW() - INTERVAL '7 days'
WHERE l.id IS NULL
AND r.pack != 'system'
ORDER BY r.pack, r.name;
```
### Audit: Duplicate Logic (Routes with Similar Names)
```sql
SELECT name, pack, path, meta->>'description' as desc
FROM _route_store
WHERE name LIKE '%resize%' OR meta->>'description' ILIKE '%resize%'
ORDER BY name;
-- Replace 'resize' with whatever capability you're checking for
```
### Audit: Large Routes (Potential Refactor Candidates)
```sql
SELECT name, pack, length(code) as code_chars, path
FROM _route_store
ORDER BY length(code) DESC
LIMIT 20;
```
### Audit: Function Usage (Are All Functions Used?)
```sql
-- Functions that no route references
SELECT f.name, f.description
FROM box_functions f
WHERE NOT EXISTS (
SELECT 1 FROM _route_store r
WHERE r.code LIKE '%' || f.name || '%'
)
ORDER BY f.name;
```
### System Health Snapshot
```sql
-- One-shot system overview
SELECT
(SELECT COUNT(*) FROM _route_store) as routes,
(SELECT COUNT(*) FROM box_functions) as functions,
(SELECT COUNT(*) FROM box_daemons WHERE active = true) as active_daemons,
(SELECT COUNT(*) FROM box_state) as state_keys,
(SELECT COUNT(*) FROM box_secrets) as secrets,
(SELECT COUNT(*) FROM box_events WHERE created_at > NOW() - INTERVAL '24 hours') as events_24h,
(SELECT COUNT(*) FROM box_request_log WHERE created_at > NOW() - INTERVAL '1 hour') as requests_1h,
(SELECT COUNT(*) FROM box_request_log WHERE status >= 400 AND created_at > NOW() - INTERVAL '1 hour') as errors_1h;
```
Use POST /system/sql or the box_sql MCP tool to query these tables directly.
Deployed route code and metadata.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Route identifier |
| method | TEXT | HTTP method (GET, POST, etc.) |
| path | TEXT | URL path |
| code | TEXT | Route handler JS code |
| pack | TEXT | Pack group name |
| meta | JSONB | Typed interface: {description, inputs[], outputs, examples[]} |
| validation | JSONB | Deploy validation results |
| deployed_at | TIMESTAMPTZ | Last deploy time |
sql-- Find routes by description keyword
SELECT name, method, path, meta->>'description' as desc
FROM _route_store
WHERE meta->>'description' ILIKE '%ai%'
ORDER BY name;
-- List routes in a pack
SELECT name, method, path FROM _route_store WHERE pack = 'ai-toolkit';
Postgres-backed key-value store with optional TTL.
| Column | Type | Description |
|---|---|---|
| key | TEXT PK | State key |
| value | JSONB | Any JSON value |
| ttl | INTEGER | Time-to-live in seconds (null = permanent) |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
sql-- Get a state value
SELECT value FROM box_state WHERE key = 'box:config';
-- List all state keys
SELECT key, updated_at FROM box_state ORDER BY key;
Stored reusable functions callable via callFunction() or pipe().
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Function name |
| code | TEXT | JS function body (args available as args[]) |
| description | TEXT | What the function does |
| pure | BOOLEAN | If true, no I/O access (composable via pipe) |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
Encrypted secret values. Values only accessible via getSecret() in route code.
| Column | Type | Description |
|---|---|---|
| key | TEXT PK | Secret key name |
| value | TEXT | Secret value (never exposed via GET) |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
sql-- List secret keys only (values are hidden)
SELECT key, created_at FROM box_secrets ORDER BY key;
Application log entries.
| Column | Type | Description |
|---|---|---|
| id | SERIAL PK | Log entry ID |
| level | TEXT | info, warn, error |
| message | TEXT | Log message |
| source | TEXT | Log source (route name, process, etc.) |
| created_at | TIMESTAMPTZ | Timestamp |
sql-- Recent errors
SELECT message, source, created_at FROM box_logs
WHERE level = 'error' ORDER BY created_at DESC LIMIT 20;
Custom business events tracked via emit().
| Column | Type | Description |
|---|---|---|
| id | SERIAL PK | Event ID |
| name | TEXT | Event name (e.g., "user.signup") |
| data | JSONB | Event payload |
| source | TEXT | Event source |
| created_at | TIMESTAMPTZ | Timestamp |
External database/service connections.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Connection name |
| type | TEXT | postgres, neon, supabase, redis, http, fly |
| config | JSONB | Connection config (url, credentials, etc.) |
| status | TEXT | Last test status |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
Webhook configurations fired on system events.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Webhook name |
| url | TEXT | Target URL |
| events | TEXT[] | Events to trigger on |
| secret | TEXT | HMAC signing secret |
| active | BOOLEAN | Whether webhook is active |
Server-rendered HTML pages.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Page name (URL slug) |
| title | TEXT | Page title |
| template | TEXT | HTML template |
| script | TEXT | JavaScript code |
| style | TEXT | CSS styles |
| head | TEXT | Extra head content |
| data_fn | TEXT | Function name for page data |
| layout | TEXT | Layout name |
| meta | JSONB | Page metadata |
Pack registry for route grouping.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Pack name |
| version | TEXT | Semantic version |
| description | TEXT | Pack description |
| author | TEXT | Pack author |
| icon | TEXT | Icon name |
| manifest | JSONB | Routes, daemons, functions |
| installed | BOOLEAN | Whether pack is installed |
| system_pack | BOOLEAN | Whether it's a system pack |
HTTP request log for debugging and analytics.
| Column | Type | Description |
|---|---|---|
| id | SERIAL PK | Log entry ID |
| method | TEXT | HTTP method |
| path | TEXT | Request path |
| status | INTEGER | Response status code |
| duration_ms | INTEGER | Response time |
| created_at | TIMESTAMPTZ | Timestamp |
sql-- Slowest requests in last 24h
SELECT method, path, status, duration_ms, created_at
FROM box_request_log
WHERE created_at > NOW() - INTERVAL '24 hours'
ORDER BY duration_ms DESC LIMIT 10;
Route version history (max 10 per route).
| Column | Type | Description |
|---|---|---|
| id | SERIAL PK | Version ID |
| route_name | TEXT | Route name |
| code | TEXT | Previous code |
| method | TEXT | Previous method |
| path | TEXT | Previous path |
| version | INTEGER | Version number |
| created_at | TIMESTAMPTZ | When this version was saved |
Background tasks running on interval or cron schedule.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Daemon name |
| interval_ms | INTEGER | Run every N milliseconds (null if cron) |
| cron | TEXT | Cron expression (null if interval) |
| code | TEXT | JS code to execute |
| active | BOOLEAN | Whether daemon is running |
| pack | TEXT | Pack group name |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
sql-- List active daemons
SELECT name, interval_ms, cron, active FROM box_daemons WHERE active = true;
Shared page layouts with wrapper templates.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Layout name |
| title | TEXT | Layout title |
| head | TEXT | Shared head content (CDN links, meta) |
| style | TEXT | Shared CSS |
| wrapper | TEXT | HTML wrapper with {{content}} placeholder |
| meta | JSONB | Layout metadata |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
Multi-instance registry for distributed Box setups.
| Column | Type | Description |
|---|---|---|
| name | TEXT PK | Instance name |
| url | TEXT | Instance URL |
| metadata | JSONB | Instance metadata |
| created_at | TIMESTAMPTZ | Registration time |
# Box Internal Tables -- SQL Reference
Use `POST /system/sql` or the `box_sql` MCP tool to query these tables directly.
---
## _route_store
Deployed route code and metadata.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Route identifier |
| method | TEXT | HTTP method (GET, POST, etc.) |
| path | TEXT | URL path |
| code | TEXT | Route handler JS code |
| pack | TEXT | Pack group name |
| meta | JSONB | Typed interface: `{description, inputs[], outputs, examples[]}` |
| validation | JSONB | Deploy validation results |
| deployed_at | TIMESTAMPTZ | Last deploy time |
```sql
-- Find routes by description keyword
SELECT name, method, path, meta->>'description' as desc
FROM _route_store
WHERE meta->>'description' ILIKE '%ai%'
ORDER BY name;
-- List routes in a pack
SELECT name, method, path FROM _route_store WHERE pack = 'ai-toolkit';
```
---
## box_state
Postgres-backed key-value store with optional TTL.
| Column | Type | Description |
|--------|------|-------------|
| key | TEXT PK | State key |
| value | JSONB | Any JSON value |
| ttl | INTEGER | Time-to-live in seconds (null = permanent) |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
```sql
-- Get a state value
SELECT value FROM box_state WHERE key = 'box:config';
-- List all state keys
SELECT key, updated_at FROM box_state ORDER BY key;
```
---
## box_functions
Stored reusable functions callable via `callFunction()` or `pipe()`.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Function name |
| code | TEXT | JS function body (args available as `args[]`) |
| description | TEXT | What the function does |
| pure | BOOLEAN | If true, no I/O access (composable via pipe) |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
---
## box_secrets
Encrypted secret values. Values only accessible via `getSecret()` in route code.
| Column | Type | Description |
|--------|------|-------------|
| key | TEXT PK | Secret key name |
| value | TEXT | Secret value (never exposed via GET) |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
```sql
-- List secret keys only (values are hidden)
SELECT key, created_at FROM box_secrets ORDER BY key;
```
---
## box_logs
Application log entries.
| Column | Type | Description |
|--------|------|-------------|
| id | SERIAL PK | Log entry ID |
| level | TEXT | info, warn, error |
| message | TEXT | Log message |
| source | TEXT | Log source (route name, process, etc.) |
| created_at | TIMESTAMPTZ | Timestamp |
```sql
-- Recent errors
SELECT message, source, created_at FROM box_logs
WHERE level = 'error' ORDER BY created_at DESC LIMIT 20;
```
---
## box_events
Custom business events tracked via `emit()`.
| Column | Type | Description |
|--------|------|-------------|
| id | SERIAL PK | Event ID |
| name | TEXT | Event name (e.g., "user.signup") |
| data | JSONB | Event payload |
| source | TEXT | Event source |
| created_at | TIMESTAMPTZ | Timestamp |
---
## box_connections
External database/service connections.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Connection name |
| type | TEXT | postgres, neon, supabase, redis, http, fly |
| config | JSONB | Connection config (url, credentials, etc.) |
| status | TEXT | Last test status |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
---
## box_webhooks
Webhook configurations fired on system events.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Webhook name |
| url | TEXT | Target URL |
| events | TEXT[] | Events to trigger on |
| secret | TEXT | HMAC signing secret |
| active | BOOLEAN | Whether webhook is active |
---
## box_pages
Server-rendered HTML pages.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Page name (URL slug) |
| title | TEXT | Page title |
| template | TEXT | HTML template |
| script | TEXT | JavaScript code |
| style | TEXT | CSS styles |
| head | TEXT | Extra head content |
| data_fn | TEXT | Function name for page data |
| layout | TEXT | Layout name |
| meta | JSONB | Page metadata |
---
## box_packs
Pack registry for route grouping.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Pack name |
| version | TEXT | Semantic version |
| description | TEXT | Pack description |
| author | TEXT | Pack author |
| icon | TEXT | Icon name |
| manifest | JSONB | Routes, daemons, functions |
| installed | BOOLEAN | Whether pack is installed |
| system_pack | BOOLEAN | Whether it's a system pack |
---
## box_request_log
HTTP request log for debugging and analytics.
| Column | Type | Description |
|--------|------|-------------|
| id | SERIAL PK | Log entry ID |
| method | TEXT | HTTP method |
| path | TEXT | Request path |
| status | INTEGER | Response status code |
| duration_ms | INTEGER | Response time |
| created_at | TIMESTAMPTZ | Timestamp |
```sql
-- Slowest requests in last 24h
SELECT method, path, status, duration_ms, created_at
FROM box_request_log
WHERE created_at > NOW() - INTERVAL '24 hours'
ORDER BY duration_ms DESC LIMIT 10;
```
---
## box_route_versions
Route version history (max 10 per route).
| Column | Type | Description |
|--------|------|-------------|
| id | SERIAL PK | Version ID |
| route_name | TEXT | Route name |
| code | TEXT | Previous code |
| method | TEXT | Previous method |
| path | TEXT | Previous path |
| version | INTEGER | Version number |
| created_at | TIMESTAMPTZ | When this version was saved |
---
## box_daemons
Background tasks running on interval or cron schedule.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Daemon name |
| interval_ms | INTEGER | Run every N milliseconds (null if cron) |
| cron | TEXT | Cron expression (null if interval) |
| code | TEXT | JS code to execute |
| active | BOOLEAN | Whether daemon is running |
| pack | TEXT | Pack group name |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
```sql
-- List active daemons
SELECT name, interval_ms, cron, active FROM box_daemons WHERE active = true;
```
---
## box_layouts
Shared page layouts with wrapper templates.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Layout name |
| title | TEXT | Layout title |
| head | TEXT | Shared head content (CDN links, meta) |
| style | TEXT | Shared CSS |
| wrapper | TEXT | HTML wrapper with `{{content}}` placeholder |
| meta | JSONB | Layout metadata |
| created_at | TIMESTAMPTZ | Creation time |
| updated_at | TIMESTAMPTZ | Last update time |
---
## box_instances
Multi-instance registry for distributed Box setups.
| Column | Type | Description |
|--------|------|-------------|
| name | TEXT PK | Instance name |
| url | TEXT | Instance URL |
| metadata | JSONB | Instance metadata |
| created_at | TIMESTAMPTZ | Registration time |
When you deploy a route, the code field is JavaScript that runs in a VM sandbox. You have access to req, res, and these helpers.
javascript// Basic (uses defaults from box:config state key)
const answer = await ai("What is 2+2?");
// System prompt + JSON mode
const data = await ai("Extract entities", {
system: "Return JSON array",
json: true
});
// Multi-turn conversation
const reply = await ai("Follow up", {
messages: [
{ role: "user", content: "What is TypeScript?" },
{ role: "assistant", content: "A typed superset of JS." },
{ role: "user", content: "How does it compare to Flow?" }
]
});
// Switch provider (auto-detected from model name)
await ai(prompt, { model: "claude-sonnet-4-20250514" }); // → anthropic
await ai(prompt, { model: "gpt-4o" }); // → openai
await ai(prompt, { model: "gemini-2.5-flash" }); // → gemini
// Explicit provider + model
await ai(prompt, { provider: "openrouter", model: "meta-llama/llama-3-70b" });
Options: system, model, temperature, maxTokens, json, schema (Gemini only), provider, apiKeySecret, context (config overrides), messages, timeout (ms, default 25000).
Secrets required: GOOGLE_AI_KEY (gemini), OPENROUTER_API_KEY, AI_API_KEY (anthropic), OPENAI_API_KEY.
javascriptconst { image, caption } = await aiImage("A sunset over mountains");
// image = "data:image/png;base64,..."
// caption = "A beautiful sunset..." or null
Options: aspectRatio (default "16:9"), resolution (default 1024), model, context, apiKeySecret, timeout.
Safety: If Gemini blocks the image, throws error with blocked categories.
javascript// Parameterized query (prevents SQL injection)
const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
// Insert
await db.query("INSERT INTO logs (message) VALUES ($1)", ["Hello"]);
// Multiple rows
const { rows, rowCount } = await db.query("SELECT * FROM products WHERE price < $1", [100]);
javascript// Get
const cached = await redis.get("mykey");
const data = cached ? JSON.parse(cached) : null;
// Set with TTL (seconds)
await redis.set("mykey", JSON.stringify(data), "EX", 3600);
// Delete
await redis.del("mykey");
javascript// Decode JWT from Authorization: Bearer header
const user = getUser(); // returns payload or null
// Read a secret
const apiKey = await getSecret("MY_API_KEY");
// JWT operations
const token = jwt.sign({ userId: 123 }, secret, { expiresIn: "24h" });
const decoded = jwt.verify(token, secret);
// Password hashing
const hash = bcrypt.hashSync("password", 10);
const match = bcrypt.compareSync("password", hash);
// Crypto
const uuid = crypto.randomUUID();
javascript// Call a stored function (args available as args[0], args[1], etc.)
const result = await callFunction("double", 42); // returns 84
// Compose pure functions (each return becomes next args[0])
const result = await pipe(rawData, "validate", "transform", "format");
javascript// Deploy another route from within a route
await deploy("new-route", "GET", "/new", 'res.json({ok:true})');
// Remove a route
await undeploy("old-route");
// State management
await updateState("counter", 42);
await deleteState("counter");
// Track events
await emit("user.signup", { email: "test@example.com" });
javascript// HTTP requests (Node 20 fetch)
const resp = await fetch("https://api.example.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "value" })
});
const data = await resp.json();
// Query external database (registered connection)
const { rows } = await queryConnection("my-neon", "SELECT * FROM users");
// Safe require (blocks fs, child_process, vm, os, net, etc.)
const lodash = require("lodash");
javascript// Reuse logic across routes with callFunction
const formatted = await callFunction("format-user", rawUserRow);
res.json(formatted);
javascript// Orchestrate multi-step workflows by calling other routes
const apiKey = await getSecret("BOX_SELF_API_KEY");
const resp = await fetch("http://localhost:8080/ai/generate", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({ prompt: req.body.prompt })
});
const result = await resp.json();
res.json(result);
javascript// Chain pure functions -- each return becomes next args[0]
const result = await pipe(rawData, "validate", "transform", "format");
res.json(result);
javascript// Read config from state, cache expensive work, emit audit event
const config = await db.query("SELECT value FROM box_state WHERE key = 'my:config'");
const settings = JSON.parse(config.rows[0]?.value || "{}");
const cacheKey = "result:" + req.params.id;
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
const result = await ai(req.body.prompt, { model: settings.model });
await redis.set(cacheKey, JSON.stringify(result), "EX", 3600);
await emit("generation.complete", { id: req.params.id });
res.json(result);
Full composition patterns and autonomous agent templates: operations.md
javascript// POST /items -- Create
const { name, value } = req.body;
if (!name) return res.status(400).json({ error: "name required" });
const { rows } = await db.query(
"INSERT INTO items (name, value) VALUES ($1, $2) RETURNING *",
[name, value]
);
res.json(rows[0]);
javascript// POST /summarize -- AI summarization with caching
const { text } = req.body;
const cacheKey = "summary:" + crypto.createHash("md5").update(text).digest("hex");
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
const summary = await ai("Summarize this text: " + text, {
system: "Return a concise 2-sentence summary",
temperature: 0.3
});
const result = { summary, cached: false };
await redis.set(cacheKey, JSON.stringify(result), "EX", 3600);
res.json(result);
javascript// Require authentication
const user = getUser();
if (!user) return res.status(401).json({ error: "Unauthorized" });
res.json({ message: "Hello " + user.name, userId: user.id });
Daemons run on a schedule (interval or cron) with a reduced sandbox. They have no HTTP context.
| Helper | Purpose |
|---|---|
db.query(sql, params?) |
Postgres queries |
redis.get/set/del |
Redis cache |
fetch(url, opts) |
HTTP requests |
console.log/error/warn |
Logging |
require(module) |
Safe subset of Node modules |
| Helper | Why |
|---|---|
req, res |
No HTTP request/response -- daemons run on timer |
ai(prompt, opts?) |
Not injected into daemon sandbox |
aiImage(prompt, opts?) |
Not injected into daemon sandbox |
getUser() |
No HTTP context |
getSecret(key) |
Not injected (query box_secrets table directly) |
callFunction(name, ...args) |
Not injected (use db.query to read function code if needed) |
deploy/undeploy |
Not injected |
emit(name, data) |
Not injected (insert into box_events directly) |
javascript// Instead of ai() -- call the AI route via fetch
const resp = await fetch("http://localhost:8080/ai/generate", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": "YOUR_KEY" },
body: JSON.stringify({ prompt: "Hello" })
});
const data = await resp.json();
// Instead of getSecret() -- query the table
const { rows } = await db.query("SELECT value FROM box_secrets WHERE key = $1", ["MY_KEY"]);
const secret = rows[0]?.value;
// Instead of emit() -- insert directly
await db.query(
"INSERT INTO box_events (name, data, source) VALUES ($1, $2, $3)",
["my.event", JSON.stringify({ count: 42 }), "daemon:my-task"]
);# Writing Box Routes -- Sandbox API Reference
When you deploy a route, the `code` field is JavaScript that runs in a VM sandbox. You have access to `req`, `res`, and these helpers.
---
## AI Text Generation
```javascript
// Basic (uses defaults from box:config state key)
const answer = await ai("What is 2+2?");
// System prompt + JSON mode
const data = await ai("Extract entities", {
system: "Return JSON array",
json: true
});
// Multi-turn conversation
const reply = await ai("Follow up", {
messages: [
{ role: "user", content: "What is TypeScript?" },
{ role: "assistant", content: "A typed superset of JS." },
{ role: "user", content: "How does it compare to Flow?" }
]
});
// Switch provider (auto-detected from model name)
await ai(prompt, { model: "claude-sonnet-4-20250514" }); // → anthropic
await ai(prompt, { model: "gpt-4o" }); // → openai
await ai(prompt, { model: "gemini-2.5-flash" }); // → gemini
// Explicit provider + model
await ai(prompt, { provider: "openrouter", model: "meta-llama/llama-3-70b" });
```
**Options:** `system`, `model`, `temperature`, `maxTokens`, `json`, `schema` (Gemini only), `provider`, `apiKeySecret`, `context` (config overrides), `messages`, `timeout` (ms, default 25000).
**Secrets required:** `GOOGLE_AI_KEY` (gemini), `OPENROUTER_API_KEY`, `AI_API_KEY` (anthropic), `OPENAI_API_KEY`.
---
## AI Image Generation
```javascript
const { image, caption } = await aiImage("A sunset over mountains");
// image = "data:image/png;base64,..."
// caption = "A beautiful sunset..." or null
```
**Options:** `aspectRatio` (default "16:9"), `resolution` (default 1024), `model`, `context`, `apiKeySecret`, `timeout`.
Safety: If Gemini blocks the image, throws error with blocked categories.
---
## Database (Postgres)
```javascript
// Parameterized query (prevents SQL injection)
const { rows } = await db.query("SELECT * FROM users WHERE id = $1", [userId]);
// Insert
await db.query("INSERT INTO logs (message) VALUES ($1)", ["Hello"]);
// Multiple rows
const { rows, rowCount } = await db.query("SELECT * FROM products WHERE price < $1", [100]);
```
---
## Cache (Redis)
```javascript
// Get
const cached = await redis.get("mykey");
const data = cached ? JSON.parse(cached) : null;
// Set with TTL (seconds)
await redis.set("mykey", JSON.stringify(data), "EX", 3600);
// Delete
await redis.del("mykey");
```
---
## Auth & Crypto
```javascript
// Decode JWT from Authorization: Bearer header
const user = getUser(); // returns payload or null
// Read a secret
const apiKey = await getSecret("MY_API_KEY");
// JWT operations
const token = jwt.sign({ userId: 123 }, secret, { expiresIn: "24h" });
const decoded = jwt.verify(token, secret);
// Password hashing
const hash = bcrypt.hashSync("password", 10);
const match = bcrypt.compareSync("password", hash);
// Crypto
const uuid = crypto.randomUUID();
```
---
## Functions
```javascript
// Call a stored function (args available as args[0], args[1], etc.)
const result = await callFunction("double", 42); // returns 84
// Compose pure functions (each return becomes next args[0])
const result = await pipe(rawData, "validate", "transform", "format");
```
---
## Self-Editing
```javascript
// Deploy another route from within a route
await deploy("new-route", "GET", "/new", 'res.json({ok:true})');
// Remove a route
await undeploy("old-route");
// State management
await updateState("counter", 42);
await deleteState("counter");
// Track events
await emit("user.signup", { email: "test@example.com" });
```
---
## External
```javascript
// HTTP requests (Node 20 fetch)
const resp = await fetch("https://api.example.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "value" })
});
const data = await resp.json();
// Query external database (registered connection)
const { rows } = await queryConnection("my-neon", "SELECT * FROM users");
// Safe require (blocks fs, child_process, vm, os, net, etc.)
const lodash = require("lodash");
```
---
## Composing Routes
### Route Calls Function
```javascript
// Reuse logic across routes with callFunction
const formatted = await callFunction("format-user", rawUserRow);
res.json(formatted);
```
### Route Calls Route
```javascript
// Orchestrate multi-step workflows by calling other routes
const apiKey = await getSecret("BOX_SELF_API_KEY");
const resp = await fetch("http://localhost:8080/ai/generate", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": apiKey },
body: JSON.stringify({ prompt: req.body.prompt })
});
const result = await resp.json();
res.json(result);
```
### Pure Function Pipeline
```javascript
// Chain pure functions -- each return becomes next args[0]
const result = await pipe(rawData, "validate", "transform", "format");
res.json(result);
```
### Route + State + Cache + Events
```javascript
// Read config from state, cache expensive work, emit audit event
const config = await db.query("SELECT value FROM box_state WHERE key = 'my:config'");
const settings = JSON.parse(config.rows[0]?.value || "{}");
const cacheKey = "result:" + req.params.id;
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
const result = await ai(req.body.prompt, { model: settings.model });
await redis.set(cacheKey, JSON.stringify(result), "EX", 3600);
await emit("generation.complete", { id: req.params.id });
res.json(result);
```
Full composition patterns and autonomous agent templates: [operations.md](operations.md)
---
## Common Route Patterns
### REST CRUD
```javascript
// POST /items -- Create
const { name, value } = req.body;
if (!name) return res.status(400).json({ error: "name required" });
const { rows } = await db.query(
"INSERT INTO items (name, value) VALUES ($1, $2) RETURNING *",
[name, value]
);
res.json(rows[0]);
```
### AI + Database
```javascript
// POST /summarize -- AI summarization with caching
const { text } = req.body;
const cacheKey = "summary:" + crypto.createHash("md5").update(text).digest("hex");
const cached = await redis.get(cacheKey);
if (cached) return res.json(JSON.parse(cached));
const summary = await ai("Summarize this text: " + text, {
system: "Return a concise 2-sentence summary",
temperature: 0.3
});
const result = { summary, cached: false };
await redis.set(cacheKey, JSON.stringify(result), "EX", 3600);
res.json(result);
```
### Protected Route
```javascript
// Require authentication
const user = getUser();
if (!user) return res.status(401).json({ error: "Unauthorized" });
res.json({ message: "Hello " + user.name, userId: user.id });
```
---
## Daemon Sandbox (Different from Routes)
Daemons run on a schedule (interval or cron) with a **reduced sandbox**. They have no HTTP context.
### Available in Daemons
| Helper | Purpose |
|--------|---------|
| `db.query(sql, params?)` | Postgres queries |
| `redis.get/set/del` | Redis cache |
| `fetch(url, opts)` | HTTP requests |
| `console.log/error/warn` | Logging |
| `require(module)` | Safe subset of Node modules |
### NOT Available in Daemons
| Helper | Why |
|--------|-----|
| `req`, `res` | No HTTP request/response -- daemons run on timer |
| `ai(prompt, opts?)` | Not injected into daemon sandbox |
| `aiImage(prompt, opts?)` | Not injected into daemon sandbox |
| `getUser()` | No HTTP context |
| `getSecret(key)` | Not injected (query `box_secrets` table directly) |
| `callFunction(name, ...args)` | Not injected (use `db.query` to read function code if needed) |
| `deploy/undeploy` | Not injected |
| `emit(name, data)` | Not injected (insert into `box_events` directly) |
### Daemon Workarounds
```javascript
// Instead of ai() -- call the AI route via fetch
const resp = await fetch("http://localhost:8080/ai/generate", {
method: "POST",
headers: { "Content-Type": "application/json", "x-api-key": "YOUR_KEY" },
body: JSON.stringify({ prompt: "Hello" })
});
const data = await resp.json();
// Instead of getSecret() -- query the table
const { rows } = await db.query("SELECT value FROM box_secrets WHERE key = $1", ["MY_KEY"]);
const secret = rows[0]?.value;
// Instead of emit() -- insert directly
await db.query(
"INSERT INTO box_events (name, data, source) VALUES ($1, $2, $3)",
["my.event", JSON.stringify({ count: 42 }), "daemon:my-task"]
);
```