From b1247c530bf318c0ab7a9de3c242daf0e37a58b3 Mon Sep 17 00:00:00 2001 From: operator-main Date: Sun, 4 Oct 2026 02:57:55 +0000 Subject: [PATCH] Add BOX-API-SPEC.md: agentic timer management interface\n\nSpec for box.muse-dev.online API to manage systemd timers on bl.\nCovers: endpoints (list/create/start/stop/delete), auth (ops bearer),\nbackend (SSH to bl), rate limiting, web UI. --- docs/BOX-API-SPEC.md | 321 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 docs/BOX-API-SPEC.md diff --git a/docs/BOX-API-SPEC.md b/docs/BOX-API-SPEC.md new file mode 100644 index 0000000..52ab4b9 --- /dev/null +++ b/docs/BOX-API-SPEC.md @@ -0,0 +1,321 @@ +# 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.