Skip to content

feat: policy registry — starter admission contracts that carry their own evidence - #108

Merged
bkd-dotcom merged 3 commits into
mainfrom
policy-registry
Sep 1, 2026
Merged

feat: policy registry — starter admission contracts that carry their own evidence#108
bkd-dotcom merged 3 commits into
mainfrom
policy-registry

Conversation

@bkd-dotcom

Copy link
Copy Markdown
Member

Two commits. The first is a bug fix that stands on its own; the second is the feature that depends on it.

1. fix: scaffold provenance was reported as declared change-control

signetry init writes policy_owner: your-team. policy_status() reported that as declared"Policy declares a human owner and version (change-controlled metadata)" — so every receipt from a freshly initialised repo asserted change-control over a policy file no human had read.

This is the project's own failure mode pointed inward: a claim made on absent evidence. A missing owner correctly reported incomplete. A placeholder owner sailed through, because the check was bool(policy_owner) and scaffold text is a truthy string.

Placeholder provenance is now treated as absent, with its own status so the receipt can say why:

status meaning
declared a real owner and version — change-controlled, not cryptographic
placeholder still scaffold text; nobody has adopted this policy
incomplete no owner or version declared at all

Anything other than declared must be treated as not change-controlled. The extra value exists to make the gap actionable; it never means "good enough". policy_status()["owner"] still reports the raw value, so a reader sees the scaffold text rather than having it hidden behind a judgement.

Matching is on the whole value after stripping <>[](){} and folding case — <your-team>, TODO, YOUR-ORG are caught; a legitimate org-infra or Team Yourself stays declared. tests/test_policy_provenance.py (26 tests) pins both directions.

Note for consumers matching on this field: a repo that ran signetry init and never edited the provenance keys now reports placeholder where it previously reported declared. That is the bug being fixed, not a regression.

2. feat: policy registry

signetry policies                        # list
signetry init --policy python-library    # install
id for scope
docs-only any repo markdown, text, images — the whole pipeline on a change that cannot break anything
dependency-bump any repo manifests and lockfiles only; the narrowest useful scope
python-library src-layout Python library code, tests, manifest; setup.py/tox/conftest.py stay out
node-service Node / TS app code and tests, not the build or release path
monorepo-service monorepos one service directory; siblings and packages/** excluded
ci-workflow-fix GitHub Actions workflow files only, one at a time — carries a caution

Writing the first admission contract is where adoption stalls. The format is easy; deciding what an agent should be allowed to touch in a given stack is a real security decision, and most teams defer it.

Why these are worth trusting rather than copying off a docs page

What ships is what lands. An entry is a literal, valid admission.yaml; init --policy copies the bytes verbatim — no templating, no merge. An adopter can diff their installed file against the published policy and get nothing back, and a test asserts that byte equality. Metadata lives in # @policy header comments, invisible to the contract parser and useful to a human reading the installed file.

Every entry carries its own evidence. Each policy declares example paths it must block and must allow; the tests run every claim through the real evaluate_contract — the same function the pipeline uses. A policy whose documentation contradicts its behaviour fails CI.

The allows direction matters as much as blocks: it's what catches an over-broad forbidden glob quietly making a policy useless, which otherwise surfaces months later as "the agent can never propose anything". Tests also reject a policy with no scope rules, because load_contract silently substitutes the default scope for an empty contract — it would govern nothing while appearing to.

The harness was validated before being trusted

A validation harness that has never failed proves nothing. I added a deliberately broken policy — claiming to block a path inside its own allowlist, with a real-looking policy_owner — and confirmed it failed exactly the four tests it should, with messages naming the policy and the offending path, then removed it.

test_the_registry_is_not_empty exists for the same reason: every other test is parametrized over the discovered entries, so an empty directory would make the whole file vacuously green — the "no evidence reads as success" failure this project is about.

A registry policy is not an owned policy

Every entry ships policy_owner: your-team and therefore reports placeholder (commit 1). Adoption is a human act. init prints what you just took on:

$ signetry init . --policy python-library
wrote /repo/.signetry/admission.yaml  (python-library — Python library (src layout, pytest))
  scope    5 allowed pattern(s), 10 forbidden, max 12 file(s)
  checks   pytest -q
  owner    unowned — set policy_owner and policy_version to adopt this policy as your own

placeholder doesn't restrict what a change can earn — authority still comes from the deterministic contract, the independent verifier and required checks. It only stops the receipt asserting provenance that doesn't exist.

The risky one is risky on purpose

ci-workflow-fix permits workflow edits because teams need CI repair governed rather than done outside Signetry. Its caution — write access to .github/workflows reads repository secrets and runs arbitrary code with them on the next push — is printed at adoption time, not left in a file nobody reopens. It forbids release/publish/deploy/sign workflows and caps the change at one file. A risky policy with no caution should be sent back in review.

dependency-bump ships required_checks: [] with a comment saying to set it: it spans npm/pip/cargo/go, and a plausible-looking wrong check command is worse than an absent one.

Verification

  • 373 tests pass (up from 347 on main; +66 registry, +26 provenance).
  • Zero new lint. ruff check on all new and modified files is clean except two pre-existing findings in contract.py:404/:426, which are part of the 73-error baseline that also exists on main (confirmed by git stash -u). My venv has ruff 0.16.5 vs the 0.15.22 pinned in uv.lock, which is why CI is green on main despite them.
  • Packaging verified against a built artifact, not assumed. uv build --wheel → the six YAML files are in the wheel → pip install into a clean venv → signetry policies lists all six → init --policy python-library in a fresh repo produces a file byte-identical to the published policy. This was the one thing that could have silently broken for pip users while passing every in-repo test.

Contributing

Adding a policy needs no registration: drop the YAML in signetry_core/policies/ and it's discovered and validated. available_policies() raises on a malformed entry rather than skipping it, so a broken policy is a build failure rather than an invisible absence. docs/site/policy-registry.md documents the bar — evidence, not taste.

Branch note

Branched off main, not off open-core-relicense, so this is reviewable on its own. Expect a trivial CHANGELOG.md conflict with core#107 (both add to [Unreleased]).

One follow-up this PR does not contain: docs/RECEIPT_SPEC.md §9 currently says provenance fields are optional and "when absent the policy MUST be treated as unsigned". policy_owner: your-team is present, so the spec's letter is satisfied by exactly the value this PR classifies as unowned. The spec needs a sentence closing that gap, but the file only exists on core#107's branch — so it goes there, after this lands.

`signetry init` writes `policy_owner: your-team`, and `policy_status()` reported
`declared` for it — "Policy declares a human owner and version (change-controlled
metadata)". So every receipt from a freshly initialised repo asserted change-control
over a policy file no human had read.

This is the failure mode the project exists to prevent, pointed inward: a claim
made on absent evidence. A missing owner correctly reported `incomplete`; a
*placeholder* owner sailed through, because the check was `bool(policy_owner)` and
scaffold text is a truthy string.

Placeholder provenance is now treated as absent, with its own status so the receipt
can say why rather than just that:

  declared     — a real owner and version (change-controlled, not cryptographic)
  placeholder  — still scaffold text; nobody has adopted this policy
  incomplete   — no owner or version declared at all

Consumers must treat anything other than `declared` as not change-controlled. The
extra value exists to make the gap actionable — it never means "good enough".
`policy_status()["owner"]` still reports the raw value, so a reader sees the
scaffold text for themselves rather than having it hidden behind a judgement.

Matching is on the whole value after stripping `<>[](){}` wrappers and folding
case, so `<your-team>`, `TODO` and `  YOUR-ORG  ` are caught while a legitimate
`org-infra` or `Team Yourself` stays declared. `is_policy_placeholder` is public
because the registry harness asserts every shipped template is recognised by it.

Note for consumers matching on this field: a repo that ran `signetry init` and never
edited the provenance keys now reports `placeholder` where it reported `declared`.
That is the bug being fixed, not a regression.
…own evidence

Writing the first admission contract is where adoption stalls. The format is easy;
deciding what an agent should be allowed to touch in a given stack is a real
security decision, and most teams defer it. Six named policies answer it:

  signetry policies                        # list them
  signetry init --policy python-library    # install one

  docs-only         markdown/text/images — the whole pipeline on a change that
                    cannot break anything, which is the right first adoption step
  dependency-bump   manifests and lockfiles only; the narrowest useful scope
  python-library    src layout + pytest; setup.py/tox/conftest.py stay out
  node-service      app code and tests, not the build or release path
  monorepo-service  one service directory; siblings and packages/** excluded
  ci-workflow-fix   workflow files only, one at a time — carries a caution

Two properties make these worth trusting rather than copying off a docs page.

**What ships is what lands.** A registry entry is a literal, valid admission.yaml
and `init --policy` copies the bytes verbatim — no templating, no merge, no rewrite.
An adopter can diff their `.signetry/admission.yaml` against the published policy
and get nothing back, and a test asserts that byte equality. Metadata lives in
`# @policy` header comments, which the contract parser ignores and a human reading
the installed file still benefits from.

**Every entry carries its own evidence.** Each policy declares example paths it
must block and must allow, and `tests/test_policy_registry.py` runs every claim
through the real `evaluate_contract` — the same function the pipeline uses. A policy
whose documentation contradicts its behaviour fails CI. The `allows` direction
matters as much as `blocks`: it catches an over-broad forbidden glob quietly making
a policy useless, which otherwise surfaces months later as "the agent can never
propose anything". Tests also refuse a policy with no scope rules, because
`load_contract` silently substitutes the default scope for an empty contract — it
would govern nothing while appearing to.

The harness was validated against a deliberately broken policy (claiming to block a
path inside its own allowlist, with a real-looking owner) before being trusted: it
failed the four tests it should, with messages naming the policy and the path.

`test_the_registry_is_not_empty` exists because every other test is parametrized
over the discovered entries — an empty directory would make the file vacuously
green, which is the "no evidence reads as success" failure this project is about.

Adoption is a human act, so every entry ships `policy_owner: your-team` and reports
`placeholder`, not `declared`. A borrowed policy nobody at the adopting org has read
is not change-controlled, and a receipt claiming otherwise would be worse than one
admitting the gap. `init` prints the scope it just installed, a warning when
`required_checks` is empty, any caution, and always the ownership line.

`ci-workflow-fix` is the one that permits something risky, deliberately: teams need
CI repair governed rather than done outside Signetry. Its caution — write access to
`.github/workflows` reads repository secrets and runs arbitrary code with them on
the next push — is printed at adoption time, not left in a file nobody reopens. It
forbids release/publish/deploy/sign workflows and caps the change at one file.
`dependency-bump` ships `required_checks: []` with a comment saying to set it,
because a plausible-looking wrong check command is worse than an absent one.

Packaging verified against a built artifact rather than assumed: the six YAML files
ship in the wheel, and `init --policy` from a clean `pip install` produces a file
byte-identical to the published one.

Adding a policy requires no registration — drop the YAML in `signetry_core/policies/`
and it is discovered and validated. `available_policies()` raises on a malformed
entry rather than skipping it, so a broken policy is a build failure, not an
invisible absence.
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Signetry Reviewer — 🟡 Needs human review

A human should decide — the required check is pending; 1 advisory finding(s) to weigh.

Deterministic gates (the authority)

Gate Status
Required status check ⏳ pending
Secret scan ✅ clean
CI permission / OIDC ✅ no forbidden change
Dependency skew ✅ ok
All green

Findings (1, 0 blocking)

  • 🟡 Change touches a protected path: signetry_core/pipeline/contract.py signetry_core/pipeline/contract.py (via cross-check)
    • signetry_core/pipeline/contract.py matches a protected pattern (signetry_core/pipeline/contract.py). Changes here alter shared/foundational surface and warrant a designated reviewer.
    • Fix: Route to a code owner / architecture reviewer.

Merge

A human should review and merge.

This review is advisory. It never merges on its own judgement — the deterministic gates + a human are the authority. Findings can have false negatives; a green bot verdict is not a guarantee.

@bkd-dotcom
bkd-dotcom merged commit 00db631 into main Sep 1, 2026
8 checks passed
@bkd-dotcom
bkd-dotcom deleted the policy-registry branch September 1, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant