Policy linter
trustacks rule lint is a fast shape check on the overlay rules in a local
checkout. It catches the mistakes that stop a rule from loading or from
showing up in the rule inventory. It is not a proof system.
trustacks rule lint # every rule under rules/
trustacks rule lint acme.requires_runbook # one rule
trustacks rule lint --overlay ../my-overlay # a checkout somewhere elseThe overlay path defaults to the current directory and must contain
environment-profile.yaml at its root. Exit code is 0 when clean, 1 when
any issue is found.
What it checks
For each directory under rules/:
| Check | Fails when |
|---|---|
| Directory name is a valid rule ID | It does not match <namespace>.<name> (lowercase letters, digits, underscores, one dot). Later checks for that rule are skipped. |
| Namespace is not reserved | The namespace is argocd, constitution, posture, practice, proposal, or trustacks. |
| Length | The rule ID is longer than 64 characters. |
| Rule file exists | rules/<id>/<id>.rego is missing. |
| Test file exists | rules/<id>/<id>_test.rego is missing. |
| Package declared | The rule file has no package line. |
| Metadata present | The rule file never mentions rule_metadata. Without it the rule is invisible to the rule inventory and to agent citations. |
| Metadata key matches | The rule file never contains the rule ID as a quoted string, so the rule_metadata key probably does not match the directory name. |
Then it runs opa check over the scoped rule, or over all of rules/ when
unscoped, and reports any parse or compile error.
Last, it runs the overlay confinement check over rules/, the same check
the runner makes before it evaluates an overlay (scoped runs report only the
named rule’s findings):
| Check | Fails when |
|---|---|
Package is under overlay. | A .rego file (tests included) declares a package that is not overlay.<name>, or OPA’s parser reports no package for it. |
Data stays in data.overlay | A .json, .yaml or .yml file would load outside data.overlay. |
| No symlinks | Any symlink under rules/. |
| Allowed file types | Any file other than Rego, JSON/YAML data, a root .manifest, or .md / .txt docs. |
The full rules, and what the runner does with an overlay that breaks them, are in the constitution reference.
Then it checks each rule’s rule_metadata against the
schema, reading it the way OPA
evaluates it:
| Check | Fails when |
|---|---|
| Required fields declared | description, kind, required_tooling_categories, tier_scope or target_kinds is missing. The runner still loads an existing rule like this, with a warning; lint refuses it so a new rule is complete. |
kind | It is not structural or behavior. |
practice_dimensions | A behavior rule names no practice, or a structural rule names some. |
tier_scope | It is not ["any"] or a list of dev, uat and prod, or it mixes "any" with tiers. |
target_kinds | It is not ["any"] or a list of target kinds (lowercase identifiers such as kubernetes or ecs-fargate), or it mixes "any" with kinds. |
subjects | When present, it is not a non-empty list of proposal and repo. |
description, required_tooling_categories | Not a non-empty string, or not a list of strings. |
A value outside the schema is also what makes the runner refuse your whole
rules/ tree, so lint is where to find it.
The naming checks are the rule-naming standard.
If opa is not on your PATH, lint prints a warning and skips the
opa check pass, the package part of the confinement check and the
rule metadata check. The file
checks still run. Install OPA so a clean lint
means what it says.
What it does not check
- Whether tests pass. Run
trustacks rule test, which runsopa test. - Whether tests exist beyond the file. A test file with one trivial test
passes lint. The scaffold from
trustacks rule newships exactly that: a baseline test and a TODO for the real one. - Whether your rule is correct or useful. A rule that denies everything lints clean.
- Signatures. Lint never touches a registry or a key.
- Refinement against the constitution. Lint does not compare your rule with constitution rules. It does not need to; see below.
trustacks rule sign does not run the full lint. It checks every rule
ID against the naming standard and runs the confinement check, and refuses
to sign an overlay that reaches outside data.overlay. It does not run the
other lint checks or your tests. Run lint and test yourself first, or
in CI.
Why an overlay cannot weaken the constitution
Overlay denies are added to the constitution’s denies. The runner asks one
question, data.proposal.deny, and gets the union of both. An overlay rule
can add a reason to refuse a proposal. It has no way to remove one, because
there is nothing to override: no allow rule, no waiver, no exception list.
That only holds if overlay rules stay in their own namespace. A file that
declared package proposal could redefine a constitution helper and switch a
rule off. So from runner 0.2.14 the runner enforces it: an overlay that
reaches outside data.overlay gets no verdict, and the run fails with
overlay rejected — overlay rules may only add to data.overlay. Earlier
runners did not check. Lint and rule sign run the same confinement check so
you find out first.
The linter still does not verify refinement, signatures, citations, or waivers. It does none of those.
In CI
The CLI ships inside the runner image, with opa alongside it. There is no
separate CLI package. Run it from the image against your checkout:
docker run --rm --user "$(id -u):$(id -g)" \
-v "$PWD:/work" -w /work \
ghcr.io/trustacks/runner:<version> \
sh -c 'trustacks rule lint && trustacks rule test'Use the runner version your workspace runs. The same command works as a CI step. See Rule CLI.
Where to go next
- Rule CLI ·
rule new,test,lint,sign - Rule-naming standard · the ID grammar
- Constitution · the rules your overlay adds to