ADR 0012: Policy authority

Status: proposed, 2026-10-07

Context

The site says a workflow file declares who may do what: policy: is a top-level block in ship.ks, its subjects are agents and humans, and each takes can: / before: / never: tuples of actions <verb> [<env>] plus ask: handles (docs/LANGUAGE.md:218-237). The nine verbs — check, build, test, plan, apply, deploy, destroy, publish, read — are the action classes, the same vocabulary ADR 0100 forward-references as ActionClass (0100:32-34).

The problem is that ship.ks is the file coding agents can write. An agent that can add a step can edit the file the step's own policy is read from: it can move apply from ask: to can:, delete never: read secrets, or lower a threshold. Nothing in the language says otherwise — the policy lives inside the thing it governs.

What is enforced today is smaller than the block suggests. pub struct Policy (crates/core/src/workflow.rs:94) holds one field, a private never: Vec<NeverRule> (:95), and its own doc comment says so (:88-92): "The workflow's policy, as far as static checks enforce it: the never: rules a declared subject must respect. The checker enforces the agents' read secrets rule against agent runs (#27); nothing else is refused yet." check_policy (crates/core/src/check.rs:1314, dispatched from :294) records the rules it reads — forbid (check.rs:1458) builds a NeverRule and pushes it, it does not refuse anything. enforce_actor_policy (check.rs:1473) is the only enforcement site in the repository; it is hardcoded to the single rule (Agent, "read", "secrets") and is called once, from finish_all (check.rs:1513). can:, before: and ask: are parsed and validated — an undeclared environment is an error — and grant nothing and gate nothing.

The rest of the surface is a diagram and a name. PolicyStore appears exactly twice elsewhere in the repository — both on the ports line of one ASCII picture, at docs/ARCHITECTURE.md:15 and README.md:24 — and in no Rust file at all. There is no policy.rs under crates/core/src/ports/. The CLI has three subcommands and no fourth (pub enum Command { Check, Run, BlocksSearch }, crates/cli/src/args.rs:29-34), so there is no keepshipping policy explain subcommand at all, and --actor human|agent|ci is the only lever an operator has over policy today: check and run both take it, since run threads the flag into check::load (crates/cli/src/main.rs:58) and an agent run is refused by that same path. And crates/engine/src/lib.rs:1 mentions policy only in its crate doc comment; there is no policy code in crates/engine/src/.

The options, briefly:

This ADR records the decision #13 raised.

Decision

Option B for the baseline, option C for the in-file half. The effective policy is the org baseline intersected with the policy: block as it stands on the default branch, and the in-file block may only narrow what the baseline allows.

Why not the other two

Before acceptance

The parts of this ADR that can be true today are decided; the parts that need code cannot be, because PolicyStore is unimplemented and can: / before: / ask: are inert. A baseline with nothing to narrow is a baseline with no teeth, so this ADR stays proposed until the first adapter and the intersection exist. It becomes accepted once:

  1. crates/core/src/ports/policy.rs exists with the baseline read port, and Policy (crates/core/src/workflow.rs:94) can be intersected with it in a test that starts from a baseline and a file that tries to widen it.

  2. A file that moves an action from ask: to can: produces a check warning naming the baseline rule it would widen, and a run against the branch's own file is refused where the default branch's file would be refused.

  3. A pull request touching policy: is detected as such — the warning exists in check, and the approval is a required check in a CI configuration the repository documents.

  4. keepshipping policy explain prints the effective policy with each rule attributed to baseline, default branch or file, and with the "no baseline configured" case stated in its first line.

  5. With no baseline configured, check warns, explain says so loudly, and the effective policy equals the file's own policy — the behaviour this ADR chose, asserted by a test rather than assumed.

Consequences