Policy schema
A CI/lock policy is a signed DSSE document that declares which attestation collections must appear, which functionaries are trusted to sign each step, and which OPA Rego rules must pass against attestation contents.
This page mirrors the witness policy schema, since CI/lock and witness use the same policy format. Source of truth:
witness/docs/concepts/policy.md. Cilock-specific notes are called out where they exist.
DSSE wrapper
Policies are JSON documents wrapped in a DSSE envelope and signed with cilock sign. CI/lock accepts two payloadType values for the same schema:
https://aflock.ai/policy/v0.1 # current canonical
https://witness.testifysec.com/policy/v0.1 # legacy (still accepted; default for `cilock sign --datatype`)The default of cilock sign --datatype is the legacy witness type for backward compatibility; witness-signed policies verify under CI/lock unchanged. Both constants live at rookery/attestation/policy/policy.go (PolicyPredicate, LegacyPolicyPredicate); CI/lock's verifier accepts either in rookery/cilock/internal/policy/validate.go.
Top-level policy object
| Key | Type | Description |
|---|---|---|
expires | string | ISO-8601 timestamp. Evaluation of expired policies always fails. |
roots | object | Trusted X.509 root certificates. Keys are the root certificate's Key ID (sha256 of the cert), values are a root object. Used for X.509 functionaries. |
publickeys | object | Trusted public keys. Keys are the public key's Key ID (sha256 of the key, or KMS reference URI), values are a publickey object. |
steps | object | Expected steps that must appear to satisfy the policy. Keys are step names (must match cilock run --step <name>), values are a step object. |
timestampauthorities | object | Trusted X.509 roots for RFC 3161 timestamp authorities. Same shape as roots. |
externalAttestations | object | Bare-predicate DSSE envelopes verified as first-class policy evidence (SLSA provenance, VSAs, cosign attestations, inclusion-proofs). Keys are local names referenced by Step.externalFrom, values are an externalAttestation object. See §externalAttestation. |
root object
| Key | Type | Description |
|---|---|---|
certificate | string | Base64-encoded PEM block of the X.509 root certificate. |
intermediates | array<string> | Base64-encoded PEM blocks of intermediate certificates belonging to certificate. |
publickey object
| Key | Type | Description |
|---|---|---|
keyid | string | sha256 of the public key, or a KMS reference URI like awskms:///arn:aws:kms:... or gcpkms://projects/.... |
key | string | Base64-encoded PEM-formatted public key. May be omitted when keyid is a KMS URI and online verification is acceptable. |
step object
| Key | Type | Description |
|---|---|---|
name | string | Step name. Must match a cilock run --step <name> invocation that produced an attestation collection. |
functionaries | array<functionary> | Identities trusted to sign attestation collections for this step. |
attestations | array<attestation> | Attestation types that must appear in the collection to satisfy this step. |
artifactsFrom | array<string> | Names of upstream steps. The materials of this step must match the products of every step listed here — chain-of-custody verification across the supply chain (per-file digest, evaluated independently of Rego). |
allowedUntracked | array<string> | Only meaningful with artifactsFrom. Glob patterns (gobwas/glob syntax, / separator: * and ? stay within one segment, ** crosses segments; the literals on either side of ** never overlap, so vendor/**/x.go does not match vendor/x.go; a run of three or more * is **, and {} or an empty alternative such as {,.exe} matches the empty string; a pattern that does not compile, such as an unclosed {, makes the whole policy invalid, never a pattern that matches nothing) for material paths this step may consume without an upstream step having produced them. Patterns match the path exactly as the material attestor recorded it: relative to the working directory in walk mode, absolute in trace mode. The path is lexically cleaned first. Empty means strict: every material must be an upstream product or material with a matching digest. cilock verify (--policy-hardening=enforce, the default) and the Judge platform enforce this through policy.EnforcedHardening(). An embedder that never calls policy.SetHardening only logs a warning. A pattern never excuses a digest mismatch on a path the upstream step did produce. Every entry is a hole in the chain of custody, so keep the list narrow. |
requiredArtifacts | array<string> | Only valid with artifactsFrom. Glob patterns (gobwas/glob, / separator). Each must match a material path that an accepted artifactsFrom step also recorded, so its digest was compared. Without it, artifactsFrom is satisfied by any one shared path, such as a system library. Use it to pin the artifact the step exists to consume, e.g. /tmp/build/cilock{,.exe} for a signing step. Record that artifact under the upstream path with --attestor-material-bind RECORDED_PATH=FILE. An absent match fails closed. |
attestationsFrom | array<string> | Names of other steps whose collected attestations are lifted into this step's Rego evaluation context as input.steps.<step>.<predicateType>. Use when a step's Rego rule must reference data produced by a sibling step (e.g. a release-gate rule that reads the SBOM emitted in the scan step). |
externalFrom | array<string> | Names of bare-predicate envelopes declared in the top-level externalAttestations map. Each referenced predicate is lifted into the step's Rego context as input.external.<name>. Use when a policy must reference DSSE envelopes whose predicate is not wrapped in a CI/lock Collection — SLSA provenance, VSAs, cosign attestations, inclusion-proofs (the inclusion-proof attestor is a bare predicate and must be wired this way, not via step.attestations). |
timestampConstraint | object | Time requirement on the step's evidence, judged against the RFC 3161 TSA time verified on a signature that matched one of the step's functionaries, never a time the signer wrote into its payload. notBefore and notAfter (RFC 3339 instants) bound the earliest such time; maxAge (a Go duration such as 168h) rejects evidence older than that at verification. A collection with no verified TSA time fails any constraint that is set. For a deadline between two steps (evidence B signed within N days of evidence A), compare tsaTime values in Rego instead; see "Verified signing times" below. |
functionary object
| Key | Type | Description |
|---|---|---|
type | string | "root" or "publickey". |
certConstraint | certConstraint object | Constraints on the signer's X.509 certificate. Only valid when type = "root". |
publickeyid | string | Key ID of a trusted public key (must appear in policy publickeys). Only valid when type = "publickey". |
certConstraint object
Every attribute must match the certificate exactly. A certificate must satisfy at least one constraint to pass. * is allowed as a wildcard if it's the only element in the array.
| Key | Type | Description |
|---|---|---|
commonname | string | Required Common Name on the cert subject. |
dnsnames | array<string> | Required DNS SANs. |
emails | array<string> | Required email SANs. |
organizations | array<string> | Required Organization fields on the subject. |
uris | array<string> | Required URI SANs, including SPIFFE IDs. |
roots | array<string> | Trust roots (Key IDs from policy roots) the cert must chain to. |
Wildcard constraint (allow any cert from a trusted root)
{
"commonname": "*",
"dnsnames": ["*"],
"emails": ["*"],
"organizations": ["*"],
"uris": ["*"],
"roots": ["*"]
}SPIFFE ID constraint
{
"commonname": "*",
"dnsnames": ["*"],
"emails": ["*"],
"organizations": ["*"],
"uris": ["spiffe://example.com/step1"],
"roots": ["*"]
}attestation object
| Key | Type | Description |
|---|---|---|
type | string | Attestation predicate type URL. Cilock-native types use https://aflock.ai/attestations/<name>/v0.1; legacy witness types https://witness.dev/attestations/<name>/v0.1 are also accepted via aliases. SBOM attestations use the native CycloneDX (https://cyclonedx.org/bom) or SPDX (https://spdx.dev/Document) URI. See attestor catalog. |
regopolicies | array<regopolicy> | OPA Rego policies that will be run against the attestation. All must pass. |
aipolicies | array<aipolicy> | AI-evaluated policies that will be run against the attestation predicate. In the generative form, each policy sends the predicate body to the AI server configured via --ai-server-url and expects \{"status":"PASS","reason":"..."\} back; in the typed decision form the model answers a constrained question and the policy derives the verdict. All must return PASS. Names must be unique within this attestation. See §aipolicy. |
externalAttestation object
A bare-predicate DSSE envelope (not wrapped in a Collection) that the policy treats as first-class evidence. Used for SLSA provenance, VSAs, cosign attestations, and the inclusion-proof attestor — anything whose envelope payload is a single in-toto Statement whose predicate is the attestation body itself, with no surrounding Collection.
| Key | Type | Description |
|---|---|---|
name | string | Local name; the same string used in any Step.externalFrom referencing this envelope. Surfaces as input.external.<name> to Rego policies. |
predicateType | string | Statement predicateType URI to match — e.g. https://slsa.dev/provenance/v1, https://in-toto.io/attestation/vsa/v1, https://aflock.ai/attestations/inclusion-proof/v0.1. |
functionaries | array<functionary> | Identities trusted to sign this envelope. Same shape as a step's functionaries — public-key or X.509 with cert constraints. |
regopolicies | array<regopolicy> | Rego policies evaluated against the bare predicate body (the Rego input is the predicate itself, not the surrounding Statement). |
aipolicies | array<aipolicy> | AI policies evaluated against the bare predicate. |
required | bool | When true (default), verification fails if no matching envelope is supplied. When false, absence is tolerated and the named predicate is simply absent from input.external. Use false for optional evidence (e.g. an inclusion-proof that's only emitted on demand). |
commitSubject | string | Optional. The exact subject-name prefix that names a commit for this external, e.g. https://pushgate.dev/v0.1/commithash: for a Pushgate VSA. SHA-1 subjects are refused as match keys by default (they are not collision-resistant). Setting this lets one signed subject spelled exactly <commitSubject><40-hex sha>, with a sha1 digest of that same value, match a requested commit (cilock verify --subjects sha1:<sha>), and under commit binding (--commit <sha>) binds the envelope to that commit. The prefix is compared case-exact; only the hex digest is case-folded; the null object id never matches. It applies to this external only (a second external of the same predicateType without it gets no SHA-1 match) and never to an attestation collection. Accepted shape: printable ASCII with no whitespace, at most 256 bytes, an absolute URL with a lower-case scheme and a host, ending in /commithash:. |
A policy may have no steps when at least one external attestation is required: the external is then the whole gate (for example, admitting a commit on its Pushgate VSA). A policy with no steps and only optional externals is refused by cilock policy validate and fails verification, because it would verify nothing.
When a required external is not found, or every candidate is refused, the error names the predicate type, the searched subjects as algorithm:value (the algorithm inferred from the digest's length), how many candidates the search returned, and, for up to five refused candidates, their subjects and the reason each was refused. A candidate refused only because its matching subject is SHA-1 says so and names the commitSubject value that would admit it.
regopolicy object
| Key | Type | Description |
|---|---|---|
name | string | Name of the rego policy. Reported on failure. |
module | string | Base64-encoded Rego module. |
The Rego module must export a deny rule. deny should be a string or array of strings, populated only when the policy fails. Anything else the module outputs is ignored. Modules are parsed as Rego v0 by default; add import rego.v1 at the top of a module that uses the if, contains or in keywords.
Deny-only. The verifier queries deny and nothing else. A collection passes when no module denies it. An allow rule is never queried, so default allow := false over an empty deny passes everything. To gate on an allow-style condition, make deny depend on it:
package example.gate
import rego.v1
default allow := false
allow if input.verificationResult == "PASSED"
deny contains "verification did not pass" if not allowA module that defines allow when no deny rule reaches it, directly or through helper rules, is refused at cilock policy validate and at verification.
What input looks like
input is the attestor's own JSON: the predicate body the attestor registered, marshaled as-is. For a step with neither attestationsFrom nor externalFrom there is no wrapper of any kind, so a command-run policy reads input.cmd and input.exitcode at the top level:
package commandrun.exitcode
deny[msg] {
input.exitcode != 0
msg := sprintf("build exited with status %d", [input.exitcode])
}As soon as a step lists anything in attestationsFrom or externalFrom, the verifier re-shapes input for every Rego policy on that step into these keys:
| Key | Contents |
|---|---|
input.attestation | The step's own attestor JSON: the object that was the whole input in the plain shape. |
input.steps.<step>.collections | One entry per passed collection of each step in attestationsFrom: {reference, name, attestations} with attestations keyed by predicate type URI. Ordered by collection reference, so a rule sees every passed collection and its order does not depend on which source answered first. Read this. |
input.steps.<step>.<predicateType> | Deprecated. The attestor of that type from the first collection in collections order only. Kept so existing policies keep verifying; the verifier logs a deprecation warning per module that reads it. A rule that must hold for every run of a step cannot be written against this key. |
input.external.<name> | The predicate body of each envelope in externalFrom that passed. An external that was skipped or never supplied is absent, so not input.external.<name> fires. |
input.collection.tsaTime | The verified signing time of the collection being evaluated. See "Verified signing times" below. Absent when that collection has no verified TSA time. |
The switch is keyed on the step declaring the lists, not on the referenced data being present: a dependency that has not verified yet still produces the wrapped shape, with an empty input.steps. A top-level path such as input.exitcode is undefined under the wrapped shape, so a module written for the plain shape stops matching the moment its step gains an attestationsFrom entry, and the verifier refuses the collection rather than let the silent deny pass it (see "Missing fields" below). Move its reads under input.attestation. The verifier logs a warning whenever the wrapped shape is active.
package deploy.provenance
# The deploy step's own command-run attestation moved under input.attestation.
deny[msg] {
input.attestation.exitcode != 0
msg := sprintf("deploy exited with status %d", [input.attestation.exitcode])
}
# Steps named in attestationsFrom: input.steps.<step>.collections[] (preferred) or the
# deprecated input.steps.<step>.<predicateType> (first passed collection only).
deny[msg] {
c := input.steps.build.collections[_]
c.attestations["https://aflock.ai/attestations/command-run/v0.2"].exitcode != 0
msg := sprintf("build collection %v exited non-zero", [c.reference])
}
deny[msg] {
build := input.steps.build["https://aflock.ai/attestations/command-run/v0.2"]
build.cmd[0] != "go"
msg := sprintf("build step ran %v, expected a go build", [build.cmd])
}
deny[msg] {
not input.steps.build["https://aflock.ai/attestations/command-run/v0.2"]
msg := "build step provided no command-run attestation"
}
# Envelopes named in externalFrom: input.external.<name>, absent when not supplied.
deny[msg] {
not input.external.releaseApproval.approved
msg := "release approval missing or not granted"
}Both modules above are extracted from this page and run through the real verifier by attestation/policy/rego_input_shape_doc_test.go, so they cannot drift from what cilock verify actually passes in.
What input is. With no attestationsFrom/externalFrom on the step, input is the JSON of the registered attestor struct, the same bytes signed inside the collection. For most attestors that puts the predicate's fields at the top level: input.exitcode (command-run), input.commithash (git), input.findings (secretscan). Four attestors register a struct that wraps the predicate in a predicate field, so their fields are one level down: test-results, steampipe, scubagoggles, and structured-data are read as input.predicate.<field>, for example input.predicate.summary.failed, not input.summary.failed. Rego treats an undefined path in a deny body as "this rule does not fire", so the verifier refuses an admit that rests on such a read (see "Missing fields" below); a flat read against a wrapped attestor is refused, not passed. Check the shape with cilock tools show <name> (the attestor page states it under "Rego input shape") or by base64-decoding the attestation in a real collection; cilock policy validate warns when a module bound to test-results/v0.1 reads a top-level predicate field.
Verified signing times
Every time inside an attestation (starttime, endtime, a scanner's endTimeUtc) was written by the signer and proves nothing about when the evidence existed. The time the verifier does trust is the RFC 3161 timestamp on the envelope's signature. Rego sees it under the wrapped shape as tsaTime, in two places:
| Path | Whose time |
|---|---|
input.collection.tsaTime | The collection this policy is evaluating. |
input.steps.<step>.collections[].tsaTime | Each passed collection of a step in attestationsFrom. |
The value is Unix nanoseconds, a number, so two of them subtract directly. It is the earliest timestamp that verified against the policy's timestamp authorities, taken only from signatures whose verifier matched a functionary of that collection's step: the same time timestampConstraint checks. Nothing in the payload can supply it, because the signer's attestor JSON sits under input.attestation and collections[].attestations, never beside tsaTime.
When a collection has no such timestamp the key is absent. A deny that reads it is then refused under "Missing fields" below, so a missing time never reads as zero and never passes a deadline.
The plain (unwrapped) shape has no tsaTime: there the whole input is the signer's JSON. A step that needs its own time without depending on another step uses timestampConstraint.
This is how a deadline between two events is written. The example requires every evaluation to be signed within 7 days of each scan the evaluate step reads through attestationsFrom: ["scan"]:
package ver.evaluation_deadline
import rego.v1
seven_days_ns := ((7 * 24) * 3600) * 1000000000
deny contains msg if {
some scan in input.steps.scan.collections
delay := input.collection.tsaTime - scan.tsaTime
delay > seven_days_ns
msg := sprintf("evaluation signed %v ns after scan %v, over the 7-day deadline", [delay, scan.reference])
}attestation/policy/tsa_time_rego_test.go runs this module through Policy.Verify: 6 days and exactly 7 days pass, 7 days plus one second fails, and a missing time on either side fails.
A timestamp shows that the signature existed by that time; it does not show when the underlying event happened. A deadline checked on tsaTime holds for the real events only if signing follows the event within a known bound, so leave that bound as margin in the rule.
Missing fields
A deny rule whose body reads a field the attestation does not carry is undefined, so it never fires, and a deny-only engine would read that silence as a pass. The verifier does not. When no module denies, it checks the input paths the admit depended on, and it refuses the collection with an error naming the path if any is missing:
- every input path a
denybody reads in a comparison, call, or assignment, for exampleinput.reftype != "tag"orsome f in input.findings(an empty list is fine, a missing one is not); - every negation
cilock policy validatereports as never firing on a missing field, for examplenot startswith(input.reftype, "tag"), including one in a helper rule thatdenyreaches.
These spellings handle a missing field on purpose and are not refused:
| You mean | Write |
|---|---|
| Treat a missing field as a value | object.get(input, "reftype", "") |
| Deny when a field is missing | not input.reftype |
| Deny only when a field is present | input.reftype alone as the first condition, then read it |
| Tell input shapes apart | Read the optional field in a helper rule, and have deny read the helper |
There is no setting that turns this off.
aipolicy object
An aipolicy has two mutually exclusive forms. Exactly one of prompt or decision must be set — both, or neither, is a policy error and verification refuses before any request leaves the process.
| Key | Type | Description |
|---|---|---|
name | string | Human-readable name; reported on failure. Must be non-empty and unique within an attestation — it is the question id when several questions go to the model in one request. |
model | string | AI model name to evaluate against. Required; there is no default. Pinned for every backend: the response must name this exact model as the one that answered (the generative backend reads model from the /api/generate response), and a different or missing model refuses evaluation with ErrAIEvaluationRefused (model_mismatch), never a PASS or FAIL. |
prompt | string | Generative form. Free text sent to the AI model along with the predicate body. The AI is required to reply with a JSON object \{"status":"PASS|FAIL","reason":"..."\}. Mutually exclusive with decision. |
decision | decision | Typed form. The model answers a constrained question; the POLICY decides PASS/FAIL from the answer. Mutually exclusive with prompt. See §decision. |
Library callers configure the endpoint with WithAiServerURL and select the typed Jev backend with WithAiProvider(NewJevProvider(apiKey)). Credentials are operator configuration, never policy fields. The default provider remains the legacy generative backend. This library API does not enable AI policies in Pushgate or introduce a CLI flag.
The Jev backend sends all questions with the same projected state and pinned model in one request to /v1/systemone. It requires an exact version such as jev-1.13.0; a different resolved response model refuses evaluation. Remote endpoints require HTTPS; HTTP is allowed only for numeric loopback addresses. Redirects and retries are disabled. Each request has a one-second timeout within a two-second batch budget, also bounded by the caller's context. Input encoding is limited to 8 MiB, requests and responses to 1 MiB, and each batch to 128 questions. Oversized evidence is refused, never silently truncated.
A well-formed answer is evaluated against the signed assertions locally. Provider errors, malformed answers, and max_tokens_exceeded produce ErrAIEvaluationRefused, not a negative finding or a completed PASS/FAIL verdict. Author-written instructions are supported; presets are optional. Authors must still qualify their questions against representative positive, negative, and adversarial evidence before relying on them.
Generative example
{
"name": "no-secrets-in-diff",
"model": "llama3",
"prompt": "Does this diff introduce a hardcoded credential? Return PASS if it does not."
}decision object
The generative form asks the model to be the judge: it returns the verdict, and the policy takes its word for it. The typed form splits those jobs. The model answers a constrained question — a probability, a choice from a fixed list, an ordinal score — and the policy turns that answer into PASS/FAIL using assertions written into the signed policy. What the gate accepts is therefore readable from the policy alone.
Exactly one of yesNo, choice or score must be set.
| Key | Type | Description |
|---|---|---|
state | regopolicy | Optional Rego projection selecting the part of the attestor the question is about. For Jev, the module must define state in its declared package (data.<package>.state); the result must be a string, object, or array. Undefined or null results refuse evaluation. Network and nondeterministic builtins are unavailable. Omitted means the whole attestor is the question's state. |
yesNo | yesNo | A boolean question scored as a probability. |
choice | choice | A single selection from a fixed set of named options. |
score | score | An ordinal score over a fixed ladder of levels. |
yesNo
| Key | Type | Description |
|---|---|---|
instructions | string | The yes/no question put to the model. |
criteria | map<string,string> | Named clarifications the model must weigh. Jev requires exactly true and false, each with a nonblank description. Jev yes/no answers have no confidence field; minConfidence is invalid for this type. |
minProbability | number | The probability of "yes" must be at least this, in [0,1]. |
maxProbability | number | The probability of "yes" must be at most this, in [0,1]. 0 asserts impossibility. |
choice
| Key | Type | Description |
|---|---|---|
instructions | string | The question put to the model. |
options | map<string,string> | The selectable options as id → description. Must be non-empty; the model answers with one id. |
allow | array<string> | Option ids that PASS. Every entry must be a key of options. |
deny | array<string> | Option ids that FAIL. Every entry must be a key of options. |
minConfidence | number | The chosen option's confidence must be at least this, in [0,1]. |
score
| Key | Type | Description |
|---|---|---|
instructions | string | The question put to the model. |
levels | array<string> | The ordered ladder of levels, lowest first. Must be non-empty. Jev requires at least two nonblank levels and returns a probability-weighted index, which can be fractional. |
minScore | number | The score must be at least this. Within [0, len(levels)-1]. |
maxScore | number | The score must be at most this. Within [0, len(levels)-1]. |
Decision example
{
"name": "tamper-risk",
"model": "jev-1.13.0",
"decision": {
"yesNo": {
"instructions": "Does this command-run attestation show the build step executing a command it did not declare?",
"criteria": {
"true": "a process in the trace whose argv is absent from the declared step command",
"false": "every observed process is explained by the declared step command"
},
"maxProbability": 0.05
}
}
}And with a choice:
{
"name": "change-risk-tier",
"model": "jev-1.13.0",
"decision": {
"choice": {
"instructions": "Classify the risk of this diff.",
"options": {
"routine": "docs, tests, comments",
"behavioural": "changes runtime behaviour",
"security": "touches auth, crypto, or a trust boundary"
},
"deny": ["security"],
"minConfidence": 0.7
}
}
}Rules the verifier enforces
Every rule below fails closed, and all of them are checked before any AI request is made.
- Exactly one of
prompt/decision. - Exactly one of
decision.yesNo/.choice/.score. modelis required.- Every decision kind sets at least one assertion (
minProbability/maxProbability,allow/deny/minConfidence,minScore/maxScore). A decision that asserts nothing about the answer would pass whatever the model said — an unasserted gate reads as coverage while providing none. choice.optionsandscore.levelsare non-empty.- Every
choice.allow/choice.denyentry is a key ofchoice.options. An assertion naming an option the model can never return never fires. - Probabilities and confidences are in
[0,1];minProbability <= maxProbability;minScore <= maxScore; score bounds within[0, len(levels)-1]. nameis non-empty and unique within itsattestation.
The typed shape is accepted, validated and signed, but no backend answers a constrained question today. A policy carrying decision is refused at verification time with no provider configured for decision policies — it is never silently treated as a pass. Only the generative prompt form is evaluated by the shipped Ollama-compatible provider.
Verification process
cilock verify runs the following checks in order, all must pass:
-
Verify signatures on each collection against
policy.publickeysandpolicy.roots. Anything failing signature verification is dropped. Same check runs againstexternalAttestationsenvelopes. -
Map signers to functionaries: each collection's signer must satisfy a functionary entry for the step. Same for each external envelope's functionaries.
-
Verify timestamps (if present) against
policy.timestampauthorities. The signing certificate must have been valid at the timestamped time. -
Verify materials/products consistency: the materials of each step must match the products of any step in
artifactsFrom. (Per-file digest match, independent of Rego.) Under enforcement, a material that noartifactsFromstep produced must match anallowedUntrackedglob. -
Lift cross-step + external evidence into Rego context. For a step with neither
attestationsFromnorexternalFrom, each Rego policy'sinputis the attestor it is attached to, unwrapped. For a step that declares either list,inputbecomes:input.attestation: the step's own attestor (the one the policy is attached to), as a single object.input.steps.<step>.<predicateType>: every attestor from each step named inattestationsFrom.input.external.<name>: the predicate body of each envelope named inexternalFrom(not the surrounding Statement); absent when the envelope was skipped or missing.
See What
inputlooks like for both shapes with runnable examples. -
Evaluate every embedded Rego policy against its target. All
denyrules must be empty. -
Evaluate every embedded AI policy against its target. All must return
{"status":"PASS"}. The AI server must be reachable; AI policies fail closed.
Exit code 0 on pass, non-zero on any failure.
Worked example
A two-step policy where clone produces source files, build produces a binary, and the build's command-run is constrained by a Rego rule that the build command must be exactly go build -o=testapp .:
{
"expires": "2030-12-17T23:57:40-05:00",
"steps": {
"clone": {
"name": "clone",
"attestations": [
{ "type": "https://aflock.ai/attestations/material/v0.3" },
{ "type": "https://aflock.ai/attestations/command-run/v0.2" },
{ "type": "https://aflock.ai/attestations/product/v0.3" }
],
"functionaries": [
{ "type": "publickey", "publickeyid": "ae2dcc..." }
]
},
"build": {
"name": "build",
"artifactsFrom": ["clone"],
"attestations": [
{ "type": "https://aflock.ai/attestations/material/v0.3" },
{
"type": "https://aflock.ai/attestations/command-run/v0.2",
"regopolicies": [
{
"name": "expected command",
"module": "cGFja2FnZSBjb21tYW5kcnVuLmNtZAoKZGVueVttc2ddIHsKCWlucHV0LmNtZCAhPSBbImdvIiwgImJ1aWxkIiwgIi1vPXRlc3RhcHAiLCAiLiJdCgltc2cgOj0gInVuZXhwZWN0ZWQgY21kIgp9Cg=="
}
]
},
{ "type": "https://aflock.ai/attestations/product/v0.3" }
],
"functionaries": [
{ "type": "publickey", "publickeyid": "ae2dcc..." }
]
}
},
"publickeys": {
"ae2dcc...": {
"keyid": "ae2dcc...",
"key": "<base64 PEM>"
}
}
}The base64 module above decodes to:
package commandrun.cmd
deny[msg] {
input.cmd != ["go", "build", "-o=testapp", "."]
msg := "unexpected cmd"
}Sign this policy with cilock sign before distribution:
cilock sign --signer-file-key-path policy-key.pem -f policy.json -o policy-signed.jsonCross-step + external-evidence example
A release-gate policy that pulls a build step's products through to a release step's Rego, and additionally requires a separately-signed inclusion-proof envelope whose treeRoot matches the build's product/v0.3 Merkle root:
{
"expires": "2030-12-17T23:57:40-05:00",
"externalAttestations": {
"binaryInclusionProof": {
"name": "binaryInclusionProof",
"predicateType": "https://aflock.ai/attestations/inclusion-proof/v0.1",
"functionaries": [{ "type": "publickey", "publickeyid": "ae2dcc..." }],
"required": true
}
},
"steps": {
"build": {
"name": "build",
"attestations": [
{ "type": "https://aflock.ai/attestations/product/v0.3" },
{ "type": "https://aflock.ai/attestations/command-run/v0.2" }
],
"functionaries": [{ "type": "publickey", "publickeyid": "ae2dcc..." }]
},
"release": {
"name": "release",
"attestationsFrom": ["build"],
"externalFrom": ["binaryInclusionProof"],
"attestations": [
{
"type": "https://aflock.ai/attestations/command-run/v0.2",
"regopolicies": [{ "name": "inclusion-proof binds build artifact", "module": "<base64>" }]
}
],
"functionaries": [{ "type": "publickey", "publickeyid": "ae2dcc..." }]
}
},
"publickeys": { "ae2dcc...": { "keyid": "ae2dcc...", "key": "<base64 PEM>" } }
}The Rego module on the release step reads:
package release.gate
deny[msg] {
build_root := input.steps.build["https://aflock.ai/attestations/product/v0.3"].merkleRoot
proof_root := input.external.binaryInclusionProof.treeRoot
build_root != proof_root
msg := sprintf("inclusion-proof root %s does not match build merkleRoot %s", [proof_root, build_root])
}The full worked example — including the build/scan steps and the cilock verify recipe — lives at multi-step-attestationsFrom in the examples repo.
See also
- Policy verification — the verification model
- The spine of the graph — how cross-step links resolve via subject digests
- Verify in a release gate — practical recipes
- witness/docs/concepts/policy.md — upstream reference (CI/lock mirrors)
Reference generated from the product documentation. Match commands and support details to your installed release.