Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

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

KeyTypeDescription
expiresstringISO-8601 timestamp. Evaluation of expired policies always fails.
rootsobjectTrusted 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.
publickeysobjectTrusted public keys. Keys are the public key's Key ID (sha256 of the key, or KMS reference URI), values are a publickey object.
stepsobjectExpected steps that must appear to satisfy the policy. Keys are step names (must match cilock run --step <name>), values are a step object.
timestampauthoritiesobjectTrusted X.509 roots for RFC 3161 timestamp authorities. Same shape as roots.
externalAttestationsobjectBare-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

KeyTypeDescription
certificatestringBase64-encoded PEM block of the X.509 root certificate.
intermediatesarray<string>Base64-encoded PEM blocks of intermediate certificates belonging to certificate.

publickey object

KeyTypeDescription
keyidstringsha256 of the public key, or a KMS reference URI like awskms:///arn:aws:kms:... or gcpkms://projects/....
keystringBase64-encoded PEM-formatted public key. May be omitted when keyid is a KMS URI and online verification is acceptable.

step object

KeyTypeDescription
namestringStep name. Must match a cilock run --step <name> invocation that produced an attestation collection.
functionariesarray<functionary>Identities trusted to sign attestation collections for this step.
attestationsarray<attestation>Attestation types that must appear in the collection to satisfy this step.
artifactsFromarray<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).
allowedUntrackedarray<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.
requiredArtifactsarray<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.
attestationsFromarray<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).
externalFromarray<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).
timestampConstraintobjectTime 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

KeyTypeDescription
typestring"root" or "publickey".
certConstraintcertConstraint objectConstraints on the signer's X.509 certificate. Only valid when type = "root".
publickeyidstringKey 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.

KeyTypeDescription
commonnamestringRequired Common Name on the cert subject.
dnsnamesarray<string>Required DNS SANs.
emailsarray<string>Required email SANs.
organizationsarray<string>Required Organization fields on the subject.
urisarray<string>Required URI SANs, including SPIFFE IDs.
rootsarray<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

KeyTypeDescription
typestringAttestation 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.
regopoliciesarray<regopolicy>OPA Rego policies that will be run against the attestation. All must pass.
aipoliciesarray<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.

KeyTypeDescription
namestringLocal name; the same string used in any Step.externalFrom referencing this envelope. Surfaces as input.external.<name> to Rego policies.
predicateTypestringStatement 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.
functionariesarray<functionary>Identities trusted to sign this envelope. Same shape as a step's functionaries — public-key or X.509 with cert constraints.
regopoliciesarray<regopolicy>Rego policies evaluated against the bare predicate body (the Rego input is the predicate itself, not the surrounding Statement).
aipoliciesarray<aipolicy>AI policies evaluated against the bare predicate.
requiredboolWhen 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).
commitSubjectstringOptional. 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

KeyTypeDescription
namestringName of the rego policy. Reported on failure.
modulestringBase64-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 allow

A 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:

KeyContents
input.attestationThe step's own attestor JSON: the object that was the whole input in the plain shape.
input.steps.<step>.collectionsOne 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.tsaTimeThe 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:

PathWhose time
input.collection.tsaTimeThe collection this policy is evaluating.
input.steps.<step>.collections[].tsaTimeEach 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 deny body reads in a comparison, call, or assignment, for example input.reftype != "tag" or some f in input.findings (an empty list is fine, a missing one is not);
  • every negation cilock policy validate reports as never firing on a missing field, for example not startswith(input.reftype, "tag"), including one in a helper rule that deny reaches.

These spellings handle a missing field on purpose and are not refused:

You meanWrite
Treat a missing field as a valueobject.get(input, "reftype", "")
Deny when a field is missingnot input.reftype
Deny only when a field is presentinput.reftype alone as the first condition, then read it
Tell input shapes apartRead 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.

KeyTypeDescription
namestringHuman-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.
modelstringAI 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.
promptstringGenerative 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.
decisiondecisionTyped 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.

KeyTypeDescription
stateregopolicyOptional 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.
yesNoyesNoA boolean question scored as a probability.
choicechoiceA single selection from a fixed set of named options.
scorescoreAn ordinal score over a fixed ladder of levels.

yesNo

KeyTypeDescription
instructionsstringThe yes/no question put to the model.
criteriamap<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.
minProbabilitynumberThe probability of "yes" must be at least this, in [0,1].
maxProbabilitynumberThe probability of "yes" must be at most this, in [0,1]. 0 asserts impossibility.

choice

KeyTypeDescription
instructionsstringThe question put to the model.
optionsmap<string,string>The selectable options as id → description. Must be non-empty; the model answers with one id.
allowarray<string>Option ids that PASS. Every entry must be a key of options.
denyarray<string>Option ids that FAIL. Every entry must be a key of options.
minConfidencenumberThe chosen option's confidence must be at least this, in [0,1].

score

KeyTypeDescription
instructionsstringThe question put to the model.
levelsarray<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.
minScorenumberThe score must be at least this. Within [0, len(levels)-1].
maxScorenumberThe 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.

  1. Exactly one of prompt / decision.
  2. Exactly one of decision.yesNo / .choice / .score.
  3. model is required.
  4. 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.
  5. choice.options and score.levels are non-empty.
  6. Every choice.allow / choice.deny entry is a key of choice.options. An assertion naming an option the model can never return never fires.
  7. Probabilities and confidences are in [0,1]; minProbability <= maxProbability; minScore <= maxScore; score bounds within [0, len(levels)-1].
  8. name is non-empty and unique within its attestation.
Decision policies are not evaluated yet

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:

  1. Verify signatures on each collection against policy.publickeys and policy.roots. Anything failing signature verification is dropped. Same check runs against externalAttestations envelopes.

  2. Map signers to functionaries: each collection's signer must satisfy a functionary entry for the step. Same for each external envelope's functionaries.

  3. Verify timestamps (if present) against policy.timestampauthorities. The signing certificate must have been valid at the timestamped time.

  4. 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 no artifactsFrom step produced must match an allowedUntracked glob.

  5. Lift cross-step + external evidence into Rego context. For a step with neither attestationsFrom nor externalFrom, each Rego policy's input is the attestor it is attached to, unwrapped. For a step that declares either list, input becomes:

    • 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 in attestationsFrom.
    • input.external.<name>: the predicate body of each envelope named in externalFrom (not the surrounding Statement); absent when the envelope was skipped or missing.

    See What input looks like for both shapes with runnable examples.

  6. Evaluate every embedded Rego policy against its target. All deny rules must be empty.

  7. 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.json

Cross-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

Reference generated from the product documentation. Match commands and support details to your installed release.