feat: policy registry — starter admission contracts that carry their own evidence - #108
Merged
Conversation
`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.
Signetry Reviewer — 🟡 Needs human reviewA human should decide — the required check is pending; 1 advisory finding(s) to weigh. Deterministic gates (the authority)
Findings (1, 0 blocking)
MergeA human should review and merge.
|
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-controlsignetry initwritespolicy_owner: your-team.policy_status()reported that asdeclared— "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 wasbool(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:
declaredplaceholderincompleteAnything other than
declaredmust 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-ORGare caught; a legitimateorg-infraorTeam Yourselfstaysdeclared.tests/test_policy_provenance.py(26 tests) pins both directions.2.
feat:policy registrydocs-onlydependency-bumppython-librarysetup.py/tox/conftest.pystay outnode-servicemonorepo-servicepackages/**excludedci-workflow-fixWriting 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 --policycopies 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# @policyheader 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
allowsdirection matters as much asblocks: 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, becauseload_contractsilently 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_emptyexists 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-teamand therefore reportsplaceholder(commit 1). Adoption is a human act.initprints what you just took on:placeholderdoesn'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-fixpermits workflow edits because teams need CI repair governed rather than done outside Signetry. Its caution — write access to.github/workflowsreads 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-bumpshipsrequired_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
main; +66 registry, +26 provenance).ruff checkon all new and modified files is clean except two pre-existing findings incontract.py:404/:426, which are part of the 73-error baseline that also exists onmain(confirmed bygit stash -u). My venv has ruff 0.16.5 vs the 0.15.22 pinned inuv.lock, which is why CI is green onmaindespite them.uv build --wheel→ the six YAML files are in the wheel →pip installinto a clean venv →signetry policieslists all six →init --policy python-libraryin a fresh repo produces a file byte-identical to the published policy. This was the one thing that could have silently broken forpipusers 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.mddocuments the bar — evidence, not taste.Branch note
Branched off
main, not offopen-core-relicense, so this is reviewable on its own. Expect a trivialCHANGELOG.mdconflict 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-teamis 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.