89 lines
4.2 KiB
Markdown
89 lines
4.2 KiB
Markdown
|
|
# Box Job Lifecycle over HTTPS (No SSH)
|
|||
|
|
|
|||
|
|
> **Box is the main surface.** All operator work goes through Box (box.muse-dev.online). The web UI, `box` CLI, and agents share the same API endpoints. No UI-only powers.
|
|||
|
|
|
|||
|
|
**Date:** 2026-10-06
|
|||
|
|
**Status:** implemented on bl (`exec-constrained.py` + `box-relay.sh`;
|
|||
|
|
`box-ctl.py` verbs pre-existed)
|
|||
|
|
**Scope:** safe job-lifecycle mutations. Deletes are deliberately NOT
|
|||
|
|
exposed (`job-delete`, `timer-delete` stay SSH/operator-only).
|
|||
|
|
|
|||
|
|
## 1. Why
|
|||
|
|
|
|||
|
|
Agents own scheduled automation but could only run jobs (`cron.run`) or read
|
|||
|
|
them (`box.exec`). Creating, updating, triggering, chaining, previewing, and
|
|||
|
|
pausing jobs needed SSH. All of this now rides the agent HTTPS path
|
|||
|
|
(`https://exec.muse-dev.online/exec`, signature or bearer auth, named-op
|
|||
|
|
allowlist, audit log).
|
|||
|
|
|
|||
|
|
## 2. New ops
|
|||
|
|
|
|||
|
|
| Op | Args | Backend | Write? | Who |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| `job.put` | `{name, definition}` | `box-ctl.py job-put` (definition on stdin) | yes (writes + commits) | known identities only |
|
|||
|
|
| `job.trigger` | `{name}` | `box-ctl.py job-trigger` | yes (dispatches now) | known identities only |
|
|||
|
|
| `job.chain` | `{from, to, on_failure?}` | `box-ctl.py job-chain` | yes (writes + commits) | known identities only |
|
|||
|
|
| `job.next` | `{job_id, success?}` | `box-ctl.py job-next` | no (dry-run) | any valid signer |
|
|||
|
|
| `cron.timer_stop` | `{name}` | `box-ctl.py timer-stop` | yes (systemd) | known identities only |
|
|||
|
|
| `cron.timer_disable` | `{name}` | `box-ctl.py timer-disable` | yes (systemd) | known identities only |
|
|||
|
|
|
|||
|
|
New `box-relay.sh` client commands:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
box cron put <name> '<json-definition>' # create or update (see §3)
|
|||
|
|
box cron trigger <name> # dispatch now (audited JSON)
|
|||
|
|
box cron chain <from> <to> [--on-failure] # wire chain_next
|
|||
|
|
box cron next <job-id> [--success|--fail] # dry-run: what dispatches next
|
|||
|
|
box timer stop <name> # pause schedule (keeps unit)
|
|||
|
|
box timer disable <name> # pause schedule (disables unit)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Raw op call (signature auth, no token):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
exec-sign.sh job.next '{"job_id": "heartbeat-20200101-000000-deadbeef"}'
|
|||
|
|
exec-sign.sh job.chain '{"from": "ops-audit-step2", "to": "ops-audit-step3"}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Notes:
|
|||
|
|
|
|||
|
|
- `job.trigger` vs existing `job.run`: `job.run` shells straight to
|
|||
|
|
`job-dispatch.py` and relays raw output; `job.trigger` goes through
|
|||
|
|
`box-ctl.py` (existence check, 300s bound, audit trail, JSON contract).
|
|||
|
|
Prefer `job.trigger` for agent-driven dispatches.
|
|||
|
|
- `job.next` job ids look like `<name>-YYYYMMDD-HHMMSS-<8hex>`; with no
|
|||
|
|
`success` flag the box infers it from the last recorded result.
|
|||
|
|
|
|||
|
|
## 3. Job definition shape (`job.put`)
|
|||
|
|
|
|||
|
|
The full schema is enforced by `box-ctl.py validate_job` (single copy);
|
|||
|
|
required fields: `name` (must match the argv name), `schedule` (`manual`
|
|||
|
|
or convertible cron), `agent` (fleet member), `prompt_template` (1–4000
|
|||
|
|
chars, no protocol literals, known `{placeholders}` only). `timeout`
|
|||
|
|
60–3600s, `on_failure` policy, optional `chain_next` (must exist) and
|
|||
|
|
`sidechat` / `dm_target` routing. `box-ctl.py` writes `jobs/<name>.json`
|
|||
|
|
and commits (`Add/Update job <name> via box-ctl`).
|
|||
|
|
|
|||
|
|
## 4. Safety notes (same posture as existing ops)
|
|||
|
|
|
|||
|
|
- **Fixed argv, validated values.** Job names match
|
|||
|
|
`^[a-z0-9][a-z0-9-]{0,63}$`; trigger/chain/timer ops require the job
|
|||
|
|
file to exist; chain rejects self-links and cycles; timer ops require
|
|||
|
|
the unit to exist. All checks run before any side effect.
|
|||
|
|
- **Stdin plumbing.** `job.put` is the first op to pipe a request body to
|
|||
|
|
`box-ctl.py` stdin (the raw definition, not the envelope); the routing
|
|||
|
|
lives in one helper (`_stdin_body`) covered by unit tests.
|
|||
|
|
- **No deletes, no kills.** `job-delete` / `timer-delete` are reachable
|
|||
|
|
only over the operator SSH path, by explicit scope decision.
|
|||
|
|
- **Audited.** Every execution records identity + op + args hash;
|
|||
|
|
`box-ctl.py` additionally audits each mutation with its target.
|
|||
|
|
|
|||
|
|
## 5. Rollout notes
|
|||
|
|
|
|||
|
|
- Restart `exec-constrained.py` after deploy for the new ops to appear in
|
|||
|
|
`GET /ops`.
|
|||
|
|
- `box-relay.sh` is served from bl (`GET /box`); agents re-fetch to get
|
|||
|
|
the `cron put|trigger|chain|next` and `timer stop|disable` commands.
|
|||
|
|
- Non-goals: deletes, `job-result` ingestion, `strat`/`loop` writes,
|
|||
|
|
approvals — future expansions, same pattern.
|