backfill: a credential for a commander that predates them

a commander made before credentials existed has no officer.env and no key
row, so it cannot reach the platform at all — the state that made the first
one curl whisper directly. deleting and recreating it would fix that and
throw away the conversation, which is the one thing a commander IS: a
long-running agreement about a job.

so this is additive and re-runnable. a commander that already has one is
left alone. CLAUDE.md is APPENDED to, never rewritten — it is a living
document its commander has been extending, and surviving is the whole
point of not recreating it — and only when it does not already mention
officer.env, so a hand-edited one does not end up with the section twice.

the credential section moved out of commanderClaudeMd for this: it is now
written in two places, and the backfill needs a self-contained section
rather than a slice of one file's layout.

verified locally: minted for the one commander here, second run reported
"already has a credential" and changed nothing, file is 0600.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-24 13:57:01 +00:00
co-authored by Claude Opus 5
parent 12a9605b01
commit a3a8516e66
3 changed files with 146 additions and 37 deletions
+6 -2
View File
@@ -91,8 +91,12 @@ export async function writeCommanderCredential(params: {
"# This commander's own credential. Source it before calling Officer:",
'# set -a; . ./officer.env; set +a',
'#',
'# It is narrowed — see `permissions` on its api_keys row — and it is revoked when this commander is',
'# deleted. Do not copy it anywhere else; anything that needs one gets its own.',
'# It reaches everything the platform does EXCEPT administering it — no installing plugins, no',
'# managing users or roles, no server settings. That holds for anything added later too: the rule is',
'# resolved per request, not frozen into a list.',
'#',
'# Revoked when this commander is deleted. Do not copy it anywhere else; anything that needs one',
'# gets its own.',
`OFFICER_API_URL=${OFFICER_API_URL}`,
`OFFICER_API_HOST=${process.env.PUBLIC_URL ? new URL(process.env.PUBLIC_URL).host : `127.0.0.1:${PORT}`}`,
`OFFICER_API_KEY=${secret}`,
+51 -35
View File
@@ -27,6 +27,56 @@ export type CommanderClaudeMdParams = {
credentialPath: string | null;
};
/**
* The section telling a commander it has a credential, where it is, and what it must not do with it.
*
* Exported because it is written in two places: at creation, and by
* `scripts/backfill-commander-credentials.ts` for a commander that predates credentials existing. That
* backfill APPENDS it to a `CLAUDE.md` its commander has been extending for days, so this has to be a
* self-contained section rather than a slice of one file's layout.
*/
export function credentialSection(credentialPath: string | null): string[] {
if (!credentialPath) {
return [
'## Reaching the platform',
'',
'This commander has NO credential — minting one failed. It cannot reach the platform API, and it',
'must not work around that by calling a sidecar directly. Tell the owner.',
'',
];
}
return [
'## Reaching the platform',
'',
'This commander has its own credential, and it is the ONLY way it should reach anything:',
'',
'```',
`set -a; . ${credentialPath}; set +a`,
'curl -H "Authorization: Bearer $OFFICER_API_KEY" -H "Host: $OFFICER_API_HOST" \\',
' "$OFFICER_API_URL/api/..."',
'```',
'',
'Use the platform, never the thing behind it. A sidecar — whisper, a media server, anything a plugin',
'wraps — usually also listens on localhost with no authentication, so calling it directly works and',
'looks correct. It is still wrong: it skips every permission, every setting the owner chose and every',
'log, and it breaks silently the day that service moves.',
'',
'Prefer a TASK over a raw call where one exists. `GET /api/tasks` lists them and',
'`POST /api/tasks/<dirName>/run` runs one and returns what it did — one reviewed definition on disk',
'instead of a re-implementation living in a loop nobody reads.',
'',
'It reaches everything the platform does EXCEPT administering it — installing plugins, managing users',
'or roles, changing server settings. That holds for features added later too: the rule is resolved per',
'request rather than frozen into a list, so a plugin installed next year is already covered without',
'this commander being re-issued.',
'',
'A 403 is an answer, not a problem to route around. It means this commander was not given that, and',
'the owner is the one who decides otherwise. Say so rather than finding another way in.',
'',
];
}
/**
* The file itself. Prose, not a config dump — this is a document the commander reads once and then keeps
* extending, and the owner may open it directly.
@@ -67,41 +117,7 @@ export function commanderClaudeMd(params: CommanderClaudeMdParams): string {
'The platform cannot find this process and cannot recover a spawned agent id after it exits, so this',
'record is the only one. Without it, deleting this commander leaves its loop running.',
'',
'## Reaching the platform',
'',
...(params.credentialPath
? [
'This commander has its own credential, and it is the ONLY way it should reach anything:',
'',
'```',
`set -a; . ${params.credentialPath}; set +a`,
'curl -H "Authorization: Bearer $OFFICER_API_KEY" -H "Host: $OFFICER_API_HOST" \\',
' "$OFFICER_API_URL/api/..."',
'```',
'',
'Use the platform, never the thing behind it. A sidecar — whisper, a media server, anything a',
'plugin wraps — usually also listens on localhost with no authentication, so calling it directly',
'works and looks correct. It is still wrong: it skips every permission, every setting the owner',
'chose and every log, and it breaks silently the day that service moves.',
'',
'Prefer a TASK over a raw call where one exists. `GET /api/tasks` lists them and',
'`POST /api/tasks/<dirName>/run` runs one and returns what it did — one reviewed definition on',
'disk instead of a re-implementation living in a loop nobody reads.',
'',
'It reaches everything the platform does EXCEPT administering it — installing plugins, managing',
'users or roles, changing server settings. That holds for features added later too: the rule is',
'resolved per request rather than frozen into a list, so a plugin installed next year is already',
'covered without this commander being re-issued.',
'',
'A 403 is an answer, not a problem to route around. It means this commander was not given that,',
'and the owner is the one who decides otherwise. Say so rather than finding another way in.',
'',
]
: [
'This commander has NO credential — minting one failed at creation. It cannot reach the platform',
'API, and it must not work around that by calling a sidecar directly. Tell the owner.',
'',
]),
...credentialSection(params.credentialPath),
'## Before arming anything',
'',
'Never arm a watcher on the first turn. The first turn is for settling what to watch and what to do',