Unknown Is Not a Principal
A fail-closed Discord guard blocked a trusted scheduler because its authorization model treated every unresolved session as the same stranger.
The publication job said it had completed. Its output said something else:
Gateway session identity could not be resolved; tool access fails closed.
The scheduled agent never got past the first mandatory preflight. A Discord security plugin denied its terminal and file tools, so there was no repository check, draft, or deployment.
That denial was correct according to the policy I had written. It was also wrong for the system running it.
The principal I forgot to model
The plugin protected an agent shared through Discord. It admitted a small allowlist, recovered sender identity from the gateway’s session store, and rejected tool calls when a persisted session could not be resolved. That last rule was intentional. Falling back to a prompt, a stale task-local value, or a caller-supplied name would have made the boundary easy to confuse and possibly easy to bypass.
The scheduler did not arrive through Discord, though. It created its own synthetic session for an internal agent run. That session was absent from the Discord gateway store, so the guard classified it as unresolved. From there, fail-closed behavior did exactly what it was supposed to do: it denied access.
The mistake was treating “unresolved by this identity source” as a principal. It is not one. It is an observation made by one subsystem.
A Discord session missing from the Discord store may be suspicious. A cron session missing from that store is normal. A desktop session, delegated worker, webhook, and test process can each have different runtime-owned evidence. Collapsing all of them into one unknown bucket made the policy simpler on paper and broader than its authority in practice.
The outer scheduler made the failure slightly harder to see. The agent returned a normal response explaining that publication had been skipped, so job metadata recorded a successful run. The transport and agent function had completed even though the intended outcome had not. A green scheduler status was evidence that the function returned, not that a page reached production.
Runtime provenance beat a plausible label
My first repair idea tried to recognize cron work from several clues: a cron-shaped session ID, a process marker, a valid job identifier, and a matching entry in the scheduler’s files. An adversarial review rejected it. Each clue proved that the call looked like scheduled work, or that a job existed somewhere. None proved that this tool call came from the live scheduler executor.
The stronger repair bound the actual agent object at the runtime-owned execution boundary. The sequential and concurrent tool executors placed that object in a ContextVar for the duration of the call and reset it in finally. Cron authority was accepted only when the bound agent platform, session ID, task ID, and hook metadata all agreed. Missing context or a mismatch still denied access.
That version passed its security specifications and restored the publication job. It was a useful fix, but it also exposed a design smell: a Discord policy had grown instrumentation inside the global tool executor so it could distinguish every non-Discord principal it might encounter.
The final design is smaller. The active plugin now registers only pre_gateway_dispatch. It validates the Discord sender, rejects malformed policy and unlisted identities, and routes admitted people into separate Hermes profiles before session creation. Non-Discord dispatches are outside that hook’s authority. The current admission suite passes nine focused tests, including malformed-policy rejection, per-sender profile routing, and confirmation that desktop dispatch is not subjected to Discord admission.
This distinction matters because Hermes profiles isolate state but are not sandboxes. They separate configuration, sessions, memory, and gateway state. They do not remove the underlying operating-system access granted to the process. In this installation, admitted profiles are trusted with their configured tools; the security claim is identity admission and state separation, not filesystem containment.
Discord’s own gateway path already has distinct stages for authorization, session lookup, agent execution, and response delivery. The custom policy now lives at the stage it understands instead of trying to govern every tool call in every execution context.
Fail closed inside a named boundary
I still prefer fail-closed authorization. The incident did not persuade me to turn an unresolved identity into an allow. It persuaded me to make the boundary explicit.
A guard should be able to answer three questions before it denies anything:
- Which principals is it responsible for classifying?
- Which runtime-owned evidence proves those principals?
- What happens to execution contexts outside its jurisdiction?
“Unknown means deny” is only safe after the system has defined unknown relative to the correct authority. Otherwise the guard can deny a scheduler, a recovery console, or its own diagnostic path while remaining perfectly faithful to a flawed model.
Another unauthorized Discord sender would not have revealed this bug. A trusted internal job did, simply by arriving through a door the guard had never learned existed.