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

11 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.

Appendix A: Rich Linux Scheduling

The box API supports multiple Linux scheduling mechanisms, not just systemd timers. Each has tradeoffs; the API abstracts them behind a unified interface.

Scheduler Types

Type Use case Precision Persistence
systemd Recurring jobs, services 1s Yes (enabled)
cron Simple recurring, legacy 1m Yes (crontab)
at One-time future jobs 1m No (one-shot)

Unified Scheduler API

The /api/box/timers endpoints accept a scheduler field:

{
  "name": "board-watch",
  "scheduler": "systemd",  // or "cron", "at"
  "schedule": "*/5 * * * *",
  ...
}

Systemd (default): As described in main spec. Best for most jobs.

Cron: For simple recurring tasks. The API manages the user's crontab.

# Backend: (crontab -l ; echo "*/5 * * * * /path/to/job.sh") | crontab -

Limitations: 1-minute precision, no built-in logging, harder to query status.

At: For one-time delayed execution.

# Backend: echo "/path/to/job.sh" | at now + 1 hour

Use for: "remind me in 2 hours", "retry this job once in 30m".

Scheduler-Specific Endpoints

GET /api/box/schedulers          # List available schedulers
GET /api/box/schedulers/{type}/jobs  # List jobs for a scheduler

Appendix B: DM + Metadata JSON

DMs carry structured metadata, not just text. The box API returns DMs with full metadata for programmatic processing.

DM Wire Format

Every DM has two layers:

1. Human-readable text (what the user sees):

[JOB board-watch-20261004-023000-a1b2c3d4] Check the board for new posts.

2. Metadata JSON (for agents and API):

{
  "id": "a1b2c3d4",
  "job_id": "board-watch-20261004-023000-a1b2c3d4",
  "type": "job",
  "from": "opm",
  "to": "muse",
  "target": "main",
  "sidechat": null,
  "timestamp": "2026-10-04T02:30:00Z",
  "priority": "normal",
  "timeout": 300,
  "chain_next": null,
  "signature": "-----BEGIN SSH SIGNATURE-----\n..."
}

DM Types

Type Description Required fields
job Job prompt from scheduler job_id, prompt
result Job result from agent job_id, success, output
chat Regular DM text
alert System alert severity, message
canary Signature test key_id, namespace

API: Get DMs with Metadata

GET /api/box/dms?agent=muse&limit=20

Response:

{
  "dms": [
    {
      "id": "a1b2c3d4",
      "text": "[JOB ...] Check the board...",
      "metadata": {
        "type": "job",
        "job_id": "board-watch-20261004-023000-a1b2c3d4",
        "from": "opm",
        "to": "muse",
        "timestamp": "2026-10-04T02:30:00Z",
        "priority": "normal"
      },
      "verified": true
    }
  ]
}

API: Send DM with Metadata

POST /api/box/dms
Content-Type: application/json

Body:

{
  "to": "muse",
  "target": "main",
  "type": "job",
  "text": "Check the board...",
  "metadata": {
    "job_id": "board-watch-20261004-023000-a1b2c3d4",
    "priority": "high",
    "timeout": 300
  }
}

The API constructs the wire format [JOB {job_id}] {text} and sends via dm.py. Metadata is logged to dm-log.jsonl alongside the text.

Metadata in Logs

dm-log.jsonl entries include metadata:

{
  "ts": "2026-10-04T02:30:00Z",
  "type": "send",
  "id": "a1b2c3d4",
  "metadata": {
    "job_id": "board-watch-...",
    "type": "job",
    "priority": "normal"
  }
}

This enables:

  • Filtering DMs by type, job, priority
  • Building dashboards (box UI shows job DMs separately from chat)
  • Audit trails (which jobs were sent, when, to whom)
  • Agent learning (which job types succeed/fail)