Organization policy¶
An organization policy is a floor a repository may raise but never lower. A security team writes it once; every
repository's ai-rulez validate and ai-rulez generate enforce it. Without a policy nothing changes.
The point of the design: a policy that lives in the repository it governs can be deleted by the pull request that violates it. So a policy is found only outside the repository (a flag, an environment variable, a managed path), never inside it. The repository can add restrictions; it cannot remove one.
Where a policy comes from¶
Layers, strongest anchor first. Every layer found is loaded and merged tighten-only (there is no "override").
| Layer | Source | Use |
|---|---|---|
flag |
--policy <file or https URL> |
a CI step the organization owns |
env |
AI_RULEZ_POLICY=<file or https URL> |
managed machines, CI images |
org |
the policy of the repository's GitHub owner, with --discover-org or [policy] discover = "org" in the user config |
a convenience for an organization's repositories (see below; not an anchor) |
managed |
/etc/ai-rulez/policy.toml (Linux and others), /Library/Application Support/ai-rulez/policy.toml (macOS), %ProgramData%\ai-rulez\policy.toml (Windows) |
a machine-wide baseline |
A flag or variable that is set but cannot be loaded is an error (AR742), never a skip: breaking the path must not
switch the policy off. An absent managed file is not an error; a present but unreadable or invalid one is. A policy
file must be a regular file of at most 256 KiB. The same file named twice is one layer.
Managed locations¶
The managed file is /etc/ai-rulez/policy.toml on Linux and other Unix systems,
/Library/Application Support/ai-rulez/policy.toml on macOS and %ProgramData%\ai-rulez\policy.toml on Windows
(C:\ProgramData when ProgramData is not an absolute path: a relative value would resolve against the repository
being evaluated). Make the file writable by administrators only. Apart from the ProgramData system variable on
Windows (keep it out of repository-controlled environment files), nothing moves it: a flag or variable a repository could
set would let it replace the managed anchor with an empty file. (The end-to-end tests build a binary with the location
moved by a link-time variable; a release binary never sets it.)
Policy URLs¶
A policy may be an https URL, so one file serves every repository. The rules are strict, because a URL is
content someone else controls:
- A digest is required. Pin it on the reference (
https://policy.example.org/base.toml@sha256:<hex>), with--policy-digestfor--policy, orAI_RULEZ_POLICY_DIGESTforAI_RULEZ_POLICY. The digest is the SHA-256 of the file with CRLF normalized to LF, the onevalidate --show-policyprints. A URL with no digest is not loaded (AR741). A digest that does not match isAR741and fails closed; a cached copy never papers over it. --policy-trust-tofurecords the digest of an unpinned URL once, and only in a terminal (a pipe or CI refuses), in the user cache with an HMAC. Later runs use the recorded digest. The warning it prints names the digest to pin.- https only, no credentials in the URL (
file:andhttp:are refused), no credentials sent, the response is at most 256 KiB, redirects stay on the same host over https (at most 5), and the content type is ignored. - Cache. A good copy is stored by URL and digest in the user cache (
~/.cache/ai-rulez/policy), with an HMAC under a per-user secret kept in the user config directory. An entry that fails its HMAC, or whose body no longer hashes to the pin, is a miss, so a checkout or restored cache cannot plant a policy. - Offline and
max_stale. When the URL cannot be reached (any network error or non-200 answer), the cached copy of the pinned digest stands in for it for at mostmax_stale(default7d:--policy-max-stale, orAI_RULEZ_POLICY_MAX_STALE;0allows none).--policy-offline(orAI_RULEZ_POLICY_OFFLINE=1) uses the cache without asking the network. Pastmax_stale, with no cached copy, or with a copy stamped more than five minutes in the future (fetched while the clock ran ahead), the run fails withAR742. There is no "skip the policy because it is unreachable".validate --show-policymarks a layer served from the cache.
Organization discovery¶
--discover-org (or [policy] discover = "org" in the user config, ~/.config/ai-rulez/config.toml) also loads
ai-rulez-policy.toml from the root of the <owner>/.github repository, where <owner> is the GitHub owner of the
repository's origin remote (https://github.com/<owner>/..., git@github.com:<owner>/..., ssh://). It is
fetched from raw.githubusercontent.com (public .github repositories only) and is the weakest layer. It is the
same as any policy URL: a digest is required, from [policy.digests] in the user config or recorded once with
--policy-trust-tofu, never from the repository.
# ~/.config/ai-rulez/config.toml (the user config, outside every repository)
[policy]
discover = "org"
[policy.digests]
example-org = "sha256:<hex>"
An owner with no policy file (HTTP 404) has no organization policy only while nothing anchors one: a pinned digest, a
digest recorded by --policy-trust-tofu or a required signature turns a 404 into AR742, like any other failure
(a withdrawn policy is not "no policy"), with
the cached copy standing in for at most max_stale. The owner is fetched once per run however many repositories
share it. A remote that is not on GitHub, or no remote at all, skips discovery with a warning, unless
--discover-org was given on the command line, which then fails closed.
Discovery is a convenience, not an anchor. The owner comes from the repository's own git configuration, so a
pull request can point origin at an owner whose policy is looser. A looser org policy cannot loosen anything (layers
only add restrictions), but discovery alone does not bind a repository either. Enforce with --policy,
AI_RULEZ_POLICY or the managed path, and use discovery to add the owner's policy on top. The telemetry and llm
network locks read only the anchored layers, since they apply before any repository is known.
Signed policies¶
A signature lets an organization publish a policy without every machine pinning its digest. Publish the Sigstore
bundle next to the policy, as <policy>.sigstore.json (for a URL, at the same URL plus the suffix). Two forms verify:
- a DSSE attestation over a policy statement (predicate
https://github.com/Goldziher/ai-rulez/attestations/policy/v1, subjectpolicy.tomlwith the policy's digest), made withai-rulez sign --org-policy <file> --key <key>(or--keyless; it refuses a file the loader would reject, and writes the bundle next to it), and - a
cosign sign-blob --bundle policy.toml.sigstore.json policy.tomlmessage signature over the file's exact bytes.
Who may sign comes from outside the repository, like the policy: --policy-signer-key <pem> (repeatable),
--policy-signer-identity with --policy-signer-issuer, --policy-trusted-root, the matching
AI_RULEZ_POLICY_SIGNER_* and AI_RULEZ_POLICY_TRUSTED_ROOT variables, or the user config:
# ~/.config/ai-rulez/config.toml
[policy]
require_signature = true
tlog = "required" # required (default with an identity signer), optional or off (default with keys only)
[[policy.signers]]
identity = "https://github.com/example-org/policy/.github/workflows/sign.yml@refs/heads/main"
issuer = "https://token.actions.githubusercontent.com"
[[policy.signers]]
key_file = "/etc/ai-rulez/policy-signer.pub"
The sources are merged: every one adds trusted signers and any can set the requirement. The signer is checked for the
subject policy with the same machinery as the lock attestation (Signing): signature, certificate chain
and log proof, signer allowed, statement covering this policy (AR724), and a per-user rollback mark so an older signed
policy cannot be replayed (AR727). Every failure is AR746 and fails closed.
- With a trusted signer configured, a signature that is present must verify even when a digest pin matches. An
unsigned policy loads on its pin alone unless
--policy-require-signed(orrequire_signature) says otherwise. - A URL policy with a verified signature needs no digest: the signature vouches for the content. Without a
signature the missing pin is the error (
AR741). - The cache keeps the bundle with the body, and a cached copy is verified again on every load. An unreachable signature fails closed when signatures are required or the policy is not pinned; otherwise the pin vouches for it.
- Policies reached by
extendsand the organization policy are checked the same way, one by one. - Signatures are not looked at when no trusted signer is configured and none is required.
extends¶
A policy may extend others, so a team policy can build on the organization's:
policy_version = 1
name = "payments team"
extends = ["org.toml", "https://policy.example.org/base.toml@sha256:<hex>"] # relative to this file, or pinned URLs
Each parent is a layer of its own (origin extends, listed after the policy that names it) and everything folds
tighten-only, so an extending policy can only add restrictions. Each hop must itself tighten its parent: a value the
child states that is weaker than what it extends (a lower severity floor, an allowlist entry the parent does not cover,
a higher budget, a shorter release age, a weaker scan level, ...) is AR743, because the fold would ignore it and the
author would believe it took effect. Switches are compared by what the child wrote: require_pinned = false,
[lock] enforce = false, include_outputs = false, [guard] generated = false, [governance] enforce = false or
forbid_self_approval = false, [hooks] allow = true, and allow_network = true under [telemetry] or [llm] are
loosenings when the parent turns the restriction on; false next to a parent that left it off is not. A key the child
does not mention is not a loosening.
Limits: at most 8 entries per extends, 5 hops deep, 32 policies in one load, no cycles (the cycle is named in the
error). A URL parent needs its @sha256:<hex> pin (no trust-on-first-use inside a chain); a policy fetched from a URL
cannot extend a local file. A parent that cannot be loaded fails the whole load closed (AR742). A policy reached
twice (a diamond, or an anchor that is also a parent) is one layer.
Policy is never read from the repository or from config.local.*. Set AI_RULEZ_POLICY from the trusted side
(organization secrets and variables, a required workflow), not from a repository-level workflow file that a fork pull
request can edit.
The policy file¶
TOML, data only. An unknown key is an error (AR743) because a typo must not silently loosen the policy.
# ai-rulez-policy.toml
policy_version = 1
name = "example-org baseline"
[sources]
allowed_hosts = ["github.com/example-org", "*.example.org"] # includes, installed skills, skill sources
deny_hosts = ["github.com/example-org/archived"]
require_pinned = true # the lock is enforced; an unpinned remote is an error
min_release_age = "7d" # the youngest tag any source may adopt
min_release_age_source = "first-seen" # release time from the forge or first sighting, never a committer's date
deny_digests = ["sha256:0000000000000000000000000000000000000000000000000000000000000000"] # known-bad content, blocked everywhere
[lint]
required_codes = ["AR001", "AR005", "AR008"] # may be neither turned off nor ignored
no_inline_ignore = ["AR001"] # ai-rulez-lint-ignore comments are not honored for these
[lint.severity_floor]
AR001 = "error"
AR008 = "warning"
[lint.max_findings]
AR005 = 0 # more than this many findings of a code is AR749
[lint.security]
allowed_hosts = ["github.com", "*.example.org"] # bounds the repository's own list
scan_imports = "error"
directive_tags = ["assistant"] # always checked by AR018; the repository may add more
trusted_orgs = ["anthropics", "github"] # the repository may name only these
[lint.scanner_policy]
preset = "baseline" # the weakest scanner preset; the repository may use a stronger one
required = ["agnix"] # always required; the repository's own list is added
fail_on = "warning" # the most permissive threshold for scanner findings
isolation = "require" # the weakest isolation of staged scanners
allow_egress = [] # the scanners --allow-egress may enable; [] forbids every egress scanner
[lint.capability]
max_network_commands = 3 # AR030 limit; the repository may set a lower one
[lint.budgets.skill]
max_tokens = 4000 # AR902 limit per content kind; the repository may set a lower one
[lint.load_budgets]
claude-skill-listing = 1200 # AR964 limit by id; the repository may set a lower one
[lock]
enforce = true
include_outputs = true
[telemetry]
allow_network = false # export off, whatever the user scope says
[llm]
allow_network = false
[guard]
generated = true
[governance]
enforce = true # approvals are enforced, whatever the repository says
require_approval = ["remote", "kind:hook"] # always required; the repository's exempt cannot narrow it
min_approvers = 2
approvers = ["alice@example.org", "bob@example.org"] # only these reviewers count
min_assurance = "review-linked" # an asserted approval does not count
forbid_self_approval = true # an author cannot approve their own change
approvers_from = "CODEOWNERS" # approvers must also own the item's path (the repository picks the file)
[signing]
require_verified = ["lock"] # the lock must carry a verified attestation
tlog = "required" # the weakest transparency-log mode
max_age = "180d" # the oldest signature the repository may accept
min_hash_version = 1
allow_repo_identities = false # the repository may trust only the signers below
[signing.thresholds]
lock = 2 # at least two distinct signers must have signed the lock
[[signing.trust]]
subject = "lock"
identity = "https://github.com/example-org/ai-config/.github/workflows/release.yml@refs/heads/main"
issuer = "https://token.actions.githubusercontent.com"
[mcp]
allowed_commands = ["npx", "uvx"] # the only commands a stdio MCP server may run
deny_transports = ["http", "sse"] # no remote MCP endpoint
[hooks]
allow = false # any [[hooks]] group is a violation
Rule codes may be written as codes (AR001) or names (secret-detected). A code this ai-rulez does not know is an
error: the policy is newer than the binary, and the message asks to upgrade. policy_version = 1 is required; a
higher number is refused.
Host patterns¶
A pattern is a host, optionally followed by a path prefix, compared case-insensitively:
| Pattern | Matches |
|---|---|
github.com |
the host, any path |
*.example.org |
example.org and every subdomain (the same rule as lint.security.allowed_hosts) |
github.com/example-org |
any path under the segment example-org, not example-org2 |
github.com/example-* |
a segment may carry one * at its start or end |
github.com/*/rules |
* is exactly one segment |
Matching is by path prefix, so a trailing /** means the same as leaving it out. A bare * host is refused. A source
is read from its URL: https://github.com/example-org/rules.git, git@github.com:example-org/rules.git and
ssh://git@host:2222/org/repo all give a host and a path. A local path names no host and is not governed by
sources.
lint.security.allowed_hosts lists hosts only (no path), because it bounds the hosts URLs inside content may point
to.
How a repository is bounded¶
Each key has one direction. The policy value is enforced (clamped) and any attempt to loosen it is reported.
| Key | Effective value | A repository loosens it by | Code |
|---|---|---|---|
sources.allowed_hosts |
sources the list covers | a source from a host the list does not cover; the source is not loaded | AR745 |
sources.deny_hosts |
union of layers | a source from a denied host; the source is not loaded | AR745 |
sources.deny_digests |
union of layers; the organization's deny list, the counterpart of the lock's [[deny]] (Approvals); both block, so a digest on either is denied |
a include, installed skill or skill source whose pinned digest is listed is not loaded; a listed authored item is reported (ai-rulez.lock names the digests, so keep the lock enforced) |
AR747 |
sources.require_pinned, lock.enforce |
the lock is enforced | [lock] enforce = false |
AR740 |
lock.include_outputs |
output digests are pinned | [lock] include_outputs = false |
AR740 |
lint.required_codes |
union of layers | [lint.severity] CODE = "off" or [lint] ignore |
AR744 |
lint.severity_floor |
the higher severity of the layers | a lower [lint.severity], or [lint] ignore of a floored code |
AR740 |
lint.no_inline_ignore |
union of layers | an ai-rulez-lint-ignore comment for a listed code; the finding is still reported |
AR740 |
lint.max_findings.<code> |
the lower ceiling per code (0 allows none). The code is protected: baselines, [lint.ratchet] and ignores do not apply |
(a ceiling is not a repository key; going over it is reported) | AR749 |
lint.security.allowed_hosts |
repository entries the list covers; the policy list when it sets none | an entry the list does not provably cover; the entry is dropped | AR740 |
lint.security.scan_imports |
the stricter level (off < warn < unset < error) |
a weaker explicit level | AR740 |
lint.security.directive_tags |
union of layers, then the repository's own tags | (nothing to report: the repository's list only adds) | none |
lint.security.trusted_orgs |
the repository's entries the list names; the policy list when it sets none or none is left. An empty policy list trusts no organization | an entry the list does not name; the entry is dropped | AR740 |
lint.capability.max_network_commands |
the lower value; an unset repository value is the lower of the policy bound and the built-in 5 | a higher explicit value | AR740 |
sources.min_release_age |
the longer age; an unset [lock] or per-source age takes it |
a younger [lock] min_release_age or per-source age, including "0" |
AR740 |
sources.min_release_age_source |
the stronger of forge and first-seen; an unset [lock] source takes forge, and keeps auto under first-seen |
a weaker [lock] min_release_age_source: commit, and auto or first-seen under forge (auto can fall back to first-seen) |
AR740 |
lint.budgets.<kind>.max_lines, .max_tokens |
the lower value per kind and field; an unset one is the lower of the policy bound and the built-in budget | a higher explicit [lint.budgets.<kind>] value |
AR740 |
lint.load_budgets.<id> |
the lower value per id; an unset one is the lower of the policy bound and the built-in limit | a higher explicit value | AR740 |
lint.scanner_policy.preset |
the stronger preset (off < baseline < strict) |
a weaker explicit preset (an unset one takes the policy's) | AR740 |
lint.scanner_policy.required |
union with the repository's list | (nothing to report: the repository's list only adds) | none |
lint.scanner_policy.fail_on |
the stricter threshold (error < warning < info) |
a looser explicit threshold | AR740 |
lint.scanner_policy.isolation |
the stronger level (none < auto < require) |
a weaker explicit level | AR740 |
lint.scanner_policy.allow_egress |
the repository's entries the policy names; the policy list when it sets none. An empty policy list allows no scanner to send content off the machine | an entry the list does not name; the entry is dropped | AR740 |
telemetry.allow_network, llm.allow_network |
false |
allow_network = true in the repository (already ignored by the trust rule, now also reported) |
AR740 |
guard.generated |
true |
a [guard] table without generated = true |
AR740 |
governance.enforce |
true |
a [governance] table without enforce = true |
AR740 |
governance.require_approval |
union with the repository's selectors; the policy's are not narrowed by exempt |
(nothing to report: exempt is simply not applied to them) |
none |
governance.min_approvers |
the larger value | a lower explicit min_approvers |
AR740 |
governance.min_assurance |
the stronger level (asserted < review-linked < signed) |
a weaker explicit min_assurance |
AR740 |
governance.forbid_self_approval |
true |
a [governance] table without forbid_self_approval = true |
AR740 |
governance.approvers_from |
the repository's own value, or "CODEOWNERS" when it has none; a policy cannot name a path |
a [governance] table without approvers_from |
AR740 |
signing.require_verified |
union with the repository's [signing] require |
(nothing to report: the repository's list only adds) | none |
signing.tlog |
the stricter mode (off < optional < required) |
a weaker explicit [signing] tlog |
AR740 |
signing.max_age |
the shorter age; an unset one takes the policy's | a longer explicit [signing] max_age |
AR740 |
signing.min_hash_version |
the larger value | a lower explicit [signing] min_hash_version |
AR740 |
signing.trust, signing.allow_repo_identities |
per subject (lock, bundle, skill, sbom, approval), for the subjects the list names: the repository's signers the list names (compared on subject, identity or identity_regexp, and issuer); the policy's entries for the subject when none is left. Entries for subjects the list does not name are not touched. An empty list (or allow_repo_identities = false alone) governs every subject and trusts nobody, so verification fails closed. A key_file is never in the list |
a signer the list does not name for a governed subject, including any key_file or shorthand identity; the entry is dropped |
AR740 |
signing.thresholds |
per subject, the larger value; a subject the repository leaves unset takes the floor | a lower explicit [signing.thresholds] value |
AR740 |
mcp.allowed_commands |
the stdio servers whose command the list names (compared as written); an empty list allows none | an MCP server running another command; the server is not loaded. An imported agent, skill or command with such an inline mcpServers entry loses the whole mcpServers key |
AR748 |
mcp.deny_transports |
union of layers | an MCP server on a denied transport (stdio, http, sse); the server is not loaded (inline mcpServers of imported content: as above) |
AR748 |
hooks.allow |
false forbids every hook group |
a [[hooks]] group; it is not loaded. An imported agent, skill or command that declares hooks in its frontmatter loses the key |
AR748 |
governance.approvers |
the repository's entries that the list names; the policy list when it sets none or none is left. An empty policy list means nobody may approve | an entry the list does not name; the entry is dropped | AR740 |
[governance] matters because approvals are records in the repository's own lock: without a floor the repository
could set approvers, min_approvers, exempt or enforce to whatever lets its change through. Reviewers are
compared lower-cased and trimmed. Even with the floor an asserted approval is not authentication; see
Approvals for the CI check that makes the floor meaningful.
A repository that does not touch a key is not reported: the policy value is just used. AR740 and the other policy
codes are always errors; [lint.severity] cannot soften them and [lint] ignore cannot hide them. The telemetry
and LLM locks hold in user scope and the environment too, since the point is to stop a machine from enabling what the
organization forbids. A policy that cannot be loaded locks both.
Merging layers¶
Merge combines two policies into the tighter one, key by key: allowlists intersect, denylists and required sets
union, severities and scan levels take the stricter value, upper bounds (max_network_commands, load_budgets) take the
lower value, and switches turn on. It is commutative, idempotent and
associative, and an empty policy is its identity, so the order of layers does not matter beyond naming the origin.
The intersection of two allowlists keeps an entry of one list when an entry of the other covers it.
covers(policy, repo) decides whether a repository pattern is inside a policy pattern. It is segment-based and
conservative: when it cannot prove that every location the repository pattern matches is also matched by the policy
pattern, the answer is no. A false reject is safe; a false accept is not (a fuzz test checks this against a brute-force
universe).
Rolling a policy out: --policy-mode warn¶
--policy-mode warn lets a policy land before every repository complies. The policy values are still enforced (the
clamp is unchanged), but each loosening attempt is reported as a warning instead of an error: the AR74x findings
become warnings, generate and validate log the violations and carry on, validate --show-policy exits 0 and
prints mode: warn, and the JSON report has "mode": "warn". Ai-rulez logs a warning at load so the mode is never
silent. An unavailable or invalid policy (AR741-AR743) still fails closed, so warn can only relax how a
violation is judged, never whether the policy is loaded. Set it on the trusted side, with the policy itself: a
repository cannot set it.
validate --show-policy¶
$ ai-rulez validate --show-policy
policy: 2 layers
env /ci/team.toml sha256:a90b00000000… (team)
managed /etc/ai-rulez/policy.toml sha256:5c1e00000000… (example-org baseline)
effective (origin in brackets)
lint.severity_floor.AR008 = error [env]
lock.enforce = true [env]
sources.allowed_hosts = ["github.com/example-org"] [managed]
repo overrides accepted: lint.security.allowed_hosts (narrowed to ["github.com"])
repo overrides rejected: 1
AR740 .ai-rulez/config.toml:12 [lint.severity] AR008 = "warning" is below the policy floor "error" (origin: env); "error" is enforced
--format json writes the same report (schema/policy-effective.schema.json): layers[{origin, source, name,
digest}], effective, provenance (dotted key to the layer, joined with + when several layers contribute to a
set), overrides{accepted, rejected} and violations[]. The digest is the SHA-256 of the file with CRLF normalized
to LF. Exit code 2 when the repository loosens the policy (the same code generate and validate give). The policy file format is
schema/policy.schema.json.
What honors the policy¶
generate,validateandvalidate --show-policy: the clamp is applied at load. A configuration that loosens the policy exits2fromgenerate, fromvalidate(with or without--strictor--config-only) and fromvalidate --show-policy;validate --strictreports each attempt as anAR74xfinding. The severity floor and required codes are enforced inside the lint run, so a floored finding cannot be demoted by a lint profile.generaterefuses to run on a configuration that loosens the policy, and so does each root ofgenerate --recursiveandvalidate --recursive. Loosening is exit2everywhere: when every failed root loosened the policy (or drifted from the lock) a recursive run exits2, and1when some root failed for another reason.lockrefuses to writeai-rulez.lockfor such a configuration (exit2, naming theAR740lines), andlock --checkreports a mismatch (exit2).doctoranddoctor --policyreport each attempt as anAR740error finding in thepolicycheck (a warning under--policy-mode warn) and exit2.- The MCP servers:
mcp --serve-skillsfails to start on a configuration that loosens the policy, and thegenerate_outputsandvalidate_configtools ofmcprefuse it (validate_configanswersvalid: false). - No suppression hides a protected code: for every code in
required_codesor raised byseverity_floor(and AR740-AR745 themselves),ai-rulez-lint-ignorecomments,[lint] ignore_paths, baseline entries and[lint.ratchet]budgets are not applied. The finding stays at its enforced severity and counts toward the exit code, and one AR740 finding per route names the attempt ([lint] ignore_paths,ai-rulez-lint-ignore comment,baseline,[lint.ratchet]) and the codes it tried to hide. - Remote sources a policy refuses are dropped before anything is fetched.
Design decisions¶
- Discovery reads the owner from
origin. A repository can change its own remote, so the org layer is additive only. The design asked whether discovering from the git remote is acceptable at all (it is an implicit fetch): it is opt-in per user or per invocation, pinned by digest, and fails closed once demanded. deny_digestsreportsAR747, the lock's[[deny]]reportsAR717. They are two lists with one effect: the content is blocked.AR717is the repository's own list inai-rulez.lock(approve --revoke --deny);AR747is the organization's list, which the repository cannot edit or remove an entry from. The codes stay apart so a finding says whose list it came from. The policy list is checked wherever the policy is applied (generate,validate,lock --check), not byapprove: approving a digest the policy denies is possible but changes nothing, because generation still refuses it. The lock is the source of the digests, which is why a repository without a lock has nothing to check.- Governance floors.
min_assurance,forbid_self_approvalandapprovers_fromare floors likemin_approvers. Without them a repository could lower its own assurance in the pull request that needs the approval. They are assertions about the lock and CODEOWNERS the repository itself carries, so pair them with branch protection (see Approvals); the policy cannot name a CODEOWNERS path, only demand that one is used. - Signing trust is per subject.
[[signing.trust]]rows may name any trust subject. The list clamps only the subjects it names (all of them when empty), so a policy that lists lock signers does not silently drop the repository's skill, bundle, sbom or approval signers.[signing.thresholds]is a per-subject floor on k of n. extendsis checked, not just folded. Merging already makes the chain at least as strict as each parent, so a looser child value would be silently dropped. Reporting it (AR743) is the safer choice: a team lead who writes a weaker value learns it does nothing.- URL policies.
AR741is a missing or mismatched digest; a stale or absent cache after a failed fetch isAR742. Trust-on-first-use is terminal-only so that nobody can script past the review of a digest. - Rollback of a signed policy. A signature with a signing time (a keyless bundle, a log entry or the policy
attestation's
issued_at) is checked against the newest time this machine verified (AR727) and against clock skew. A key-signedcosign sign-blobbundle without a log entry has no time: the machine instead remembers the bodies a newer one replaced and refuses them (AR727). That covers the versions this machine has seen, like the time check; a fresh machine starts empty, and skew cannot be judged. - Fail closed everywhere. A demanded policy that cannot be read is
AR742, and an unusable policy locks telemetry and LLM network use. - An explicit empty allowlist means nothing is allowed.
lint.security.allowed_hosts = []in a policy is enforced as a single host that never resolves (policy.invalid), because an empty list inconfig.tomlswitches the host check off. - Unset is not a violation. A repository that leaves a key alone gets the policy value silently; only an explicit weaker value is reported.
[guard]. The config type cannot tell an explicitgenerated = falsefrom an unset table, so the guard is reported only when the repository wrote a[guard]table withoutgenerated = true.- Provenance names the strongest layer that decides a key (a tie goes to the stronger anchor), and every contributing layer for a set.
max_findingsis a gate, not a forgiveness. Up to the ceiling the findings keep their own severity; one more isAR749. The code is protected, so baselines,[lint.ratchet]and ignores cannot absorb it. The design's[lint.size] max_tokensis the existing[lint.budgets.<kind>]table here.- Imported hooks and MCP servers. MCP servers cannot be imported from an include, but an agent, skill or command
an include or an installed skill delivers can declare
hooksandmcpServersin its frontmatter.[hooks]and[mcp]bound those too, once the content is loaded: a file outside the project's own configuration directory (or inside a local include's directory) is imported. A violation unloads the key and isAR748, reported against the imported file. Every spelling of the key counts (mcpServers,mcp-servers,mcp_servers, any letter case): the renderers that pass these keys through to a harness and the policy read one registry of the keys that execute or connect. Builtin packs are not bounded (they ship with the binary). - Every command is bound. Each command's loads, CRUD operations and MCP setup run under the policy, and so do the
configurations
generateloads itself (monorepo members, the shared view forroles.json, the drift baseline).catalog diffandapproveload earlier revisions from a temporary directory, but--discover-orgstill reads the remote of the real project. - Known gaps. A policy value is not checked against the secret scanner yet.
deny_digestsbounds what the repository's ownai-rulez.locksays. - No policy, no change. Without a flag, variable or managed file the loader does nothing (tested).