Watchpoint policies
A watchpoint policy makes a policy step conditional on which files a change touches. Mark auth/** with the review class critical. Write a policy step that applies to critical. A push that touches auth/login.go must carry that step's evidence. A push that touches only README.md does not.
Phase 1 is warn only. Nothing is refused yet. Where the gate sees a missing class-scoped step it can say so, and the push still goes through. Block mode is not built.
What ships, and where it stands
This page describes behavior in three states. Each section marks which one it is in.
| State | Meaning |
|---|---|
| Available | Merged to main as of 2026-11-11 |
| Open PR | Code is written and in review. It is not on main and not in any release |
| Not yet opened | Assigned and designed. No pull request exists yet, so nothing here is checked against code |
| Piece | State |
|---|---|
Review classes and control on a watchpoint (API fields) | Available (#18947) |
| Class and pattern validators | Available as a library (#19007). They are applied to writes by the control guard below |
| HUMAN and SHARED control, AAL2 guard, class version check (#19047) | Available |
| Class map built from a repository's watchpoints, and the class scope for a push | Available as library code (#19037). The release evaluator does not call it yet |
Policy v0.3 appliesTo, class matcher, cilock verify --class-map --changed-paths | Available (open source, subtrees/rookery) |
| Watchpoint write errors shown to the user (#19134) | Available |
watchpoint:write scope for agent tokens | Not yet opened |
Class scope in evaluate-release (#19144) | Available |
applicable-releases flag class_scoped_steps (#19137) | Available |
| Observation in front of the verdict and class-scope text at the Pushgate edge (#19138) | Available |
change-approval binds to its commit through commitSubject (#19151) | Available |
What a watchpoint and a review class are
A watchpoint is a marked region of a repository: a file path or glob, in one repository, that the platform tracks. A GLOB watchpoint holds a doublestar pattern such as auth/**. A REGION watchpoint holds a line range in one file.
A review class is a plain name you attach to a GLOB watchpoint, such as critical or security. It says what kind of scrutiny a path needs. It says nothing about who reviews or what evidence counts. That lives in the signed policy.
The platform collects the monitored GLOB watchpoints of one repository into a class map: each pattern paired with its classes. Both HUMAN and SHARED watchpoints feed the map. A REGION watchpoint is not part of it, and a repository's entries never gate a push to another repository.
Setting classes today is an API call. createWatchpoint and updateWatchpoint take reviewClasses and control; updateWatchpoint also takes appendReviewClasses and clearReviewClasses. There is no screen for it.
HUMAN and SHARED
Every watchpoint has a control value, SHARED by default. The platform enforces the table below in the data layer (#19047).
| Action | HUMAN watchpoint | SHARED watchpoint |
|---|---|---|
| Create | Tenant OWNER or ADMIN with an AAL2 step-up | Anyone with write access: a member, an enrolled agent, AI or evidence discovery |
Edit the pattern, the classes or monitored | OWNER or ADMIN with AAL2 | Anyone with write access |
| Delete | OWNER or ADMIN with AAL2 | Anyone with write access |
Change control, either direction | OWNER or ADMIN with AAL2 | OWNER or ADMIN with AAL2 |
Two details matter.
AAL2 means a passkey, used just now. The guard reuses the check that protects policy binding activation: an interactive session, an OWNER or ADMIN role, and a passkey step-up no older than two minutes (BindingActivationMaxAge, judge-api/internal/auth/binding_activation.go:17). The role alone is never enough. An agent holds an API token, and a token cannot perform a step-up, so no agent credential can create, edit or delete a HUMAN watchpoint, whatever role it carries.
Changing control always needs a person. The rule is judged on the state before the change, and writing control to the value it already has counts. Promoting a SHARED row to HUMAN and demoting it back are the same act. Agents and discovery can therefore keep SHARED watchpoints current, and cannot make or loosen a HUMAN one.
The guard runs in the data layer, so a direct write cannot skip it. A change that needs a person must name one watchpoint. And it only lands on a row that still has the control the guard checked: if a person promotes the row to HUMAN while an agent's write is in flight, the agent's write matches nothing and fails. Every edit of a watchpoint's classes or pattern increments its class_version, and the write lands only on the version the guard read. A concurrent change to either makes the second write fail with a message to re-read and retry.
Limit: HUMAN protects the watchpoint, not the policy. Who may publish and activate the signed policy is a separate control. See trust architecture.
What a pattern and a class may look like
These rules apply when a watchpoint carries classes. The data-layer guard applies them to every create and edit (#19047).
A class name:
- matches
[a-z][a-z0-9_-]{0,63}: lowercase, starts with a letter, at most 64 characters - appears at most 16 times on one watchpoint; duplicates collapse and the set is stored sorted
A pattern:
- is at most 256 bytes, with no control characters
- is relative to the repository: no leading
/, no\, and no empty,.or..segment - contains no
{,}or[!. Write one watchpoint per alternative instead of{a,b}. Use[^a]for a negated class - does not start with
!or-. There is no negation and no override: the set is a union - is not a blanket.
**,*and**/*are refused, and so is any spelling that matches every path
Why the strict list: the class matcher treats a pattern it cannot read as matching every changed path, so that a broken map never lets a change through unchecked. A stored { or [! would therefore gate every push by accident. Rejecting it at write time keeps the failure out of the gate.
Classes belong to GLOB watchpoints. A REGION watchpoint with classes is refused.
A refused write reaches you as a GraphQL error that says why, not a generic internal error (#19134). Create, update, delete and discovery upserts all use it:
| Cause | Code (extensions.code, reason) | What it says |
|---|---|---|
A HUMAN watchpoint, or a change of control, without a person | FORBIDDEN, HUMAN_WATCHPOINT | Only a signed-in workspace OWNER or ADMIN with a passkey step-up from the last 2 minutes can create or change a human-controlled watchpoint. An API token or agent credential cannot. |
| A pattern, class or shape the platform will not take | BAD_USER_INPUT, WATCHPOINT_INVALID | The validator's own text, such as the rule that braces and [! are not supported |
| The watchpoint changed while the write was checked | CONFLICT, WATCHPOINT_CHANGED | This watchpoint changed while your change was being checked. Reload it and try again. |
| The same repository, path, line range and source already exists | BAD_USER_INPUT, WATCHPOINT_EXISTS | A watchpoint with this repository, path, line range and source already exists. |
Any other failure is still reported as a generic internal error with a correlation id. Source: judge-api/watchpoint_write_errors.go.
How a change resolves to classes
The matcher is the open-source classmap package, the same code the platform and cilock verify run.
- Union. The classes of a change are every class of every matching entry. There is no ordering, no last-match-wins and no negation. Adding an entry can add a class to a path. It can never remove one.
- Old and new path. A rename matches on either path. A move shown as a delete plus an add matches through the delete.
- ASCII case is ignored.
Auth/Login.gomatchesauth/**. A change cannot dodge an entry by changing the case of a path. - An incomplete observation applies every mapped class. If the list of changed paths timed out or was too large, every class the map names applies. A class no entry names stays unrequired.
- An empty complete change hits nothing. A complete list with no paths touches no class.
- A change list nobody vouched for is incomplete.
Completedefaults to false.
Patterns support * and ? within one segment, [...] classes, and ** as a whole segment for any number of segments, zero included, so a/** matches a. The full spec is class-map.md in the open-source tree.
| Map | Changed path | Classes |
|---|---|---|
auth/** : critical | auth/login.go | critical |
deploy/** : critical, ops | deploy/prod.yaml | critical, ops |
auth/** : critical | Auth/Login.go | critical |
auth/** : critical | misc/login.go, renamed from auth/login.go | critical |
auth/** : critical | main.go | none |
Writing the policy step
A step in a policy of type https://aflock.ai/policy/v0.3 may carry appliesTo:
{
"expires": "2027-09-29T00:00:00Z",
"steps": {
"build": { "name": "build" },
"review": { "name": "review", "appliesTo": { "class": "critical" } }
}
}
build is always required. review is required only when the change touches critical. Steps that do not apply are dropped before any evidence is read. The step bodies here are trimmed to the parts that matter; a real step also names its attestations, functionaries and rules.
cilock sign and cilock policy publish choose v0.3 on their own when a step declares appliesTo. The engine refuses these shapes:
| Refusal | Cause |
|---|---|
class-scope-missing | The policy has class-scoped steps and the caller gave no class scope. A missing scope is never read as "no classes touched" |
all-steps-conditional | Every step is conditional, so a change touching no class would pass with no evidence |
conditional-step-referenced | A step reads a conditional step of another class |
applies-to-needs-policy-v0.3 | appliesTo in a policy of an older type |
What a class can require
The step you attach to a class decides the proof. Two kinds are in scope for phase 1.
A pinned model review. The existing code-review external (deploy/pushgate/code-review.policy.json) accepts a review only when the signer is an enrolled reviewer agent, the model, prompt and rubric digests match the policy's pins, the review binds the exact head and base commits, and the newest review decides. Put a step that requires it behind appliesTo to demand it for one class only. The reviewer pin is the only authenticated fact about the reviewer. The platform does not authenticate which model wrote the change, so "a different model than the author" is not enforced.
A human-signed review. A person produces a change-approval attestation with cilock attest --human -a change-approval. The attestor refuses any run not signed through the --human browser login (subtrees/rookery/plugins/attestors/change-approval/change_approval.go). You pass it the repository, the base and head commits, the digest of the observed changes, the digest of the policy release, and your email. A policy step can require it, because since #19151 the attestation names its head commit as <type>/commithash:<sha1>, the one shape an external's commitSubject can declare.
The shape that works is an optional external plus a class step that reads it:
{
"steps": {
"build": { "name": "build", "...": "always required" },
"review": {
"name": "review",
"appliesTo": { "class": "critical" },
"externalFrom": ["change-approval"],
"attestations": [{ "type": "...", "regopolicies": [ "deny unless input.external[\"change-approval\"] exists" ] }]
}
},
"externalAttestations": {
"change-approval": {
"name": "change-approval",
"predicateType": "https://testifysec.com/attestations/change-approval/v0.1",
"commitSubject": "https://testifysec.com/attestations/change-approval/v0.1/commithash:",
"required": false,
"regopolicies": [ "deny unless decision is approve" ]
}
}
}
The external is not required on its own, so a change that touches no critical path needs no approval. The review step reads it, so a critical change does. Seed the verification by the commit (cilock verify --subjects sha1:<head>). The review step also needs its own evidence collection, like any step.
This exact shape is tested in subtrees/rookery/attestation/policy/change_approval_class_step_test.go, with a key-signed stand-in for the person. It covers five cases: no critical path passes, an approval of this commit passes, no approval fails, a recorded rejection fails, and an approval of another commit fails. It does not cover the functionary. To require a person, pin the external's functionary to your platform's certificate authority with your approvers' emails and minassurancelevel: "aal2" (attestation/policy/constraints.go); I did not test that constraint with a real certificate.
Three things a policy step cannot check, so do not rely on a step for them: that the predicate's head_commit equals the pushed commit (the step's rule sees only the predicate, not the pushed commit), that its release_digest names the release being evaluated, and that the approver is not the change's author. Only the platform's own gate, change.sensitive-path-human-review, checks those (judge-api/pkg/pillarpolicy/library/change.sensitive-path-human-review/policy.rego).
What Pushgate shows in warn mode
Only repositories with a class-scoped step are affected. applicable-releases answers class_scoped_steps: true when any enforced release has a step with appliesTo; for every other repository the answer is unchanged.
For those repositories the gate observes the push's changed paths before the verdict, with a 3 second limit (jade/factory/edge/git/observeafter.js). If the observation is cut or fails, the platform treats the change as incomplete and applies every mapped class. An observation never refuses a push. The cost shows under the stage observe_front in the push's timing.
The platform's evaluate-release reads the repository's monitored GLOB watchpoints that carry classes, builds the class map, and selects the steps (handlers_pushgate_class_scope.go). It records which steps applied and did not, the classes touched, whether the list was incomplete, the class-map digest, and the matched watchpoints with their paths. If it cannot read the watchpoints, the verdict is a signed failure that names the cause; it is never read as "no classes". The record is for display and audit. The verdict versions do not sign it yet.
When a release fails, the gate adds one line in this form:
class-scoped: watchpoint auth/** (class critical) matched auth/login.go; steps applied: build, review
It names the watchpoint pattern, its classes, the matched paths (three at most) and the steps that applied. It does not say which step failed, and it is explanation only: the signed verdict decides. Where the observation was incomplete, it says the changes could not be fully observed, so every class-scoped step applies.
In phase 1 a failure here warns and the push goes through. Policy bindings already carry warn or block (#18756). Block mode for class-scoped steps is not built.
Agents and tokens
Agents can maintain SHARED watchpoints and their classes. They can never create, edit, delete or promote HUMAN ones, because that needs a passkey step-up that an API token cannot perform.
A dedicated watchpoint:write scope for agent tokens, so a token can write watchpoints without holding broader write access, is not yet opened. Do not assume it exists.
Verify the same thing offline
The matcher and the policy engine are open source. You can reproduce the platform's class decision without the platform:
cilock verify -p policy.signed.json -k policy-pub.pem \
--class-map class-map.json --changed-paths changed.json ./artifact
class-map.json pairs patterns with classes. changed.json is the change under verification:
{"complete": true, "changes": [
{"path": "auth/login.go", "status": "M"},
{"path": "misc/b.go", "old_path": "auth/a.go", "status": "R"}
]}
Give both flags or neither. Without "complete": true, every class-scoped step is required. Verify logs the map digest, the classes touched and the steps it evaluated.
Neither file is evidence. A verifier that takes the change list from the party being verified decides nothing. Compute it from the commits being verified, for example with git diff --name-status -M BASE HEAD, and treat a diff you could not read in full as incomplete. The map digest (sha256: over a canonical encoding of every pattern and class pair) lets two parties confirm they used the same map. The spec is class-map.md.
Compared to CODEOWNERS
Both map paths to a review requirement. They differ in how overlaps resolve, what is demanded, and how it is proved. The CODEOWNERS column is from GitHub's About code owners, read 2026-11-11.
| CODEOWNERS | Watchpoint classes | |
|---|---|---|
| Where it is read | The pull request's base branch | The platform's watchpoints for the repository, or a map you pass to cilock verify |
| Overlapping patterns | "The last matching pattern takes the most precedence", so a later line can remove an owner | Union of every match. Adding an entry never removes a class |
| Negation | ! does not work | None. Refused at write time |
| Character ranges | [ ] does not work | [...] works; [! is refused, write [^ |
| Braces | Not documented | Refused. One entry per alternative |
| Case | Paths are case sensitive | ASCII case ignored |
| What a line names | Who may approve: users or teams with write access | What is required: a class, which a policy step turns into evidence |
| Proof it was met | An approval in the pull request, enforced by the "Require review from Code Owners" branch rule. One owner's approval is enough | A signed attestation bound to the exact commit, checkable offline |
| Protecting the file itself | You must define an owner for the CODEOWNERS file | A HUMAN watchpoint needs an OWNER or ADMIN with a passkey step-up to edit |
| Unreadable or oversized input | A file over 3 MB is not loaded; a bad line is skipped | An unreadable pattern, or an incomplete change list, makes classes apply rather than vanish |
| Gate today | Enforced by GitHub | Warn only |
The last row is the honest one. CODEOWNERS blocks a merge today. Watchpoint policies, in phase 1, tell you.
With CODEOWNERS a later, more specific line is a way to ask for less review. Here the only way to ask for less is to narrow or delete a watchpoint, and for a HUMAN one that needs a person with a fresh passkey.
Limits worth knowing
- Warn only. No push is refused because of a class.
- The set is only as good as the observation. If the change list is incomplete, the gate applies every class, which warns more, not less.
- A SHARED watchpoint can be narrowed by any writer, an agent included. Mark what must not be quietly loosened as HUMAN.
- Classes do not name reviewers. Who may satisfy a step is in the signed policy.
- Open source verification reproduces the class decision. It does not reproduce the platform's watchpoint rows.