Skip to content

Control actions table

A project’s page in BoB Control offers bob commands as buttons, in three groups: Start work, Check and Ship. Every button comes from one declared table, scripts/dashboard/control-actions.json (#3920). The page runs nothing the table does not declare, and adding an action changes no page code.

File scripts/dashboard/control-actions.json
Schema bob-control-actions/1, validated by schemas/control-actions.schema.json (make check-schemas)
Read by the dashboard server (resolveActions in scripts/dashboard/control-project-page.js) and the run route (scripts/dashboard/control-action-run.js)
Guarded by make check-control-actions (scripts/checks/check-control-actions.js)
Tested by tests/test-control-actions.sh
{
"schema": "bob-control-actions/1",
"actions": [
{ "id": "plan-remaining", "group": "Check", "label": "What is left in the plan",
"command": "bob plan remaining <N>", "mutates": false,
"placeholders": { "N": { "label": "Flight plan", "pattern": "^[1-9][0-9]*$", "prefill": "flightplan" } } }
]
}
Field Required Meaning
schema yes bob-control-actions/1.
description no A note on the file itself.
actions[] yes One row per button, at least one. The page lists them in the order written, within their group.
actions[].id yes The key a run request names: lowercase, digits and -, up to 41 characters. A request never carries the command. Unique in the table.
actions[].group yes Start work, Check or Ship.
actions[].label yes What the action does, in plain words, up to 60 characters.
actions[].command yes The command template: bob <noun> and what follows, on one line. Its noun and verb must be a row of scripts/bob/commands.json.
actions[].mutates yes true when the command changes something. See Commands that change something.
actions[].confirm no true makes every run ask first, its dry run too.
actions[].description no The one line shown beside the action. Without it, the line is the command’s tagline from commands.json.
actions[].placeholders no One entry per <NAME> in the command. See Placeholders.

No other field is allowed, at either level.

make check-control-actions requires each of these, under its group (#3920):

Group Commands
Start work bob plan build, bob warp start, bob do "<question>", bob decompose <N>
Check bob status, bob warp status, bob plan remaining <N>, bob groom --all, bob tour
Ship bob warp finalize, bob deploy (confirm), bob warp stop (confirm)
  1. Make sure the command is routed: it must be a row of scripts/bob/commands.json (bob <noun> --help answers for it).
  2. Add one row to actions[] in scripts/dashboard/control-actions.json, in its group and in the position it should appear. Give it an id, a group, a label and the command. Set mutates to whether it changes anything.
  3. For each <NAME> in the command, add a placeholders entry. Give it a pattern whenever the value has a known shape.
  4. Set confirm: true when even a dry run should ask first.
  5. Run make check-schemas check-control-actions. Both must pass.
  6. Run make test-control-actions. The page shows the new row on its next load; the server reads the table each time.

No page code changes. A row the server cannot resolve (its command is not routed) is left off the page, and the check fails on it first.

mutates: true decides which button the row gets and when it runs:

The row Button What happens
mutates: false Run Runs at once, in the project’s terminal.
mutates: true, and the command has --dry-run Dry run Runs with --dry-run. Run for real… then appears, and asks first.
mutates: true, no --dry-run Run… Shows the full command and runs it only after Run it.
confirm: true Dry run… Asks before the dry run too.
Any row with a placeholder Fill in… Opens the form first; the rules above apply to the run it sends.

Whether a command has --dry-run is read from its flags in commands.json, not declared here. On the server, a mutating action runs without confirmed: true only as its dry run. Its real run, and every run of a confirm row, needs confirmed: true; without it the route answers 409 with the command it would have run.

A placeholder is a <NAME> in the command, or "<NAME>" when the value is one quoted argument. Each has an entry under placeholders:

Field Meaning
label The field’s label in the form.
pattern A regular expression the value must match. Without it, any one line of up to 300 characters.
prefill What the page fills in from the project’s state. flightplan is the open flight plan’s number: the warp run’s plan, else the first open plan.

The form has one field per placeholder, the command’s own flags from commands.json with their descriptions, and a live preview of the command that will run. The server checks each value against its pattern, accepts only the command’s own flags, and shell-quotes every value. Anything else is 400.

A button sends POST /control/project/run with { project, action, values?, flags?, dry_run?, confirmed?, machine? }, never the command. machine names the fleet machine the caller expects the project’s terminal on:

Rule Answer
A body with any other field 400
An action the table does not declare 404
A value that fails its pattern, or a flag that is not the command’s own 400
A real run of a mutating action, or any run of a confirm row, without confirmed: true 409, with the command it would have run
A caller that is neither this machine with its token nor a machine machines.json declares, or a request arriving on an address other than loopback or the tailnet 403
Run 200. The command is typed into the project’s terminal, and one line is added to $BOB_HOME/control-runs.jsonl (bob control runs prints it)

The command bar uses the same route with command_id in place of action (The command bar).

Finding Means
unknown-command An action’s command is not a row of scripts/bob/commands.json.
duplicate-id Two actions share an id.
missing-action One of the actions the page ships with is not in the table, or not under its group.
unknown-declared scripts/dashboard/control-commands.json (the command bar’s reads and machine lists) names a command that is not routed.

Exit 0 is clean, 1 is a finding, and 2 means a file could not be read.

Value Defined in
The stuck threshold: a session quiet for more than 60 minutes is stuck on the Now page Fleet Feed → the stuck threshold (STUCK_AFTER_MINUTES in scripts/dashboard/control-running.js)
An ask’s unblocks field, and the order of the asks bob asks → What an ask unblocks (scripts/asks/unblocks.js)