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.
This commit is contained in:
@@ -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 <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):**
|
||||
```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/<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):**
|
||||
```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-<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):**
|
||||
```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.
|
||||
Reference in New Issue
Block a user