Permissions, approval, and the egress chokepoint
Supernova’s adversary model is not a remote attacker. It is the platform’s own agents, fed hostile text: a prompt can talk an in-process allowlist into anything, so the permission system’s root requirement demands authority that is “auditable, additive, OS-enforced, and impossible for a prompt to talk its way around.” The result is four small systems that each answer exactly one question about an action — and one door through which every external effect must pass, so that safety is a property of the path itself rather than of anyone’s good behavior.
Four layers, one question each
| Layer | Question | Owns |
|---|---|---|
| auth | Who is acting? | The single Principal contract (D4), transport-identity bindings, an encrypted-local secret store |
| permission | Is this actor allowed to? | Capability registry, group membership, OS-level enforcement |
| approval | Should this specific action happen? | Risk tiers, trust policies, human decisions, grant tokens |
| link | How does it reach the outside? | The only external-send path, with the gates built into it |
Each layer refuses to own its neighbor’s thing: permission owns the model and the check() primitive but not the gating act — the system that owns an operation calls the check at its own call site (D54, ratified 2026-06-20). Auth owns credentials; link resolves them at action time and never stores any.
flowchart TD
A["agent process"] -->|"asserts identity via whoami"| AU["auth: resolve Principal"]
AU -->|"principal_id"| P["permission: capability group check"]
P -->|"cap missing"| D1["denied — a designed outcome, not an error"]
P -->|"cap held"| AP["approval: risk tier and trust policy"]
AP -->|"denied or expired"| D2["hard wall — no auto-retry"]
AP -->|"approved: one-shot grant token"| L["link: the only door out"]
L -->|"scope digest re-verified at the boundary"| X["external world"]
RG["resource gate: bwrap sandbox — fs binds, net namespace, seccomp"] -.->|"confines the process itself"| A
Permission: the kernel as the capability gate
The model [live in automatt] is three levels, carried over deliberately unchanged (Matt, 2026-06-04: “current plan for permissions is to keep it as-is”): atomic capability groups where one Linux group grants exactly one capability (cap-commit, cap-approve, cap-network, …), roles as config-file bundles that compile down to group memberships, and a real OS user per agent. The enforcement claim is the whole point: an agent without cap-commit cannot commit because the kernel says no — “not bypassable via prompting, environment manipulation, or in-process checks,” per the permission requirements doc. The flagship invariant: never check agent name for capability. Code checks group membership only.
Capabilities are additive-only — granted, never expressed as restrictions. Everything starts at zero; ephemeral agents get no groups at all, structurally rather than by policy. A grant validator (D40) makes one rule non-configurable in code: an ephemeral identity can never be placed in a privileged group, and granting a privileged capability requires already holding it.
Two honesty mechanisms are worth noting. First, every capability declares its enforcement class — os-enforced or hook-enforced — because a CLI flag like --no-verify is not a Linux permission; the weaker guarantee is labeled rather than hidden. Second, a denial is not a failure: the outcome taxonomy is allow / deny (a designed branch, kept out of the error stream) / cannot-determine, and the last of these fails closed — deny, then crash loudly.
Beneath the action gate sits an orthogonal resource gate (D39, 2026-06-19): each agent process is meant to run in a bwrap sandbox with default-deny filesystem binds, a denied network namespace unless cap-network is held, and a seccomp filter. The two gates compose; per the permission invariants, “confinement only restricts reach, it never GRANTS authority.” The sandbox primitives are [engineered, not deployed] — verified against live bwrap, including a confined agent pod that ran the Rust toolchain (cargo, rustc) and git in-sandbox while a write outside its worktree failed with EROFS (2026-07-02) — but fleet-wide confinement of every spawn surface is still [supernova design], and host-level sun permission sync deliberately fails closed until a host OS backend is wired.
Approval: five tiers and a hard wall
Approval is the decision half of the gate — permission answers whether the actor may act at all, approval whether this specific action should happen — [live in automatt]: the predecessor’s core rule is that every action touching personal data or external services passes through it. Risk levels are a closed, ordered enum, each with a time-to-live after which a pending request expires as denied:
| Tier | TTL | Typical disposition |
|---|---|---|
| read | 300s | often auto-approved by policy |
| low | 600s | policy-dependent |
| medium | 1800s | policy-dependent |
| high | 3600s | usually a human |
| critical | 7200s | a human |
Trust policies are declarative config mapping action type, risk level, and agent to a disposition; most-specific wins, and on a precedence tie the system fails closed to human review. Pending requests generate periodic nudges bounded by the TTL, not by rate limits — an opinionated stance that the human should be pestered until they rule or the request dies.
The distinctive rule is what happens after a no. Per a 2026-06-20 ruling, “every approval request originates from an agent running into a wall and explicitly requesting to bypass it,” and a denial is a hard wall: there are no auto-dispatched retries. The agent adapts, tries a different approach, escalates through chat, or abandons. This came from approval fatigue in practice — Matt: “sometimes we want agents to just run into walls … we don’t want to pester me and nova every time.” The agent, not the platform, decides when a wall is worth an explicit ask.
Two structural details keep approvals from being a rubber stamp:
- Approval is a token, not a boolean. An approved request yields an immutable grant token — a digest of the action type, normalized parameters, and approval id — re-verified at the egress boundary. Approving one email cannot authorize a different email; scope creep is structurally rejected.
- Walled-command elevation (D3, a port of automatt’s
am sudo[live in automatt]): a low-privilege agent that hits a wall runssun approval request cmd <command> --reason <why>, and on approval receives a one-shot token scoped to exactly that command, consumed on use, with a 60-second lifetime. Elevation cannot bypass tests or hooks — it allows only the specific command submitted.
Agent-to-agent approval exists but is mechanically gated: the resolver must hold cap-approve (checked through permission, never by name), and a requester can never approve its own request. Even a Discord approval button resolves only after link verifies the provider’s Ed25519 signature, auth maps the external user id to a principal, and that principal proves cap-approve — “a raw bus event can never resolve a request.”
Link: one door, with the gate built into the frame
Link owns all external send (D1). It is a specialization of the bus reactor pattern with the gates built in — Matt: “links are special kind of reactor with built-in approvals.” That framing does the security work: because an external-boundary action is defined as a link action, and every link action carries the permission check plus the approval scope-verify before executing, there is no second path that could diverge into a bypass. The invariant is blunt: “No ungated outbound side effect, ever.” Blocked actions emit a .blocked event; nothing is silently dropped.
The same chokepoint stamps inbound traffic: every incoming item gets a trust tier from the transport-verified sender identity resolved via auth — not from the channel it arrived on. A message from an unknown sender on a trusted channel still lands as external_unknown and routes to triage rather than the realtime bus. Delivery at the boundary is idempotent, so a redelivered event cannot send two emails.
Build state: the gated send path is [engineered, not deployed] — live sends to Discord, ntfy, and Home Assistant require a cap-link actor, a non-self approval, and an auth credential reference, with a durable outbox before the provider call. The rest of the adapter inventory the design requires (Gmail, Calendar, Cloudflare, Proxmox, and others) is [supernova design], and no real-provider OAuth consent has been evidenced yet. The chokepoint is real; few doors are connected to it so far.
One deliberate hole in the layering is worth admitting rather than hiding: config writes are not gated by a runtime permission call. Matt: “config does NOT need permission, permission is technically enforced by operating system not by live system” — a whole-file OS write group guards the config file (D62), which breaks the circular dependency where permission’s own configuration would need permission to edit. Per-section gating was designed, found mechanically impossible to enforce, and retired; the decisions ledger keeps the tombstone.
Where this stands
The approval engine, one-shot elevation, and OS users/groups enforcement run today in automatt [live in automatt] — those behaviors are what Supernova treats as proven requirements. The Rust rewrite has all four systems implemented with test evidence [built in-repo]: durable approval lifecycle with the signed Discord resolve path, permission checks with container-level user/group projection and revocation, encrypted-local secret stores, and the gated live-send path. What is not yet true: per-agent OS users on every spawn surface and fleet-wide sandbox confinement are both draft requirements with planned evidence, and host-level permission actuation fails closed by design until a backend exists. Notably, the project’s own evidence system polices this gap — classifiers reject dry-run receipts, fixture proof, and placeholder ids as production-closing evidence, so local proof cannot masquerade as live proof.
What this costs
- It is heavy for one person’s data. Four systems, hooks, containers, a sandbox layer, and an outbox contract — justified by the prompt-injection adversary model, but much of it is not yet wired end to end, and the complexity bill arrives before the protection does.
- Hook-enforced caps are honor-system-adjacent. The docs admit that a missing hook means no enforcement;
cap-approveand the CLI-flag caps are only as strong as the hook layer, unlike the kernel-backed groups. The enforcement-class labels make this auditable; they do not make it go away. - The flagship claim outruns the deployment. “An agent literally cannot commit” is proven inside container tests, not on a running fleet. Both per-agent-users-everywhere and confinement-everywhere are draft.
- The agent-to-agent gate is coarse. One
cap-approvegroup answers “is this a high-privilege agent.” A finer token-based scheme was proposed during the automatt-era security review, judged a stretch, and deferred rather than rejected — the promotion path is an acknowledged open item. - Expiry-as-denied can silently stall work if nobody is watching the inbox; the mitigation is a warning-severity event and monitor aging, which is thinner than one would like.
- At-least-once delivery pushes idempotency work everywhere — dedup keys, one-shot token consumption, provider-receipt reconciliation — substantial machinery whose only job is making sure a redelivered event doesn’t send two emails.
- Superseded scars remain: per-section config gating retired as unenforceable, human multi-tenancy dropped outright (2026-06-20; one human, project namespaces as the only tenancy axis), and resource quotas punted to ops cgroup governance — the sandbox gates reach, not CPU or memory.