every agent session gets a key, scoped to notify

a commander has officer.env and can reach the platform. an ordinary chat
agent had nothing — the only token in the tree lives in the MCP server's
environment, which the agent's own Bash cannot see. so an agent asked to
tell its owner something either could not, or asked to be handed a personal
API key: the owner's full authority, pasted into a transcript. one refused
tonight and was right to.

minted per session, permissions ['notify'], expiring in a week, named
session:<key> so re-arming replaces rather than accumulates and a survivor
is findable rather than one of a hundred identical rows.

"an agent may tell me things" is a far smaller thing to hand out than "an
agent may act as me", and it is the whole of what was being asked for.
everything else an agent needs it already has: it runs as somebody, in
their home, with their files. widening this later is one entry in an array;
starting wide and narrowing later never happens.

in the environment rather than a file, because it dies with the process:
nothing to leak, nothing to clean up after a crash, no file for a later
session to find. the commander's credential is a file for the opposite
reason — a detached loop reads it hours later.

members get it too, through the allowlist rather than round it. those
OFFICER_* names sit next to NEVER_ENV deliberately, and they are not that
kind of thing: NEVER_ENV guards the OWNER'S credentials, which would make a
member's shell more privileged than the member. this grants a member one
capability they already have, over their own devices, and nobody else's.

best effort throughout — a turn still starts if minting fails, and the
agent is simply back where it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 09:06:29 +00:00
co-authored by Claude Opus 5
parent 01cecdad60
commit 538a623bb8
3 changed files with 119 additions and 2 deletions
+17 -2
View File
@@ -54,8 +54,17 @@ export type MemberRun = {
*/
const ALLOWED_ENV = ['LANG', 'LC_ALL', 'TERM', 'TZ', 'NO_COLOR', 'CLAUDE_CODE_ENTRYPOINT'] as const;
/** Set by `memberEnv` itself rather than inherited, so vetted the same way. */
const ALSO_ALLOWED = ['HOME', 'CLAUDE_CONFIG_DIR'] as const;
/**
* Set by `memberEnv` itself rather than inherited, so vetted the same way.
*
* The three `OFFICER_*` entries are this member's OWN session credential, minted per turn and narrowed to
* `notify`. They belong here deliberately, and they are not the kind of thing `NEVER_ENV` below guards
* against: those are the OWNER'S credentials, which would make a member's shell more privileged than the
* member. This one grants a member exactly one capability they already have — telling their own devices
* something — and grants it to nobody else. Without it a member's agent has no way to reach the platform
* at all, which is a worse answer than a narrow key.
*/
const ALSO_ALLOWED = ['HOME', 'CLAUDE_CONFIG_DIR', 'OFFICER_API_URL', 'OFFICER_API_HOST', 'OFFICER_AGENT_KEY'] as const;
/**
* Credential names that must never appear in the allowlist. **This guards the constant, not the instance.**
@@ -127,6 +136,12 @@ function memberEnv(run: MemberRun, inherited: Record<string, string | undefined>
}
env.HOME = run.home;
env.CLAUDE_CONFIG_DIR = memberClaudeConfigDir(run.home);
// The member's own session credential, when one was minted. Absent is fine: the agent then has no key,
// which is where every member's agent was before this existed.
for (const name of ['OFFICER_API_URL', 'OFFICER_API_HOST', 'OFFICER_AGENT_KEY'] as const) {
const value = inherited[name];
if (value !== undefined) env[name] = value;
}
return env;
}