Skip to content
74 changes: 74 additions & 0 deletions governance/ENFORCEMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Enforcement

Every rule in `constitution.md` has a row here. A row records:

- **Mode**: test, agent or human (see the constitution's "How rules are
enforced").
- **Check**: the CI check, plus a yes/no review question for any part a test
cannot reach.
- **Today**: what the check finds on `main`, measured. The measurements below
were taken on `f9bfd08` (2026-10-12).
- **Status**.

Status values:

- `enforced`: the check runs in CI on every pull request, and a pull request
built to violate it was shown to fail. The row links that demonstration.
- `running`: the check runs in CI, but no failure demonstration is recorded
yet.
- `dormant`: the check runs but asserts nothing until a named precondition is
met. It becomes `enforced` on its own when the precondition holds.
- `planned`: agreed, not yet written. The row names the milestone it lands in.
A row planned for two milestones is either built or struck.
- `unbuilt`: no check exists, and nobody has started one.
- `manual`: enforced by agent or human review only, with the review question
and the owner named.

A new check starts from today's findings as an allow-list. Each entry links an
issue, and the list can only shrink (A1).

**No check blocks a merge yet.** `main` requires a pull request, but no status
checks and no approvals, and `enforce_admins` is off (see ENF-R1). Until R1 is
in place, every row below is visible in CI but does not stop a merge.

| ID | Mode | Check | Today | Status |
|----|------|-------|-------|--------|
| ENF-S1 | test + human | Planned: a committed inventory of every `exec` command, plugin step type and `gh_api` method in a shipped framework, each recorded as reading setup. A step not in the inventory fails. `gh_api` steps must be GET. Adding a row needs maintainer review against the question "Could this step's answer change while nothing in the repository or its platform settings changes (tool version, network, time, history)?" | About 15-20 rows. 5 outcome uses: `zizmor` ×2 (BR-01.01, AC-04.02), `gittuf_verify_policy`, `gittuf_commits_signed`, `repro_witness_attestation`. 2 borderline: `repro_hermetic_build`, `repro_bit_for_bit`. These are cut or reframed per ADR-0001. | planned: scope-reset milestone |
| ENF-S2 | test | CI `Test` job: `tests/darnit/tools/test_compliance_errors.py`, `tests/darnit/assertions/test_compliance_effects.py`, `tests/darnit/harness/test_harness_compliance.py`, `tests/darnit/config/test_strict_framework_loading.py`, and the adversarial corpus gate (`tests/darnit_baseline/corpus/`, including `plugin-redefines-manual`). Planned: a test that every shipped `promotion` matches corpus eligibility. | Known gaps from the wiring audit: the shared-handler cache, list-valued `when` (N/A), `gh_api`/`mcp` truthiness, a plugin registering without its plugin context, a dropped control leaving its level compliant. 0 promotions are shipped. | running (only the corpus gate has a failure demonstration) |
| ENF-S3 | test | CI `Test` job: `tests/darnit/context_integrity/`. | Known gap: raw `.project/project.yaml` values satisfy a remediation `when` guard, and the `project.*` template namespace is unguarded. | running |
| ENF-S4 | test | CI `Test` job: `tests/darnit/context_integrity/test_single_writer.py`, `test_user_files_survive.py`. Planned: an allow-list of filesystem write sites in `packages/*/src`, so a new write site outside the executor fails. | Write sites outside the executor are reachable from remediation: `darnit_baseline/threat_model/remediation.py` (5 sites; overwrites uncommitted user files), `remediation/helpers.py:53`, `darnit_baseline/tools.py:1144-1145`. | running, with known gaps |
| ENF-S5 | test | CI `Test` job: `tests/darnit/config/operator/test_repository_cannot_configure.py`, `test_baseline_toml_removed.py`, `tests/darnit/trust/`. | Known gap: `remediate_audit_findings` builds its trust target from the checkout's origin remote. | running, with known gaps |
| ENF-F1 | test | Planned: an AST check over `packages/darnit/src` that fails on an import of an implementation package, a framework-name literal outside the one default constant, and a `register_control` call outside the control loader. | 0 imports. 5 framework-name literals in 4 files: `agent/graph.py:402,416`, `cli.py:504`, `config/merger.py:691`, `config/context_schema.py:350`. `register_control` is called only by the loader, but CLAUDE.md shows it as a plugin API. | planned: first checks milestone |
| ENF-F2 | test | Today: tier-1 parity tests (`tests/darnit/parity/tier1/`) compare `audit_openssf_baseline` with `HarnessRun` only. Planned: a parity matrix of entry points (CLI, MCP, builtin, harness, `darnit run`) × options over the parity fixtures, plus an AST check that profile, tags and default-framework resolution happen in one module. | Profile resolution happens in 3 places and is absent from the CLI; tags are parsed in 3 places; the default framework is set in 4. CLI `audit` has no `--level`; MCP audit has no include/exclude; the harness has no tags or profile. | planned: second checks milestone |
| ENF-F3 | test + agent | Planned, three ratchets:<br>(1) Every MCP tool parameter and CLI flag is read (AST); configuration fields later.<br>(2) Whole-repo mypy against a baseline generated in CI's environment. Any `attr-defined`, `call-arg` or `name-defined` error fails outright.<br>(3) The set of shipped functions with no executed line, from CI's coverage run, may only shrink. Protocol stubs and test-fixture packages are excluded.<br>Agent question: "Does a test reach each new or changed default implementation through a real entry point?" | (1) 6 unread: `auto_init_config`, `template`, `get_pending_data` `level` and `profile`, `--profile` on `audit` and `plan`. (2) 201 errors, 21 in the always-fail tier. (3) 101 functions. Today's CI `Type Check` job checks 2 files with `\|\| true` and gates nothing. | planned: first checks milestone |
| ENF-F4 | test | CI `Non-dev install smoke` job (#575, extended by #596): builds every published wheel, installs it without development dependencies, runs one audit, and fails on a control that could not be evaluated or on more than one wheel per package. | Passes on `main`. Shown to fail when `packaging` is removed from darnit-core (#575 review). Plugins loaded only through registration (gittuf, reproducibility) are not imported in full. | running (a failure demonstration exists in review; link it from a closed pull request to mark `enforced`) |
| ENF-C1 | test + human | Planned: committed snapshots of each contract surface: MCP tool schemas, the argparse tree, `model_json_schema()` of both configuration schemas, and JSON/SARIF/predicate output for one fixture. A removal or rename in a snapshot diff requires a BREAKING line. Human question: "Did the ADR or spec that decides this change land with or before it?" | Today's `Spec Sync` check finds 8 hard-coded handler names in a document and enforces almost nothing. | manual (maintainers); snapshots planned |
| ENF-R1 | test + human | `main` branch protection:<br>- required checks `Test`, `Lint`, `Spec-Implementation Sync`, `Build`, `DCO` and `agent-review`;<br>- required code-owner review, with CODEOWNERS listing only the owned paths, so a pull request touching them needs an owner other than its author;<br>- administrators included.<br><br>The `agent-review` check runs a fresh agent session that the author cannot steer. It has its own identity, takes the constitution's review questions as input, treats pull request text as untrusted, and reads no secrets from a fork's code. It passes when it raises no blocking finding, or when a maintainer other than the author has approved the pull request.<br><br>Human part: a weekly digest of pull requests merged without a second person goes to the maintainers, and anything doubtful becomes an issue.<br><br>Planned:<br>- a scheduled job that fails when protection drifts from this declaration;<br>- a test that the CI test command has no marker, `-k`, `--deselect` or `--ignore` selection;<br>- a hermetic job that runs the suite with an empty HOME, CI variables set and the network blocked, and fails if anything is written under HOME. | Protection requires a PR only: 0 approvals, no required checks, `enforce_admins` off. No `agent-review` check exists. CODEOWNERS owns everything (`*`) and names only one maintainer. CI has run the whole suite since #592. With an empty HOME, the suite writes 377 files there, including `~/.darnit/verification_cache`, a pip cache, tree-sitter grammars and sigstore TUF metadata (a network fetch). | unbuilt (settings change pending) |
| ENF-A1 | test + agent | Planned: a committed file of ratchet counts and sets, compared on every pull request:<br>- suppressions by kind;<br>- the mypy baseline;<br>- the unexecuted-function set;<br>- reader allow-lists;<br>- the inventory of Protocols, entry-point groups and registries;<br>- CI's test selection.<br>An increase fails unless the pull request links an issue and a maintainer approves.<br>Agent question: "Did this change weaken, skip or suppress anything to make a run pass?" | Baseline: 85 `# noqa` (47 of them `BLE001`), 54 `# type: ignore`, 41 test skips (`mark.skip`/`skipif`, `pytest.skip(`, `importorskip`), 5 `pragma: no cover`, 2 `continue-on-error` and 10 `\|\| true` in workflows and scripts, 3 ruff per-file-ignores. 17 extension points (9 Protocols, 3 entry-point groups, 5 registries). | planned: first checks milestone |
| ENF-A2 | test + human | `DCO` check (not yet required; it cannot tell an agent-added sign-off from a person's). Planned:<br>- a check that fails on an AI `Co-authored-by` or `Signed-off-by` line, on a PR body without exactly one AI-assistance box ticked, or on AI use declared without an `Assisted-by` trailer;<br>- a check that fails on a local home-directory path in a tracked file;<br>- an output test that an audit report contains no home path or user profile.<br>Human question: "Can the author explain this change?" | Since AI_POLICY landed (38 commits): 1 AI co-author line, 6 AI-disclosed PRs without `Assisted-by`, 6 PRs with no AI box ticked. 21 home-directory paths in tracked files, mostly Spec Kit `tasks.md`. | planned: first checks milestone |
| ENF-A3 | test + agent | CODEOWNERS entry for `governance/` naming the maintainers, and required code-owner review on `main`. Planned: `scripts/governance_lint.py`, which requires every MUST in `governance/` to carry an id, every id to have a row here, and every row to be cited by a rule. Agent question: "Did a maintainer ask for this change to `governance/`?" | CODEOWNERS lists only `@mlieberman85`, plus a stale `/main.py` line. Code-owner review is not required. | unbuilt |

## Build order

1. ENF-R1 settings (no code) and a CODEOWNERS pull request that owns only
the expensive paths and names both maintainers. Nothing gates until these
land. Until the `agent-review` check exists, R1 falls back to a second
maintainer's approval on every pull request.
2. ENF-F3 (1), the reader check for MCP parameters and CLI flags, starting
with 6 allow-list entries linked to #528, #541 and #576.
3. ENF-F3 (2), the mypy gate, replacing today's `Type Check` job.
4. ENF-F1, the AST check, either fixing the 5 framework-name literals or
starting with them allow-listed.
5. ENF-A2, the attribution and home-path checks.
6. ENF-A1, the ratchet file.
7. ENF-F4: record the failure demonstration (#575 merged).
8. ENF-R1's hermetic job. The writes to HOME are fixed first, or allow-listed.
9. ENF-F2, the parity matrix and the single-resolver check.
10. ENF-F3 (3), the coverage ratchet.
11. ENF-C1, the contract snapshots.
12. ENF-S1, the inventory, after the scope-reset classification.
13. ENF-A3, governance_lint.

Prove each new check: open a pull request built to violate it, watch the check
fail, close that pull request unmerged, and link it from the row. That link is
what moves a row from `running` to `enforced`.
146 changes: 146 additions & 0 deletions governance/constitution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Constitution

These are the non-negotiable rules for this repository. Every rule carries an
enforcement id that maps to a row in `ENFORCEMENT.md`. Each row says how the
rule is enforced and how far that enforcement reaches today.

Changing a rule requires an ADR in `decisions/`. Do not edit a rule in place
without one.

This document is deliberately short. Every agent session loads it, so each
rule costs context in every session, forever. Before adding a rule, read
`where-decisions-live.md`: most decisions belong somewhere else.

What darnit is, and is not, is decided in
[ADR-0001](decisions/0001-what-darnit-is.md). This document sits under the
project's [Technical Charter](../CHARTER.md) and
[GOVERNANCE.md](../GOVERNANCE.md), which decide who decides. This document
decides what a change must satisfy.

## How rules are enforced

Every rule names one or more enforcement modes. A deterministic check is
always preferred.

- **test**: a check in CI that fails on a violation, and has been shown to.
- **agent**: the rule is in every agent's instructions (`AGENTS.md`), and the
agent applies it while writing and reviewing, using the yes/no question in
the rule's row.
- **human**: a maintainer applies it in review, and is accountable for it.

A rule may rely on agent or human enforcement alone only if its row says why a
test is impossible or disproportionate, gives the yes/no question a reviewer
answers from the diff, and names an owner. A rule enforced that way that
catches nothing for two milestones is reviewed for deletion. A rule whose
check cannot fail is not enforced.

---

## 1. Scope and correctness

**S1.** A control's result MUST depend only on the audited project's setup:
its files, its CI configuration, its platform settings (read, never changed by
a check) and its confirmed project data. The same setup MUST give the same
result on any machine, at any time, with any tool version darnit supports. A
PASS states that a practice is set up, never that its outcome holds: for
example, that gittuf is set up, not that history satisfies the policy; that a
scanner runs in CI, not that its findings are clean. `[ENF-S1]`

**S2.** No false PASS. A step MUST conclude only an outcome in its effective
set: its registered ceiling, narrowed by `concludes`, widened only by a
promotion that the corpus backs with zero false PASS results. A model judgment
MUST NOT conclude PASS. No plugin MAY replace a core step type. A level is
compliant only when every applicable control at that level is PASS. WARN,
ERROR, PENDING, an unhonored claim and a control that failed to load all count
against it. `[ENF-S2]`

**S3.** darnit MUST NOT conclude a value that needs a person's judgment
(`auto_detect = false`). Detection may propose a candidate. Only a person's
confirmation makes a value usable, and an unusable value MUST NOT reach a
verdict, a remediation input, an attestation or a write. `[ENF-S3]`

**S4.** Remediation MUST write only through the remediation executor, MUST
record every write in the run manifest, and MUST NOT write a path with
uncommitted user changes. `[ENF-S4]`

**S5.** The audited repository is untrusted input. Nothing in it, including its
own configuration files and its version-control remotes, configures darnit or
grants trust. Trust and tool configuration come only from operator
configuration or verified CI metadata. `[ENF-S5]`

---

## 2. Framework shape

**F1.** The core (`packages/darnit`) MUST NOT import or name an implementation
package or a framework. The default framework is one named constant. Controls
are data: only a framework's TOML defines them, and Python adds step types,
remediation handlers and tools. `[ENF-F1]`

**F2.** There is one execution path. Every entry point that audits or
remediates (the CLI, each framework's MCP tools, the builtin tools, the
harness, `darnit run`) MUST resolve framework, level, tags, profile,
include/exclude and output format through one shared resolver, and MUST give
the same controls and statuses on the parity fixtures. `[ENF-F2]`

**F3.** Shipped code can run. Everything darnit accepts MUST have a production
reader: every CLI flag, MCP tool parameter, step setting and configuration
field. Every function in a shipped package MUST be executed by a test, and a
shipped default implementation of a Protocol by a test that reaches it through
a real entry point. A change MUST NOT add a type error. `[ENF-F3]`

**F4.** Each published package, installed from its built wheel without
development dependencies, MUST run an audit with no import error. `[ENF-F4]`

---

## 3. Contracts

**C1.** darnit's public contracts are: the CLI, the MCP tool surface, the
framework TOML schema, the operator configuration schema, the plugin protocol
and the output formats (JSON, SARIF, attestation predicate). A change to a
contract MUST be visible in review, MUST land with or after its ADR or feature
spec, and, when it breaks a user, MUST appear under a **BREAKING** entry in
`CHANGELOG.md`. Breaking changes also follow GOVERNANCE.md's RFC process.
`[ENF-C1]`

---

## 4. Process

**R1.** A change MUST reach `main` only through a pull request whose
required checks pass on its head. The required checks include:

- the whole test suite, run hermetically: no real HOME, no network, no
dependence on CI environment variables;
- an independent agent review.

A pull request that touches a path owned in CODEOWNERS MUST also be approved
by a code owner other than its author. The owned paths are the ones where a
wrong merge is expensive: `governance/`, the release and CI workflows, trust
and confirmation, the remediation executor and platform engine, and plugin
discovery. A pull request whose agent review raises a blocking finding needs
a maintainer other than the author to resolve it.

Administrators are not exempt. The one exception is a merge from a GitHub
security advisory, which is recorded in the advisory. `[ENF-R1]`

**A1.** Every ratchet only tightens: suppressions, skips, baselines,
allow-lists, the extension-point inventory and CI's test selection. Loosening
one requires a maintainer-approved pull request that links an issue. A
contributor (person or agent) who cannot satisfy a rule MUST stop and file an
issue that names the rule and the conflict, and MUST NOT weaken a check to
make a run pass. `[ENF-A1]`

**A2.** Contributions follow [AI_POLICY.md](../AI_POLICY.md). Committed files,
pull request and issue text, and darnit's own output MUST NOT contain local
filesystem paths, an auditor's personal data, credentials, or the names of
private projects. `[ENF-A2]`

**A3.** A change to `governance/` MUST be approved by a code owner of
`governance/`. Where the Charter gives the decision to the TSC (scope, and
contribution policy), the change MUST be approved by a majority of the TSC's
voting members, recorded as pull request approvals as GOVERNANCE.md
describes. Every MUST in `governance/` carries an enforcement id with a row in
`ENFORCEMENT.md`. An agent MUST NOT change `governance/` unless a maintainer
asked it to. `[ENF-A3]`
Loading
Loading