The Daemon Isn't a Backdoor, It's Just Another Ape
I had built the Nest daemon's WS handler as if it had to use my IdP token. But the daemon runs as its own service user with no access to it. The solution wasn't to give it my token — but to give it its own identity.
The Nest daemon is a control plane: a long-lived process that runs per machine, supervises agents and takes in spawn intents. I built it a WebSocket handler that connects to the troop SP so the server can push spawn commands instead of being polled every five minutes. The handler called ensureFreshIdpAuth() — get the fresh IdP token, send it over the socket. In the planning phase that looked clean. On the first real start it never ran.
The daemon runs as _openape_nest, a hidden service user, HOME=/var/openape/nest. There's no auth.json there. ensureFreshIdpAuth() had nothing to fetch, because no one ever ran apes login there. There's no human in that HOME.
How it was meant before
The assumption in the plan: the daemon is my infrastructure. I start it, so it operates with my identity. The old poll model worked exactly like that — except it dodged the problem without me noticing. The 5-minute sync called apes run --as <agent> per agent. Every agent has its own DDISA identity in its own HOME. The setuid jump into the agent user was the auth. The daemon itself never needed a token, because it never spoke itself — it only ever let an agent speak.
The WebSocket path doesn't have that. A persistent connection to the server belongs to the daemon, not to an agent. The daemon has to speak itself. And the moment it speaks itself, it needs an identity — and I had silently assumed in the plan that it was mine. WS auth is a different model than the per-agent sync, not a variant of it. I overlooked that cut.
Why the obvious solution is wrong
The reflex is obvious: apes login as me, copy the auth.json to /var/openape/nest/, done. It works too. That's exactly why it's dangerous.
The daemon is long-lived, runs as a system-near service, supervises other processes. That's the place in the architecture where an owner token may least be allowed to go missing. And even if it doesn't leak: every action the daemon then performs carries act:human with my subject. When the daemon spawns an agent, the audit log says I did that. The log then lies not out of malice, but because the identity is modeled wrong.
A control-plane daemon that holds its owner's token is by definition a backdoor. Not because someone makes it one — but because the trust assumption "this privileged process acts as me" is exactly what a security architecture should not rest on.
The shift
The daemon isn't my infrastructure with my credentials. It is itself an ape.
_openape_nest gets its own DDISA agent identity, its own Ed25519 key material, its own apes login in its own HOME. The troop WS handler accepts this identity — act:agent next to act:human, not instead of:
// troop WS handler: both actors are first-class,
// neither is a special case of the other
const claim = verifyActClaim(token);
if (claim.act === "human") {
// owner connects from the browser / the CLI
} else if (claim.act === "agent") {
// the Nest daemon connects with its own DDISA identity
} else {
return reject(socket, "unknown actor");
}
The daemon now speaks as itself. Spawn intents that come in over this socket aren't my actions the daemon passes through — they're actions of the daemon, with its subject in the audit trail.
One question remains: a spawn happens for me. The daemon has its own token, but mine isn't reachable on the machine — and shouldn't be. How does the daemon act on-behalf-of-human without holding my token?
Over a delegation grant. RFC 8693 token exchange is actually strict: subject_token is mandatory, because the typical case is a confidential client that has both tokens. Our case is a different one — the daemon has its own token, mine is unreachable. Pragmatism: subject_token becomes optional when a delegation_grant_id is present; the IdP derives the delegator from the grant. No longer strict RFC, but the same effect — and the HITL point stays where it belongs: in the escapes grant flow, not in a copied token. I approve a delegation once, the daemon acts within its bounds, every step is server-side auditable.
What fell away
ensureFreshIdpAuth() in the daemon path. The whole "get Patrick's token, pass it on" code path. There's no passed-through owner token anymore, so there's no place it can leak.
Instead: one apes login as _openape_nest, the ssh keypair is copied along during the migration to the service user so the IdP identity stays the same — no re-enroll, all delegations and grants persist. One stumbling block: the 1h token expiry hits, and the auto-refresh can fail on the stale key_path in the migrated auth.json. Solution: explicitly trigger apes login --key after the migration. The pattern is reusable for any other OpenApe daemon that goes the same way.
The cut
I wanted to give the daemon my token. It became: the daemon gets its own identity.
That's not just an auth fix. It's a hint I now recognize everywhere. Whenever a component wants to be "privileged infrastructure" — the one that runs with the owner credentials, that acts as the human, that needs a special path in the auth — that's almost always a symptom, not a design goal. It means: this component doesn't have its own identity yet, and I plugged the gap with mine.
Give it its own, and the special treatment disappears. The daemon is then no longer a backdoor holding my token. It's just another ape — same protocol, own key material, HITL over the same grant flow as everyone else.
Code: github.com/openape-ai/openape, MIT-licensed. The Nest daemon lives in @openape/nest, the WS handler in the troop SP. The delegation-only token-exchange path lives in the IdP auth code (exchangeWithDelegation in @openape/cli-auth).