kosli evaluate runs client-side: the policy lives in your repository, the check
runs on your CI runner, and Kosli only learns the outcome if you explicitly attest it back —
a self-reported result Kosli takes on trust. Server-side policy evaluation moves the Policy
Decision Point (PDP) into Kosli. You ship a versioned policy to Kosli, Kosli executes it
against facts it already holds, and Kosli records the decision itself, along with the exact
policy version and inputs it used.
Start here
Publish a policy, then evaluate it
Two commands, which the rest of this page fills in:my-release-trail and records a decision against control
RCTL-043, citing the policy version, the parameters, and the exact facts it used. --flow and
--trail say where the decision is recorded, as on any other Kosli command, and --context says
which trail’s facts to evaluate — the same trail here, a different one when you need it. --assert
fails the pipeline step if the policy denies. Everything else on this page is detail around those
two commands.
Adding --control is what records a decision. Omit it and Kosli still runs the policy and keeps
the result, but writes no decision — an
ad-hoc evaluation you can inspect without adding anything
to the audit trail. That makes this one command two things: a way to check a policy against real
facts, and a way to claim a control is satisfied.
What is in the beta, and what is not
In: publishing versioned policies, evaluating them against a trail or trails, running evaluations that record no decision, recording decisions against controls, validating and testing policies, and reviewing or exporting the decisions a policy has produced. Not in: evaluating anything other than trails, evaluating the contents of attachments, composing several published policies into one evaluation, scheduled evaluations, pinning an evaluation to a policy version, and advisory-only outcomes. See the full list, with reasons.Where to go next
Author and ship policies
For governance platform engineers. Publishing and evaluating, the Rego contract, testing, and
managing policies over time.
Audit the decisions
For auditors and control owners. Review, filter, sample, and export decisions — and re-run one
yourself to check it.
Fix a failing evaluation
For application teams. Understand why a policy denied and what to do about it.
This is distinct from the existing client-side
Evaluate trails with OPA policies tutorial, where the
CLI runs the policy locally. Here, Kosli stores the policy and performs the evaluation.
Why server-side
Running evaluation inside Kosli changes who computed the decision — and where the data is processed:
Because Kosli both stores the policy and runs it, a decision points at exactly what ran — the
policy version and its content digest, its parameters, and the trail facts that were in scope. The
record is complete enough to reconstruct the evaluation, which is what makes the decision
trustworthy for audit: you can take a decision, export what it was given, and run the policy again
yourself — see Re-run a decision offline.
Running evaluation next to the data is also faster and scales further: Kosli doesn’t have to
ship a trail’s full evidence out to your runner before evaluating, so evaluations can cover much
larger volumes of facts in the cloud.
Prerequisites
These prerequisites describe the proposed beta experience — they are not runnable today.
- Install Kosli CLI.
- Get a Kosli API token.
- Have at least one Control defined, and a Flow with a Trail carrying the attestations you want to evaluate.
Author, ship, and evaluate a policy
This section takes a policy from local Rego to a recorded decision. The later sections build on it — validating and managing policies, and viewing and inspecting the decisions they produce.1
Author a policy locally
Author your policy as code in your own repository. A policy is its policy-as-code (Rego), a
human-readable name, a short description of its purpose, and an input schema
declaring whether it evaluates a single trail or multiple trails. You give it a stable
identifier when you publish it to Kosli (next step).Your repository stays authoritative for authoring a policy — it is where the Rego is written,
reviewed, and tested. Kosli is authoritative for what executed: once a version is published,
that stored version is what runs and what a decision cites, so the audit record never depends on
your repository still being reachable or unchanged.The result schema requires an Note the second
allow rule; you can return more alongside it. Follow the same
fail-safe design rules as client-side policies — see
Evaluate trails with OPA policies for the reasoning behind
them. The full contract a policy is written against — entry point, input, parameters, available
built-ins, and result — is in The Rego authoring contract.pr-approved.rego
violations rule. Without it, a trail that is missing the attestation entirely
leaves both allow and the first violations rule undefined — a denial with an empty violations
list and nothing for the team to act on. Cover the missing-fact case explicitly, so every way the
policy can deny produces something to fix.Before you ship it, you can validate and test the policy against
real trail data.In the beta, a policy evaluates the facts on the trail(s) you point it at — attestations on
trail / trails. Evaluating other targets (snapshots, artifacts, generic input) and
composing multiple policies into one are not included (see What’s out of scope).2
Ship a versioned policy into Kosli
Publish the policy into Kosli via the CLI (or API). The positional argument is the policy’s
stable identifier (
pr-approved in the example below) — this is what decisions and evaluations
use to refer to the policy, so it is immutable once created; choose one that matches how your
organization names its policies. --name and --description are the human-readable label and
purpose, and both can be changed later without changing the identifier.Pass a single .rego file, as in the example below, or a directory when the policy is split across
several files. A directory holds one policy’s bundle, not a collection of separate policies —
Kosli stores the whole set as one versioned bundle with one digest, so a decision still points at one
unambiguous thing. Tests — files named *_test.rego, following OPA’s convention — are stored with
the bundle but never executed server-side.Kosli records the first version on publish. Every later change creates a new version and
records who made it, so a decision can always point back to the exact policy that produced it —
see Manage policies for updating, tagging, and archiving over time.Only org admins can publish a policy. Creating, updating, and archiving a policy all require
org admin, the same as controls. Running an
evaluation and recording a decision does not — an ordinary API token or service account is enough,
so pipelines can invoke a policy without being able to change one. That split means the team being
audited can run the control, but only an admin can decide what the control checks.kosli create policy is the same command used for environment policies:
--type rego selects an evaluation policy, while the default --type env creates an environment
policy. The positional argument differs between the two — for --type env it is the
policy name, while for --type rego it is the immutable identifier described above. In the
beta, authoring is CLI/API only — the Kosli UI is read/tag-only.3
Evaluate and record a decision
With You then either fetch that evaluation (see
Check an asynchronous evaluation) or, more usually in a
pipeline, add The flags that carry the meaning, in two groups:
Evaluating facts from another flow. Usually The decision records which flow and trail the facts came from, so a reviewer can always see that a
release decision was reached from a build flow’s evidence rather than from the release trail itself.
kosli evaluate policy, Kosli fetches the in-scope facts and runs the policy server-side.
Passing --control additionally records a decision against that control, which is what this
step does; omitting it runs the evaluation on its own.Evaluation is asynchronous by default — the command returns once the evaluation is queued,
naming the evaluation that will carry the outcome:--sync to wait for the outcome, as the example below does.--policy and --context are always required — they are the evaluation. The destination
group — --control, --flow, --trail, --name, and the artifact — is required together when
you record a decision, and rejected when you don’t. --flow is the one exception: it may be
passed on its own, because a bare trail=<trail_id> reference resolves against it.--context names the trail whose facts are read; --flow and --trail name the trail the
decision is written to. They are often the same trail, as above — see the Evaluating facts
from another flow example below for when they are not.Use --assert in place of --sync when the step should also fail on a denial — that is how an
evaluation becomes a pipeline gate. Once recorded, a decision appears against the policy — see
View the decisions for a policy.All flags for kosli evaluate policy
All flags for kosli evaluate policy
More on
--context. In the beta the only reference type is a trail: trail=<trail_id> names a
trail in --flow, and trail=<flow_id>/<trail_id> names one in another flow. Repeat the flag to
evaluate several trails — see Evaluating multiple trails. Kosli
resolves these references into the evaluation a decision cites;
they are not a cluster or account selector. The typed form leaves room for other context types —
snapshots and reported workloads, for example — later, without new flags.--context and --flow / --trail name the same
trail. Because each --context reference carries its own flow
(trail=<flow_id>/<trail_id>), you can evaluate a trail in a different flow — for example,
evaluating a build flow’s trail but recording the decision on a release flow’s trail:The server captures the evidence for you. Because Kosli runs the evaluation, the decision
automatically records the policy (and its version), the evaluation report, and any violations —
you don’t attach them by hand with
--attachments / --user-data as you do today when you
attest a client-side result yourself. Likewise, --compliant is derived from the policy’s
allow result rather than passed in. This is the core difference from
recording a decision yourself.The decision this records is the same kind of decision described in
Working with controls — the difference is that Kosli
computed it server-side rather than you attesting the outcome yourself. Enforcement is
unchanged: require the control via
for_control in an environment policy
and gate promotion with kosli assert artifact --environment,
exactly as before. Recording the decision is the PDP; asserting it is the PEP.4
View and trace the decision
As soon as the evaluation completes, the decision appears in Kosli against the control and the
policy that produced it. Open it in the Kosli app to see the outcome, and
the evaluation it was reached from:
A decision carries two timestamps. The requested time is when the evaluation was enqueued;
the recorded time is when the decision was written, once the evaluation ran. Kosli fixes the
trail moment — which attestations are in scope — at the requested time. The trail data itself is
only queried and serialized once the evaluation executes, but always as of that fixed timestamp,
not as of whatever the trail contains by then — so a queued evaluation is deterministic no matter
how long it waits. Kosli never backdates the record.For example, evaluation
ev_7f3a91c2 is requested at 09:14:22 and executes at 09:14:25. If a new
pull-request approval landed on the trail at 09:14:24 — after the request, before execution — it is
still not part of this decision: the query at 09:14:25 reads the trail as it stood at 09:14:22,
not as it stands when the evaluation happens to run. If that new fact changes the answer, it takes a
new evaluation to say so.Because the policy version, parameters, and trail moments are all pinned to that requested time,
the decision states exactly what was evaluated and when it applied — enough to
re-run it offline. To review and filter every decision a policy has
produced, see View the decisions for a policy.Check an asynchronous evaluation
Without--sync, the outcome arrives on the evaluation the command named. An evaluation exists
from the moment it is queued, so you can ask about it before there is any outcome — or any
decision — to look at:
STATUS is queued until the evaluation runs, then completed or failed — see
When evaluation itself fails. Branch on STATUS, not on whether a
field is present: the response carries the same keys at every status, with the ones that aren’t known
yet returned empty, so a client written against a completed evaluation still parses a queued one.
Once it is completed, the evaluation names the decision it produced, if any, so a queued evaluation
and the decision it becomes are one hop apart in both directions.
An evaluation reaches a terminal status once. Its result is written a single time and never
revised, so completed and failed are final — re-evaluating creates a new evaluation rather than
changing this one.
Add --wait to block until the evaluation is terminal instead of polling for it:
--show-input behaves as it does on the client-side
kosli evaluate trail, so jq '.input' extracts the
policy input and kosli evaluate input consumes it unchanged.
Evaluate without recording a decision
Leave--control off and Kosli runs the policy exactly as it would otherwise — same facts, same
pinning, same recorded result — but writes no decision:
--trail, --name,
--fingerprint, --description, and --annotate all require a decision to be recorded. --flow
is accepted on its own, since a bare trail=<trail_id> reference resolves against it.
--assert works here too, and gates the pipeline on the outcome without recording a decision. That
is a reasonable way to try a policy against real pipelines before you let it close a control — but it
is not a control: the evaluation records that the check ran, and nothing records that the control was
satisfied.
Evaluating multiple trails
A policy published with--input-schema trails evaluates several trails at once and receives
them as input.trails. Repeat --context to name each one:
trail-schema policy is rejected before the evaluation is queued, as is passing one reference to a
trails-schema policy. Several trails are read, but the decision is still recorded on exactly one
trail — the one named by --flow and --trail.
The Rego authoring contract
Kosli evaluates your policy with OPA, so the tooling you already use locally —opa fmt,
opa check, opa test — works on these policies unchanged. What Kosli fixes is the contract
around the policy: the entry point it evaluates, the shape of input, the parameters and
built-ins a policy may use, and the result Kosli reads back.
Everything in this section is proposed and open for comment. If a constraint here would stop
you shipping a control you need, that is the most useful feedback you can give us.
Entry point
A policy declarespackage policy, and Kosli evaluates data.policy.allow. Keeping the package
fixed means the CLI, the server, and your own opa test cases all agree on where the entry point
is, and it is what lets your tests import the policy under test as policy.allow.
What the policy sees in input
A policy evaluates the attestations in the slots on the trail — nothing else. Trail events
(trail started, trail updated) are not attestations and are not in input, and neither are
attestations from earlier moments of the same trail. Slots are matched by name, not by
attestation type.
Attachments are not opened. A policy sees an attestation’s own fields, not the bytes of the
files attached to it, so it cannot parse a SARIF file, an SBOM, or a PDF report to reason about
individual findings. To evaluate that content, summarize it into JSON at attestation time — the
counts, severities, or package identifiers the policy needs — using whichever field the attestation
type gives you for structured data:
- For a custom attestation type you
define, pass the summary as
--attestation-data, validated against your type’s schema. This is the better fit when the summary is a recurring shape worth its own schema. - For a built-in type (
generic,snyk,sonar, and others) with no schema field for your summary, pass it as--user-datainstead.
--attachments. The summary is what the policy
reads; the attachment is what lets an auditor go back to the file the summary was derived from and
check that it is faithful.
The --input-schema you declare when you publish the policy selects the shape of input:
Those schemas are published as JSON Schema so you can type-check a policy against
input before
you ship it. Associate one with your rules using an OPA
metadata annotation:
pr-approved.rego
opa check resolves schema["trail-v1"] to trail-v1.json in
the directory you pass to --schema:
allow at its false default. --strict additionally
rejects unused imports and unused local assignments.
Schemas give you type checking while authoring; they are not how Kosli validates a policy on
publish. Kosli performs its own validation — see
Validate and test a policy.
Parameters
Parameters are the only input to a policy from outside the trail. They arrive asdata.params,
and every evaluation records the parameters it used, so a decision always shows what the policy
was run with.
data.params paths the Rego depends on, and validates the parameters you supply at
evaluation time — an unknown parameter name or a missing one is rejected before the evaluation is
queued, by the CLI where it can be caught locally and by the server as the actual gate.
This closes off a failure that is otherwise very hard to spot. Supplying maxHigh to a policy that
reads max_high leaves the alias undefined, so allow falls back to false and the violations
rules that reference the same alias don’t fire either — the result is a recorded non-compliant
decision with no violations explaining it, and a failed pipeline with nothing to fix. Rejecting the
parameters up front turns that into an immediate, obvious error.
Derivation reads parameter paths statically, so a policy that computes parameter names at
evaluation time —
data.params[key] for some derived key — cannot be checked this way, and Kosli
falls back to accepting whatever you supply. Prefer literal parameter names.Result
Give
allow a default allow := false and drive it through positive assertions, so an
evaluation that goes wrong denies rather than allows.
Available built-ins
Because Kosli executes the policy, evaluation runs with no network access and must produce the same decision every time it is re-evaluated. Both constraints are expressed as an OPA capabilities file, which you can check a policy against locally before publishing:Evaluation time
A policy cannot read the clock, because a decision that depends on when it happened to run is not replicable. Instead, Kosli injects the evaluation’s request timestamp — the moment the decision is valid as of — asdata.kosli.evaluation_time, in nanoseconds since the Unix
epoch, so it can be compared directly against a parsed attestation timestamp:
approval_timestamp is whichever timestamp the attestation you are evaluating carries — the
field depends on the attestation type. Time windows work as normal, they are anchored to the moment
the decision applies to, and re-evaluating the stored context produces the same answer.
Engine and Rego version
Kosli pins the OPA version it evaluates with and records it on every decision alongside the policy version, so a decision states exactly what ran. Author against that version locally and check withopa check --v1-compatible to avoid a policy that compiles on your machine but not in Kosli.
When evaluation itself fails
A policy that times out, exhausts its memory budget, or hits a runtime error is a broken policy, not a non-compliant artifact, and Kosli reports the two differently — a failed evaluation records a typed error rather than a--compliant=false decision, and --assert exits with a distinct
code so a pipeline can tell “this change is not compliant” from “this policy is broken”.
The typed error lands on the evaluation, so a failure is visible without --sync or
--assert — the distinction holds in the asynchronous path too:
queued indefinitely: Kosli writes a typed abandoned error against it after a timeout,
so every evaluation reaches a terminal status.
A built-in that errors aborts the evaluation rather than evaluating to undefined — an undefined
built-in would deny with no violations, which is the outcome this design avoids everywhere else.
print and trace output is discarded rather than captured on the report; express diagnostics
through violations or a custom result field instead.
Validate and test a policy
Before you ship a policy — or wire it into a pipeline gate — check that it holds up.Validate
kosli validate policy checks that a policy is well-formed without recording anything. It
confirms that:
- the Rego compiles — a bundle is validated as a whole, not file by file;
- it matches its declared input schema (
trail/trails); - it satisfies the result schema — an
allowrule is mandatory.
--policy takes a single .rego file, as here, or a directory holding the policy’s bundle —
the same forms kosli create policy and kosli evaluate policy accept, so you can validate
exactly what you are about to publish.
Evaluate a local policy
You don’t have to publish a policy before it can evaluate anything. Point--policy at a local,
unpublished .rego file — or at a directory holding the policy’s bundle — and Kosli evaluates it
server-side just as it would a stored policy. This is how you iterate on a policy you haven’t
shipped, against the facts it will really run on:
--policy file and --control are mutually
exclusive. Publishing is what puts a policy through the
org-admin gate, so a decision from unpublished Rego would let anyone who can record a decision supply
the code behind it. Publish a version — a no-op if nothing changed — and the same
evaluation records a decision.
Manage policies
Update a policy
Publishing a change is the same command as the initial publish: re-runkosli create policy with
the updated Rego, name, or description. Each publish creates a new version and records who made
it — add --comment to note what changed — so a decision always points back to the exact version
that produced it. The identifier never changes.
0:
terraform apply: a pipeline
that republishes an untouched policy leaves the history alone instead of filling it with identical
versions.
What counts as a change:
Comparison is against the current version, not the whole history. So republishing the bundle
from an earlier version is a real change and does create a new version — that is how you roll back,
and the history records that the rollback happened rather than hiding it.
Review a policy change
A policy is the code that decides whether a control passes, so a change to it is a change to your control environment. Reviewing that change is part of the lifecycle — author, test, version, review, ship — and in the beta the review happens in your policy repository, not in Kosli. The practice we recommend, and the one Kosli’s records are designed to corroborate:1
Keep policies in a protected repository
Require pull requests, and require review from the people accountable for the control — a
CODEOWNERS entry per policy is the simplest way to make sure the right control owner sees a change
to their control. Restrict who can push directly.2
Make the control owner's question answerable at review time
The question a control owner has to answer is not “is this valid Rego?” but “does this policy
actually meet the control objective?” Reviewing the change alongside its
tests is what makes that answerable — the tests state, in cases, what
the policy is claiming to enforce.
3
Publish only from CI, after review
Because only org admins can publish, publish from a CI job
running with an admin credential on merge, rather than by hand from a laptop. The reviewed commit
then becomes the published version, and the two cannot drift.You can publish every policy on every merge without thinking about which ones changed —
publishing an unchanged policy is a no-op, so only the policies you actually
edited get a new version.
4
Point the version back at its review
Use
--comment to reference the change record, so Kosli’s version history and your review history
join up:There is no review or approval step inside Kosli in the beta: a policy version is live as soon as
it is published, and there is no draft state or second-approver requirement. If your control
environment needs approval to be enforced by Kosli rather than by your repository, tell us — it
affects how much of this belongs in the product.
Tag a policy
Tags are versionless labels for organizing and filtering policies — grouping by team, framework, or control area. Tagging does not create a new version.Archive and unarchive
When a policy is retired, archive it rather than deleting it. Archiving is light-touch: the policy is removed from the main list in the UI but remains evaluable, and its versions and past decisions are preserved. Archiving records who archived it and when, and does not create a new version. Unarchive it if it comes back into use.List and filter policies
Browse your policies in the Kosli app — select Policies in the sidebar to see the full catalog. Search by name or identifier, and filter by tag to narrow the list. Each policy shows its identifier, current version, and tags.--type rego, and by
name or tag — each entry shows the identifier and current version:
View a policy
Select a policy to open it. The Policy tab is the default view: it shows the policy-as-code that Kosli actually runs, read-only, for the version you choose — not a name and description of it..rego file or several, so the tab lists the files in the version and
shows the one you select. Alongside them it shows what an auditor needs to check the record: the
bundle digest a decision cites, the bundle’s roots (the data paths it owns), the
entrypoint that Kosli evaluates, the declared input schema, and the parameters the
policy reads. Tests shipped with the policy are listed and marked not executed — Kosli stores them
as evidence the policy was tested, but only the entrypoint runs.
Policies are read-only in the Kosli UI. Edit them in your repository and publish a new version with
kosli create policy.
View the decisions for a policy
This is where auditors and control owners review what a policy has decided. Every decision a policy produces is listed against that policy, across all versions of the policy. In the Kosli app, select Policies, choose a policy, and open its Decisions tab. Each row shows the timestamp, the trail and artifact the decision applies to, the policy version that produced it, and the outcome (compliant or non-compliant). Filter by outcome, version, or time range, and page through the results to sample them.--compliant=false surfaces every non-compliant evaluation for follow-up; combine it with
--since or a version filter to scope the export.
Each row shows the requested and recorded timestamps, so an export is enough to sample decisions
by when they applied rather than only by when they were written.
The same filtered sampling and CSV export are proposed for a control’s Decisions tab, so an
auditor can sample by control as well as by policy. That is not part of the
controls feature today.
Evaluations that record no decision do not appear here, since this view lists decisions — in the
beta you reach an ad-hoc evaluation by its ID.
Re-run a decision offline
Sampling a decision usually raises the question of whether it was right. Everything the evaluation was given is held on the evaluation itself, which the decision names — so you can export it and evaluate the policy again yourself, with stock OPA, on your own machine:kosli get decision, so the evaluation ID is one hop from the decision you are looking at.
The export contains what the evaluation was given, as ordinary OPA inputs:
Evaluate it with OPA directly.
--bundle loads the whole policy directory, so the same command
works whether the policy was one file or many:
Inspect a failing evaluation
When a policy denies, an application team needs to know why — and the decision records it through itsviolations. Whether the policy’s rules weren’t met (for example, a pull request with
no approver) or an expected attestation was missing from the trail, both surface as violations on
the decision.
A policy can return more than allow and violations — any additional fields in its result
are captured on the decision, so you can surface richer, human-readable context beyond a bare
pass/fail.
When you gate a pipeline with --assert, Kosli prints the violations as markdown so failures are
readable directly in CI logs:
Out of scope for the beta
The following are explicitly not included in the beta and may be addressed later. They are listed so you can tell what has been deferred from what has not been considered — if something here blocks you, that is worth telling us.- Evidence-attachment content evaluation — evaluating the contents of attached files. Summarize
the attachment into JSON at attestation time —
--attestation-datafor a custom attestation type,--user-dataotherwise — and a policy can evaluate that instead — see What the policy sees ininput. - Composable policies — combining several separately published policies, each with its own identifier and version, into one evaluation. This is distinct from a bundle, where one policy spans several Rego files under a single identifier, version, and digest. Bundles are supported.
- Non-trail evaluate targets — snapshots, artifacts, and generic input.
- Scheduled evaluations and risk measurement.
- Associating policies to controls — a stored policy-to-control link that lets Kosli infer
which policy closes which control. In the beta you name the control per evaluation with
--control; there is no stored mapping. - Pinning an evaluation to a policy version — an evaluation always uses the policy’s current version. There is no way to hold a pipeline on version 3 while version 4 is rolled out.
- Re-running an evaluation in Kosli — you cannot replay a stored evaluation, either through a different policy or version to compare outcomes, or as-is to check that it still reaches the same answer. Export the fixture and run it locally.
- Advisory outcomes — a decision is compliant or non-compliant. There is no advisory or warn-only mode that records a concern without failing the control.
- Verbose evaluation tracing — beyond
violationsand any custom result fields the policy returns, there is no rule-by-rule trace of how a policy reached its answer. - Filtering decisions by branch or environment — decisions can be filtered by outcome, policy version, and time, but not by where the change came from or where it was heading.
Next steps
- Working with controls — define controls and enforce them with
for_controlandkosli assert. - Environment policies — require controls before an artifact runs in an environment.
- Enforce policies — gate promotion on compliance in your pipeline.
- Evaluate trails with OPA policies — the client-side
kosli evaluatethis builds on.
Feedback
This is a design preview. If you are reviewing the proposed beta flow, the most useful feedback is on the command surface (kosli evaluate policy and how policies are published),
the Rego authoring contract — particularly any constraint there
that would stop you shipping a control you need — the decision and audit model, and anything in
What’s out of scope that you would expect in the beta. Share
comments with the Kosli team.