Files
box/docs/BOX-API-SPEC.md
T

6.9 KiB

BOX-API-SPEC.md: Agentic Timer Management Interface

Overview

The box page (https://box.muse-dev.online/) provides an agentic interface for scheduling and managing systemd timers on bl (the main compute host). Agents and humans use the same REST API; the web UI is a thin client over it.

Principle: Timers are jobs. The box API is the control plane for the JOB system (see JOB-SPEC.md). Agents can list, create, and manage timers without SSH access to bl.

Authentication

Same ops auth as the box page:

  • Bearer token: Authorization: Bearer <operator-token> (for agents)
  • PIN session: Cookie from /api/box/login (for humans via UI)
  • Agent signatures: Box-signed requests (for automated agents)

All endpoints require ops-level auth. Anonymous requests get 403. Public users see only the PIN login page, never timer data.

Base URL

https://box.muse-dev.online/api/box

Endpoints

List Timers

GET /api/box/timers

Returns all systemd user timers on bl.

Response (200):

{
  "timers": [
    {
      "name": "job-board-watch.timer",
      "active": true,
      "enabled": true,
      "last_run": "2026-10-04T02:30:00Z",
      "next_run": "2026-10-04T02:35:00Z",
      "job_name": "board-watch"
    }
  ]
}

Get Timer Status

GET /api/box/timers/{name}

Response (200):

{
  "name": "job-board-watch.timer",
  "active": true,
  "enabled": true,
  "last_run": "2026-10-04T02:30:00Z",
  "next_run": "2026-10-04T02:35:00Z",
  "last_result": "success",
  "job_definition": { ... }  // parsed YAML
}

Errors:

  • 404: Timer not found

Create Timer (and Job)

POST /api/box/timers
Content-Type: application/yaml

Body is a Job YAML definition (see JOB-SPEC.md).

Example body:

name: board-watch
schedule: "*/5 * * * *"
agent: muse
prompt_template: "Check board..."
timeout: 300

Response (201):

{
  "name": "job-board-watch.timer",
  "job_name": "board-watch",
  "created": true
}

Errors:

  • 400: Invalid YAML or missing required fields
  • 409: Timer already exists

Backend actions:

  1. Write /home/super/Projects/NetVM/jobs/<name>.yaml
  2. Generate systemd .service and .timer units
  3. systemctl --user daemon-reload
  4. systemctl --user enable --now job-<name>.timer

Control Timer

POST /api/box/timers/{name}/start
POST /api/box/timers/{name}/stop
POST /api/box/timers/{name}/enable
POST /api/box/timers/{name}/disable
POST /api/box/timers/{name}/restart

Response (200):

{
  "name": "job-board-watch.timer",
  "action": "start",
  "success": true
}

Delete Timer

DELETE /api/box/timers/{name}

Response (200):

{
  "name": "job-board-watch.timer",
  "deleted": true
}

Backend actions:

  1. systemctl --user stop job-<name>.timer
  2. systemctl --user disable job-<name>.timer
  3. Remove unit files
  4. systemctl --user daemon-reload
  5. Optionally keep or delete the job YAML (query param ?keep_yaml=true)

List Jobs

GET /api/box/jobs

Returns job definitions (YAML files in /home/super/Projects/NetVM/jobs/).

Response (200):

{
  "jobs": [
    {
      "name": "board-watch",
      "schedule": "*/5 * * * *",
      "agent": "muse",
      "has_timer": true,
      "timer_active": true
    }
  ]
}

Get Job Definition

GET /api/box/jobs/{name}

Response (200):

{
  "name": "board-watch",
  "definition": { ... }  // parsed YAML
}

Trigger Job Now

POST /api/box/jobs/{name}/trigger

Manually trigger a job immediately (outside its schedule). Useful for testing.

Response (200):

{
  "job_name": "board-watch",
  "job_id": "board-watch-20261004-023500-x1y2z3",
  "triggered": true
}

Backend Implementation

The board server (on VM, port 8090) proxies to bl via SSH.

SSH path: VM → bl via tailnet (super@100.123.153.75), using VM's SSH key. The board server runs as super on VM, which has passwordless SSH to bl.

Command mapping:

API SSH command on bl
List timers systemctl --user list-timers --no-pager --output=json
Get status systemctl --user status job-{name}.timer --no-pager
Start systemctl --user start job-{name}.timer
Stop systemctl --user stop job-{name}.timer
Enable systemctl --user enable job-{name}.timer
Disable systemctl --user disable job-{name}.timer
Create Write YAML + units, daemon-reload, enable --now

Security:

  • The board server validates job YAML before writing (schema check)
  • Timer names are sanitized: ^[a-z0-9-]+$ only
  • No shell injection: use systemctl with fixed args, never shell-expand user input
  • SSH uses a restricted key (only systemctl --user commands allowed via authorized_keys command restriction)
  • All actions logged to /srv/box/audit.jsonl

Rate Limiting

Use shared rate limiter (see bin/rate_limiter.py on bl):

  • Max 10 timer operations per minute per agent
  • Prevents accidental timer spam

Error Handling

All errors return JSON:

{
  "error": "Timer not found",
  "code": "NOT_FOUND"
}

Codes:

  • NOT_FOUND (404)
  • INVALID_YAML (400)
  • ALREADY_EXISTS (409)
  • SSH_ERROR (502) — bl unreachable
  • FORBIDDEN (403) — insufficient auth

Agentic Usage Examples

Python (using requests):

import requests

TOKEN = "opm-bearer-token-here"
BASE = "https://box.muse-dev.online/api/box"
H = {"Authorization": f"Bearer {TOKEN}"}

# List timers
r = requests.get(f"{BASE}/timers", headers=H)
print(r.json())

# Create job
job_yaml = open("myjob.yaml").read()
r = requests.post(f"{BASE}/timers", headers={**H, "Content-Type": "application/yaml"},
                  data=job_yaml)
print(r.json())

# Trigger now
r = requests.post(f"{BASE}/jobs/myjob/trigger", headers=H)
print(r.json())

Bash (curl):

TOKEN="opm-bearer-token-here"

# List
curl -H "Authorization: Bearer $TOKEN" \
  https://box.muse-dev.online/api/box/timers

# Create
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/yaml" \
  --data-binary @myjob.yaml \
  https://box.muse-dev.online/api/box/timers

Web UI

The human UI at https://box.muse-dev.online/ provides:

  • Timer list with status indicators (active/inactive, last run)
  • Create form (YAML editor with validation)
  • Start/stop/enable/disable buttons
  • Job definition viewer
  • Manual trigger button
  • Log viewer (job-log.jsonl, sidechat-log.jsonl)

The UI uses the same API endpoints as agents. No separate backend.

Relationship to Other Specs

  • JOB-SPEC.md: Defines job YAML format, dispatcher, agent handler. This spec defines the API to manage those jobs.
  • DM-SPEC.md: DMs are the transport for job prompts. This spec manages the schedulers that send those DMs.
  • SIDECHAT_SPEC.md: Jobs can create side chats. This API can list which side chats belong to which jobs.

Status

Spec v0.1. Owner: operator-main. Sanctioned by the human 2026-10-04. Implementation pending.