inherit: never denials propagate upward + an override keyword for conscious upward escalation (fail-closed tiering) #169

Closed
opened 2026-06-25 17:19:29 +00:00 by coilysiren · 4 comments
Owner

Model settled 2026-06-25 (Kai), after two reframes. Final shape: the least-privileged base explicitly nevers dangerous actions, those denials inherit upward, and the most-privileged tier uses an override keyword to grant itself specific denied actions by name. Deny low, override high. (An earlier edit wrongly removed override - it is required; it is the sole construct that crosses an inherited never.)

Context

ward is migrating its whole ward-kdl ops surface to permission tiers - ward-kdl-read ⊂ ward-kdl-write ⊂ ward-kdl-admin, one standalone binary per tier, loaded by context (read in CI containers, write in warded feature containers, admin native). forgejo already runs this chain via inherit (cli-guard#160). The full migration (ward epic ward#339) extends it to every area.

Posture: fail-closed, build-up. read is the least-privileged base; the dangerous capabilities are denied there and only the trusted admin tier re-grants them, by name.

The model (what the engine must guarantee)

1. Everything inherits upward, automatically

A tier under inherit automatically carries every rule of the tier it inherits - can, never, restrict, and the spec/base-url/auth singletons - and re-declares nothing. This supersedes the child-local restrict of cli-guard#160: forgejo's tiers restate restrict owner matches coily* today only because restrict does not inherit; under this model that boilerplate goes away.

2. The least-privileged base explicitly nevers dangerous actions

read does not merely omit delete - it declares never delete (and never fork, never archive, the repo-label denials, etc.). Deny-by-default is made explicit at the most-exposed tier, so the dangerous capability is actively forbidden where the binary is least trusted, and a later careless can cannot silently re-open it (see precedence). These nevers inherit upward, so they hold at write and admin too - unless a tier overrides one (below).

3. override: the sole way a higher tier crosses an inherited never

The most-privileged tier re-grants a specific denied action by name:

// in the admin tier
override can delete repo      // lifts the inherited `never delete` for repo ONLY
override can delete milestone
  • Only override crosses a never. A plain can never does (precedence, below).
  • It re-grants exactly the verb+resource named. Non-overridden siblings stay denied: with never delete * in read and only override can delete repo in admin, delete issue is still refused at every tier.
  • Every escalation is therefore enumerated and reviewable in one place - the higher tier's guardfile is the complete list of dangerous capabilities that tier holds.

4. Precedence (the load-bearing rule)

An inherited never beats a plain can. The only construct that beats an inherited never is an override in a higher tier naming the exact verb+resource.

This makes wildcard grants safe: a tier may write can delete * or can create * and the inherited never delete issue / never create label still block their carve-outs. Deny is sticky upward; nothing but a named override reverses it.

Worked example (forgejo, the ward#278 surface as base-nevers + admin-overrides)

  • read: the reads, plus never delete *, never fork repo, never archive repo, never unarchive repo, never create org, never delete org, the repo-label denials (never create label, never edit label), never delete issue, never view pr, never list pr.
  • write: inherits read, adds create/edit (curated). The inherited never create label still beats a broad can create - no override, so it stays denied.
  • admin: inherits write, adds override can delete repo, override can delete milestone, override can delete release, override can delete org-label - and only those. delete issue, delete org are never overridden, so they stay denied at admin. That is exactly the curated forgejo policy, now expressed as deny-low + override-high.

Acceptance

  • Under inherit, a tier inherits all rules with no re-declaration, proven by a test where a base restrict/never takes effect in an un-restating higher tier.
  • A plain can (including wildcard can <verb> *) cannot cross an inherited never: test can delete * at a tier + inherited never delete issue => delete issue still refused.
  • override can <verb> <resource> does cross the inherited never, for that resource only: test override can delete repo grants repo delete while inherited never delete * keeps delete issue refused.
  • An override is rejected at build time if there is no inherited never it lifts (no silent no-op overrides), and a plain can that would need to cross a never is a build error pointing the author at override.
  • inherit doc (cli-guard#160) gains a "what inherits, precedence, and override" section and notes the restrict-now-inherits change.

Security note

Fail-open-risk change to the enforcement engine - the precedence rule plus override are the load-bearing parts. The whole point is that nothing except a named override can re-open a base never. Recommend it land with review, not fire-and-forget to main.

References

ward#339 (the tier-migration epic), ward#278 (forgejo port - authors against this model), ward#240 (the chain), cli-guard#160 (inherit + the child-local restrict this supersedes), cli-guard#159 (wildcard grants - safe under the precedence rule).

> **Model settled 2026-06-25 (Kai), after two reframes.** Final shape: the least-privileged base **explicitly `never`s** dangerous actions, those denials **inherit upward**, and the most-privileged tier uses an **`override` keyword** to grant itself specific denied actions by name. Deny low, override high. (An earlier edit wrongly removed `override` - it is required; it is the sole construct that crosses an inherited `never`.) ## Context ward is migrating its whole `ward-kdl` ops surface to **permission tiers** - `ward-kdl-read ⊂ ward-kdl-write ⊂ ward-kdl-admin`, one standalone binary per tier, loaded by context (read in CI containers, write in warded feature containers, admin native). forgejo already runs this chain via `inherit` (cli-guard#160). The full migration (ward epic ward#339) extends it to every area. Posture: **fail-closed, build-up.** read is the least-privileged base; the dangerous capabilities are denied there and only the trusted admin tier re-grants them, by name. ## The model (what the engine must guarantee) ### 1. Everything inherits upward, automatically A tier under `inherit` automatically carries **every** rule of the tier it inherits - `can`, `never`, `restrict`, and the `spec`/`base-url`/`auth` singletons - and **re-declares nothing**. This **supersedes the child-local `restrict`** of cli-guard#160: forgejo's tiers restate `restrict owner matches coily*` today only because `restrict` does not inherit; under this model that boilerplate goes away. ### 2. The least-privileged base explicitly `never`s dangerous actions read does not merely **omit** delete - it declares `never delete` (and `never fork`, `never archive`, the repo-label denials, etc.). Deny-by-default is made **explicit at the most-exposed tier**, so the dangerous capability is actively forbidden where the binary is least trusted, and a later careless `can` cannot silently re-open it (see precedence). These `never`s inherit upward, so they hold at write and admin too - unless a tier `override`s one (below). ### 3. `override`: the sole way a higher tier crosses an inherited `never` The most-privileged tier re-grants a specific denied action **by name**: ``` // in the admin tier override can delete repo // lifts the inherited `never delete` for repo ONLY override can delete milestone ``` - **Only `override` crosses a `never`.** A plain `can` never does (precedence, below). - It re-grants **exactly** the verb+resource named. Non-overridden siblings stay denied: with `never delete *` in read and only `override can delete repo` in admin, `delete issue` is still refused at every tier. - Every escalation is therefore **enumerated and reviewable in one place** - the higher tier's guardfile is the complete list of dangerous capabilities that tier holds. ### 4. Precedence (the load-bearing rule) **An inherited `never` beats a plain `can`. The only construct that beats an inherited `never` is an `override` in a higher tier naming the exact verb+resource.** This makes wildcard grants safe: a tier may write `can delete *` or `can create *` and the inherited `never delete issue` / `never create label` still block their carve-outs. Deny is sticky upward; nothing but a named `override` reverses it. ## Worked example (forgejo, the ward#278 surface as base-nevers + admin-overrides) - **read**: the reads, plus `never delete *`, `never fork repo`, `never archive repo`, `never unarchive repo`, `never create org`, `never delete org`, the repo-label denials (`never create label`, `never edit label`), `never delete issue`, `never view pr`, `never list pr`. - **write**: inherits read, adds `create`/`edit` (curated). The inherited `never create label` still beats a broad `can create` - no override, so it stays denied. - **admin**: inherits write, adds `override can delete repo`, `override can delete milestone`, `override can delete release`, `override can delete org-label` - and **only** those. `delete issue`, `delete org` are never overridden, so they stay denied at admin. That is exactly the curated forgejo policy, now expressed as deny-low + override-high. ## Acceptance - Under `inherit`, a tier inherits **all** rules with **no re-declaration**, proven by a test where a base `restrict`/`never` takes effect in an un-restating higher tier. - A plain `can` (including wildcard `can <verb> *`) **cannot** cross an inherited `never`: test `can delete *` at a tier + inherited `never delete issue` => delete issue still refused. - `override can <verb> <resource>` **does** cross the inherited `never`, for that resource only: test `override can delete repo` grants repo delete while inherited `never delete *` keeps `delete issue` refused. - An `override` is rejected at build time if there is no inherited `never` it lifts (no silent no-op overrides), and a plain `can` that would need to cross a `never` is a build error pointing the author at `override`. - inherit doc (cli-guard#160) gains a "what inherits, precedence, and `override`" section and notes the `restrict`-now-inherits change. ## Security note Fail-open-risk change to the enforcement engine - the precedence rule plus `override` are the load-bearing parts. The whole point is that nothing **except** a named `override` can re-open a base `never`. Recommend it land with review, not fire-and-forget to main. ## References ward#339 (the tier-migration epic), ward#278 (forgejo port - authors against this model), ward#240 (the chain), cli-guard#160 (`inherit` + the child-local `restrict` this supersedes), cli-guard#159 (wildcard grants - safe under the precedence rule).
Author
Owner

Tracked under the ward tier-migration epic ward#339. Engine prerequisite for every area that selectively escalates past a broad base denial (forgejo: allow delete repo, keep never delete issue - ward#278). Security-sensitive: recommend review, not fire-and-forget to main.

Tracked under the ward tier-migration epic ward#339. Engine prerequisite for every area that selectively escalates past a broad base denial (forgejo: allow delete repo, keep never delete issue - ward#278). Security-sensitive: recommend review, not fire-and-forget to main.
Author
Owner

everything inherits upwards, automatically

*everything* inherits upwards, automatically
Author
Owner

🔒 Reserved by ward agent --driver claude — container ward-cli-guard-issue-169-claude-593e4f07 on host 4f2062d4fed1 is carrying this issue (reserved 2026-06-25T18:16:36Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `ward-cli-guard-issue-169-claude-593e4f07` on host `4f2062d4fed1` is carrying this issue (reserved 2026-06-25T18:16:36Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

Retro from the container that built this 🐾

The engine work landed faster than I expected — the existing inherit flatten + deny-wins machinery was already shaped right, so this was mostly adding override as a can-with-a-flag and teaching three resolve sites to let it cross a deny.

The part that actually fought back was precedence. There were existing same-tier can+never tests that expect a silent deny-wins, but #169 wants an explicit can over an inherited never to be a loud build error. Reconciling those nearly pushed me toward plumbing provenance through the typed model — until I realized inherited-vs-local is only knowable at the flatten/merge seam, and the wildcard-coverage check (never delete "*" covers can delete repo) is purely textual, no spec needed. Doing the validation right there kept it clean and left every pre-existing test untouched. One genuine bug I caught mid-build: a tier that restates an inherited never deduped it out of the prefix and made a legit override look like a no-op — fixed by validating against all parent denies, not the deduped set.

Honestly the pre-commit gate was the bigger slog than the feature: golangci-lint and trufflehog weren't in the container, the 80-line/4000-char doc cap forced splitting out specverb-override.md, and the ≤2-line comment rule meant trimming ~25 comments.

Confidence is high — acceptance criteria each have a test, build errors included. Rough edges worth a follow-up: a degenerate same-file never+override is allowed a bit loosely, and the real stress test is ward#278 porting the full forgejo surface onto deny-low + override-high. (Note: the 3 cli/sandbox seccomp test failures are pre-existing and environmental — this linuxkit box can't enforce seccomp; unrelated to this change.)

Retro from the container that built this 🐾 The engine work landed faster than I expected — the existing `inherit` flatten + deny-wins machinery was already shaped right, so this was mostly adding `override` as a `can`-with-a-flag and teaching three resolve sites to let it cross a deny. The part that actually fought back was precedence. There were existing same-tier `can`+`never` tests that expect a *silent* deny-wins, but #169 wants an explicit `can` over an **inherited** `never` to be a loud build error. Reconciling those nearly pushed me toward plumbing provenance through the typed model — until I realized inherited-vs-local is only knowable at the flatten/merge seam, and the wildcard-coverage check (`never delete "*"` covers `can delete repo`) is purely textual, no spec needed. Doing the validation right there kept it clean and left every pre-existing test untouched. One genuine bug I caught mid-build: a tier that *restates* an inherited `never` deduped it out of the prefix and made a legit override look like a no-op — fixed by validating against all parent denies, not the deduped set. Honestly the pre-commit gate was the bigger slog than the feature: golangci-lint and trufflehog weren't in the container, the 80-line/4000-char doc cap forced splitting out `specverb-override.md`, and the ≤2-line comment rule meant trimming ~25 comments. Confidence is high — acceptance criteria each have a test, build errors included. Rough edges worth a follow-up: a degenerate same-file `never`+`override` is allowed a bit loosely, and the real stress test is ward#278 porting the full forgejo surface onto deny-low + override-high. (Note: the 3 `cli/sandbox` seccomp test failures are pre-existing and environmental — this linuxkit box can't enforce seccomp; unrelated to this change.)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/cli-guard#169
No description provided.