Skip to Content
ReferencePolicy linter

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 else

The 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/:

CheckFails when
Directory name is a valid rule IDIt does not match <namespace>.<name> (lowercase letters, digits, underscores, one dot). Later checks for that rule are skipped.
Namespace is not reservedThe namespace is argocd, constitution, posture, practice, proposal, or trustacks.
LengthThe rule ID is longer than 64 characters.
Rule file existsrules/<id>/<id>.rego is missing.
Test file existsrules/<id>/<id>_test.rego is missing.
Package declaredThe rule file has no package line.
Metadata presentThe rule file never mentions rule_metadata. Without it the rule is invisible to the rule inventory and to agent citations.
Metadata key matchesThe 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):

CheckFails 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.overlayA .json, .yaml or .yml file would load outside data.overlay.
No symlinksAny symlink under rules/.
Allowed file typesAny 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:

CheckFails when
Required fields declareddescription, 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.
kindIt is not structural or behavior.
practice_dimensionsA behavior rule names no practice, or a structural rule names some.
tier_scopeIt is not ["any"] or a list of dev, uat and prod, or it mixes "any" with tiers.
target_kindsIt is not ["any"] or a list of target kinds (lowercase identifiers such as kubernetes or ecs-fargate), or it mixes "any" with kinds.
subjectsWhen present, it is not a non-empty list of proposal and repo.
description, required_tooling_categoriesNot 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 runs opa test.
  • Whether tests exist beyond the file. A test file with one trivial test passes lint. The scaffold from trustacks rule new ships 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

Last updated on