# 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 ` (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):** ```json { "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):** ```json { "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:** ```yaml name: board-watch schedule: "*/5 * * * *" agent: muse prompt_template: "Check board..." timeout: 300 ``` **Response (201):** ```json { "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/.yaml` 2. Generate systemd `.service` and `.timer` units 3. `systemctl --user daemon-reload` 4. `systemctl --user enable --now job-.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):** ```json { "name": "job-board-watch.timer", "action": "start", "success": true } ``` ### Delete Timer ``` DELETE /api/box/timers/{name} ``` **Response (200):** ```json { "name": "job-board-watch.timer", "deleted": true } ``` **Backend actions:** 1. `systemctl --user stop job-.timer` 2. `systemctl --user disable job-.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):** ```json { "jobs": [ { "name": "board-watch", "schedule": "*/5 * * * *", "agent": "muse", "has_timer": true, "timer_active": true } ] } ``` ### Get Job Definition ``` GET /api/box/jobs/{name} ``` **Response (200):** ```json { "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):** ```json { "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: ```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):** ```python 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):** ```bash 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.