create a commander from a plugin's template, and refuse to arm a blank one
second half of the backend. GET /command-hq/plugin-commanders lists what the
installed plugins offer; POST /commanders takes {plugin, template, config} and
builds one from it.
the list is SEPARATE from GET /commanders, which returns what exists. merging a
thing that runs with a thing that could be made, distinguished by a flag, means
every consumer has to remember which it is holding.
`watch_dir` doubles as the reference path when a template supplies one. asking for
a reference path AND a watch directory is asking the same question twice and
inviting the two answers to disagree.
THREE FILES, EACH A COPY rather than a reference into the plugin:
state/config.env what the owner answered, in the form the watcher sources
watch.sh copied, so plugins:update cannot change the script a commander
is already running — a watcher whose code moves under it while
armed is a thing nobody could debug
AGENTS.md the template's prose, appended to the brief already written
config.env is generated with every value SINGLE-QUOTED, and that is not decoration:
WATCH_DIR is a path somebody typed, and this fleet has directories called `ABOUT US`
and `70´S`. unquoted, the first space ends the assignment and the watcher watches
the wrong folder — with `rm` downstream of that. verified by sourcing both that path
and one containing an apostrophe in a real shell.
THE ARM GUARD LIVES IN armCommandHqWatcher, not in a route. arming is not a button:
it happens when the agent starts a tracked background job and the platform notices.
a check anywhere else is one the agent routes around by simply starting its loop,
which is exactly what it does. a row with no template is unaffected.
verified end to end on the running server: the template listed, a commander created
from it, watch.sh + config.env (mode 600) + the brief on disk, config.env sourcing
cleanly, and the watcher refusing to start when config.env is absent — which is the
configure-before-running rule enforcing itself at the last line of defence.
still to come: the Command HQ section that shows the offers and draws the form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -135,3 +135,36 @@ export function missingInputs(
|
||||
.filter(([key, spec]) => spec.required && !(config?.[key] ?? spec.default ?? '').trim())
|
||||
.map(([key]) => key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Everything a created commander needs on disk beyond its `CLAUDE.md`.
|
||||
*
|
||||
* ── `config.env`, not a JSON file ──
|
||||
*
|
||||
* The watcher is a shell script and this is what it sources. A JSON config would mean the script
|
||||
* parsing JSON in bash, which is either `jq` (another dependency for four values) or something
|
||||
* hand-rolled that breaks on the first path with a space in it. The database row remains the source
|
||||
* of truth; this is a projection of it that bash can read.
|
||||
*
|
||||
* ── Every value is quoted, and that is not decoration ──
|
||||
*
|
||||
* `WATCH_DIR` is a path the owner typed, and this fleet has directories called `ABOUT US` and
|
||||
* `70´S`. Unquoted, the first space ends the assignment and the watcher silently watches the wrong
|
||||
* folder — or, with `rm` involved downstream, the wrong folder's contents.
|
||||
*/
|
||||
export function configEnv(
|
||||
inputs: Record<string, CommanderInput>,
|
||||
config: Record<string, string> | null | undefined,
|
||||
): string {
|
||||
const lines = ['# Written by Officer when this commander was created. Edit through the UI, not here.'];
|
||||
|
||||
for (const [key, spec] of Object.entries(inputs)) {
|
||||
const value = config?.[key] ?? spec.default ?? '';
|
||||
// Single quotes, with any embedded single quote closed and re-opened around an escaped one — the
|
||||
// standard shell trick, and the only form that is safe for arbitrary text.
|
||||
const quoted = `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||
lines.push(`${key.toUpperCase()}=${quoted}`);
|
||||
}
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user