Safety model
NXD is built so that the destructive step is the one that is hardest to reach by accident. Every safety property below is a refusal in code, not a convention.
The core separation
| Command | Reads live state | Writes a proposal | Mutates infrastructure |
|---|---|---|---|
plan | yes | yes | no |
apply | yes | no | yes, for exactly one persisted plan |
verify | yes | no | no |
apply never re-plans. It executes one plan that already exists on disk, which
is what makes the reviewed artifact and the executed artifact the same object.
Why approval binds to a digest
A plan is written to disk and hashed. Approval is recorded as evidence bound to that hash, so authorization cannot drift from what was reviewed.
apply refuses when any of these hold:
- The plan expired. Plans carry
expiresAtUnix. Applying past it fails with the expiry and current time, and the fix is a new plan — expiry is not overridable. - The digest does not match. Evidence names a specific plan digest. Evidence for a different plan is rejected.
- The approval summary does not match the actions.
approvalRequirementsis recomputed from the plan’s own actions and install mode, and compared. A summary that was edited, stripped, or forged after planning is refused with “plan approvalRequirements do not match its actions; recreate and review the plan”. The summary is convenience data, never an independent authority. - Evidence is missing on a destructive plan with non-interactive stdin. CI cannot silently inherit a human’s approval. The refusal prints the digest, the expiry, every requirement with its risk and resources, and the two commands needed to proceed.
- Evidence is supplied for a plan that needs none. Mismatched expectations are an error in both directions.
Approval evidence has its own constraints: the principal must be a non-empty operator identity under 128 characters with no control characters, expiry must be in the future, and evidence may not outlive the plan it approves. Evidence defaults to a one-hour TTL, clamped to the plan’s own expiry.
Risk classification
Each action carries one of five risk levels, ordered lowest to highest:
| Risk | Meaning |
|---|---|
ReadOnly | Observation and verification assertions; changes nothing |
Reversible | Mutates state that can be restored without data loss |
ServiceImpacting | Activates profiles, reboots, or mutates running workloads |
Destructive | Removes or overwrites state that is not recoverable from NXD |
IdentityCritical | Modifies cryptographic identity or credential bindings |
Evidence records the highest risk present in the plan, so approving a low-risk plan never authorizes a high-risk one.
Default plan output groups work by target and lists destructive and
identity-critical actions first, naming the exact resources touched.
--verbose renders the full graph with identifiers and dependency edges.
Secrets
Secrets appear in configuration only as bindings and are resolved at runtime.
A secret value must never appear in Nix expressions, canonical JSON, plans,
logs, events, argv, URLs, or committed documentation. argv matters
specifically because process arguments are readable by other users on the same
host — secrets reach subprocesses through the environment or 0600 files, never
as command-line arguments.
Enrollment continuation
Interactive durable enrollment (deploy, switch --reenroll, and install or
convert intents) asks once for the enrollment plan plus a displayed, bounded
same-session continuation across the selected targets.
The continuation is not pre-authorized work. It is freshly planned, persisted, digested, and validated against the authorized scope before any approval evidence is written. Any change to source, target, action, provider, secret, dependency, or risk stops for explicit review.
The split exists because minting is decided by reading the current persisted binding and matching it against live inventory, and minting mutates an input that the installing plan’s own planning reads. One plan cannot honestly contain both. Non-interactive and separate plan/apply workflows still require evidence per digest.
Plan retention
nxd clean is a dry run unless --apply is given. Even then it only archives
expired plans that carry no approval, journal, or pin evidence — it never
deletes reviewed evidence.
--pin <plan>protects a plan through a long review.--unpin <plan>releases it.--recover <archived-plan>restores an archived plan.
Recovery restores the artifact, not its authority: expiry, compatibility, and approval checks all still apply.
What this does not protect against
Stated plainly, because a safety model that implies more than it delivers is worse than none:
- An operator who approves a plan without reading it. Approval proves what was authorized, not that anyone understood it.
- Anything outside NXD’s model — a change made directly on a host, or through a provider’s own UI, is drift NXD can only detect at the next plan.
- Correctness of your desired state. NXD converges infrastructure to what you declared; it has no opinion on whether that is what you wanted.