--- id: node title: A host proves what it is once, and issues credentials to the sessions on it status: open crates: [didbot-attest, didbot-hook, didbot-hookd] dependsOn: [credentials, attestation] exitCriterion: > A machine holding a node credential issues a session credential to a hook that asks for one, a machine without one is refused, and many hooks starting at once produce one provisioning request per session rather than one per hook. --- # node [credentials](credentials.md) states the shape and names this as its first item: *the shape mirrors SPIFFE — a node attests once, a local component issues credentials, and the workload talks to the server itself.* This epic is that local component, split out because it is a process with a lifetime, an install story and a privilege boundary of its own, and those do not fit inside a bullet. ## Why a daemon rather than the hook Both were on the table. The hook is the simpler answer and it does not survive contact with a busy machine. A hook is a short-lived process spawned per tool call. Putting the node credential in it means every one of those processes reads the credential off disk, so the blast radius of anything that can observe a process is the whole node identity rather than one session. And a machine running several sessions and their subagents fires many hooks at once, each asking the server for an account, so provisioning arrives as a stampede with nowhere to be coalesced. A long-lived component fixes both: one process holds the credential, and it is the natural place to serialise and rate-limit what reaches the server. The cost is honest and worth writing down: it is another thing to install, supervise and version on every agent host, and [deploy](deploy.md) already notes that agent hosts are their own deployment. [dev-setup](dev-setup.md)'s service/profile/binding model is where it has to fit. - [ ] **Decide whether this is a new crate or a role [`didbot-hookd`](../crates/didbot-hookd/) grows.** There is already a daemon on that host doing adjacent work. A second one needs a reason better than tidiness, and growing the first one needs a reason better than convenience. ## What it holds, and what that is worth - [ ] **One device credential per node**, with the file treated as seriously as the server treats its signing keys. [`NodeCredentialBackend`](../crates/didbot-attest/src/node_credential.rs) is the backend: one secp256k1 key per node, the verifier keeping only the public half, so a claim for one node can only be produced by whoever holds that node's credential and a compromise yields one node rather than the deployment. - [ ] **The bootstrap, concretely.** How a host gets its credential in the first place is the root of this whole chain and the step most easily hand-waved. Somebody issues it out of band, and that somebody is the operator; what that looks like, and what stops a second machine using a copy, is this epic's to answer rather than to assume. - [ ] **Say what it is not.** The crate's own documentation already does: the credential is a file, so anything that can read the node's disk can copy it, and the copy is indistinguishable from the original. That is what hardware attestation is for, and it is a stub in [attestation](attestation.md) rather than a thing this epic delivers. ## The local transport is a privilege boundary - [ ] **Harden the socket the way [e-stop](e-stop.md) had to.** That epic shipped the pattern after the exposure review found its admin socket landing world-writable at a predictable path whenever `XDG_RUNTIME_DIR` was unset — which is containers, most CI runners and many service accounts. A directory this process owns at `0700`, the socket `chmod`ed to `0600` after bind rather than left to the umask, a default path that is not world-writable, and a staleness check that an attacker cannot satisfy by binding first. Repeating that mistake here would be worse, because what is behind this socket is credential issuance. - [ ] **A hook that cannot reach the daemon is refused, legibly.** Silence must not degrade into an unstamped call. [dev-setup](dev-setup.md) documents the recovery path that exists today — `PreToolUse` provisions when a session has no usable account — and whatever this becomes has to keep that working rather than replace it with a new way to fail quietly. ## Boundaries it must not move - [ ] **A subagent gets no credential of its own.** The custody boundary is the session; subagent identity is attribution. [credentials](credentials.md) says so and this epic is where it would quietly stop being true. [subagents](subagents.md) is where that boundary itself gets revisited, rather than merely guarded here. - [ ] **Nothing the daemon accepts from a hook may widen anything.** A hook-asserted fact is a claim about accounts this server minted, checkable against what it issued, and never taken on its word — [agent-accounts](agent-accounts.md)'s rule, which a component sitting between the hook and the server is well placed to erode. ## A layer this is not - [ ] **Cloud instance identity attests a different machine.** The AWS EC2 instance-identity backend in [attestation](attestation.md) proves things about the cloud instance it runs on. Agents run on laptops and workstations, which have no instance identity document, so it is not the mechanism for the hosts this deployment has. It is built and dormant, and becomes relevant the day agents run in cloud sandboxes, which [account-types](account-types.md) contemplates. The node credential is the mechanism for the hosts that exist now. ## Done Nothing closed yet.