Skip to main content
View Markdown ↗

Copy this page

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

On this page

Gate on Dagger checks

dagger check exits 0 when every check it was asked to run passed. That includes a run in which it was asked to run none of the tests: add test = ["!**"] under [modules.go.settings] in dagger.toml and it still prints ✔ PASSED. This guide records dagger check with the CI/lock dagger attestor, which reads Dagger's own OpenTelemetry trace, and writes a policy that asks what ran: the Go test check must appear, pass and select at least one test.

Two limits apply to everything below. The attestor reads span attributes of Dagger v1.0.0-beta.19, and Dagger 1.0 is a beta whose trace is not a published schema, so a later Dagger can rename what the attestor reads; a policy that requires a named check then refuses, it does not admit. And the evidence says what this machine's Dagger reported: test results are written from inside the test container, so the code under test can write them, and a cached check ran no tests in this run.

Every command and output on this page comes from runs on 28 and 29 October 2026 on an Apple M2 Max, with Dagger v1.0.0-beta.19 and a clone of testifysec/dagger-pushgate-demo, a small Go library with seven tests. The push through Pushgate in the last step was not part of those runs.

What you need

  • Dagger 1.0 beta and a container runtime it can start its engine in.

  • A CI/lock build that has the dagger attestor. CI/lock 4.6.2 does not; the attestor is on the main branch and not yet in a release. Check your binary:

    cilock attestors list | grep dagger
    │ dagger                   │ https://aflock.ai/attestations/dagger/v0.1           │ prematerial │
  • jq, to embed the policy's Rego modules.

  • For the last step, a repository behind Pushgate and an enrolled CI/lock agent.

1. Install Dagger 1.0 beta

curl -fsSL https://dl.dagger.io/dagger/install.sh | DAGGER_VERSION=1.0.0-beta.19 BIN_DIR=$HOME/.local/bin sh
dagger version
version:     v1.0.0-beta.19
commit:      80699ac6
dirty:       no
platform:    darwin/arm64/v8
runner-host: tcp://127.0.0.1:12345

Our runner-host points at an engine already running in a container on the same machine (_EXPERIMENTAL_DAGGER_RUNNER_HOST). Yours will show the engine image Dagger starts for you.

2. Keep the trace on your machine

The attestor starts an OTLP receiver on 127.0.0.1 before the wrapped command and points OTEL_EXPORTER_OTLP_ENDPOINT at it. That receiver makes no outbound connection. Dagger itself can also send its trace to Dagger Cloud, which is a separate path. Turn it off for these runs:

export DO_NOT_TRACK=1 XDG_CONFIG_HOME=$(mktemp -d)

DO_NOT_TRACK=1 turns off Dagger's telemetry, and an empty XDG_CONFIG_HOME means the CLI finds no Dagger Cloud login. The attestor records whether DO_NOT_TRACK was set, as doNotTrack.

3. Record a run

git clone https://github.com/testifysec/dagger-pushgate-demo demo && cd demo
openssl genpkey -algorithm ed25519 -out ../key.pem
cilock run --step dagger-check -a environment,git,dagger \
  --signer-file-key-path ../key.pem --offline \
  --outfile dagger-check.att.json -- dagger check
TRACE  ✔ PASSED
dagger check  1.5s

CHECKS  ✔ 2 passed
✔ dag://?check=go/packages/generate/stale&go-package=cmd/semver&go-package=semver 0.5s OK
✔ dag://?check=go/packages/tests/test&go-package=semver&go-test=TestSuiteLoad&go-test=TestParse&go-test=TestParseRejects&go-test=TestSpecOrdering&go-test=TestBuildMetadataIgnored&go-test=TestNumericBeforeAlphanumeric&go-test=TestMajorDominates 0.4s OK
cilock run summary:
  step:       dagger-check
  anchor:     git remote https://github.com/testifysec/dagger-pushgate-demo
  signer:     file
  ...
  attestor:   environment — ran
  attestor:   git — ran
  attestor:   dagger — ran
  command exit: 0

-a replaces the default attestor set, so git is named too: its commit subject is what Pushgate matches evidence on. --offline and a file key keep this run on the machine; step 6 signs with your enrolled agent instead.

Read the dagger predicate back:

jq -r '.payload' dagger-check.att.json | base64 -d | jq '.predicate.attestations[] | select(.type | endswith("/dagger/v0.1")) | .attestation | {daggerVersion, traceReceived, doNotTrack, summary, goTests: [.checks[] | select(.check == "go/packages/tests/test") | {passed, cached, selected: (.params["go-test"] | length), tests: (.tests | length)}]}'
{
  "daggerVersion": "v1.0.0-beta.19",
  "traceReceived": true,
  "doNotTrack": true,
  "summary": {
    "checks": 2,
    "checksPassed": 2,
    "checksFailed": 0,
    "checksCached": 2,
    "tests": 0,
    "testsPassed": 0,
    "testsFailed": 0
  },
  "goTests": [
    {
      "passed": true,
      "cached": true,
      "selected": 7,
      "tests": 0
    }
  ]
}

This run was a cache hit: the engine had run the same tests on the same source earlier, so cached is true and no test results are in the trace. The check's link still names the seven selected tests. On a run that executes the tests, tests lists each one with its result; the attestor page shows one.

4. Write the policy

Two Rego modules. The first requires dagger check to exit 0:

.pushgate/rules/command-succeeded.rego
package dagger_command_succeeded

import rego.v1

deny contains msg if {
	not is_number(object.get(input, "exitcode", null))
	msg := "unreadable evidence: command-run has no numeric exitcode"
}

deny contains msg if {
	code := object.get(input, "exitcode", 0)
	code != 0
	msg := sprintf("dagger check exited %v", [code])
}

The second reads the dagger predicate. It is the example policy on the attestor page, which the attestor's own tests run against recorded Dagger traces of a passing run, a cache hit, a failing test, tests switched off and zero checks. Save it as .pushgate/rules/dagger-go-tests.rego.

The policy names the step, the two attestation types and a functionary. Replace YOUR-TENANT-ID with your tenant; the empty root and timestamp authority are placeholders the platform fills when your human signs.

.pushgate/policy.json
{
  "expires": "2027-10-28T00:00:00Z",
  "roots": {"fulcio-root": {"certificate": ""}},
  "timestampauthorities": {"platform-tsa": {"certificate": ""}},
  "steps": {
    "dagger-check": {
      "name": "dagger-check",
      "functionaries": [
        {
          "type": "root",
          "certConstraint": {
            "commonname": "*",
            "dnsnames": ["*"],
            "emails": ["*"],
            "organizations": ["*"],
            "uris": ["spiffe://platform.testifysec.com/tenant/YOUR-TENANT-ID/agent/*"],
            "roots": ["fulcio-root"]
          }
        }
      ],
      "attestations": [
        {
          "type": "https://aflock.ai/attestations/command-run/v0.2",
          "regopolicies": [{"name": "command-succeeded", "module": ""}]
        },
        {
          "type": "https://aflock.ai/attestations/dagger/v0.1",
          "regopolicies": [{"name": "dagger-go-tests", "module": ""}]
        }
      ]
    }
  }
}

Embed the modules:

jq --rawfile run .pushgate/rules/command-succeeded.rego --rawfile dag .pushgate/rules/dagger-go-tests.rego \
  '.steps["dagger-check"].attestations[0].regopolicies[0].module = ($run | @base64) | .steps["dagger-check"].attestations[1].regopolicies[0].module = ($dag | @base64)' \
  .pushgate/policy.json > policy.tmp && mv policy.tmp .pushgate/policy.json

5. Prove it offline

cilock policy prove records the step for real, signs it with a throwaway key, and checks that the policy admits it and refuses the same step wrapping a command that fails. Nothing is uploaded.

cilock policy prove -p .pushgate/policy.json --step dagger-check -- dagger check
Local verify: passed
  step dagger-check: real run admitted; failing run refused (dagger check exited 1)
  validate: passed as an unsigned draft; roots.fulcio-root, timestampauthorities.platform-tsa are empty platform placeholders the platform fills when your human signs
  the throwaway key, evidence and scratch-signed policy were deleted; nothing was uploaded, signed for real, or activated

Now switch the tests off and prove again:

printf '\n[modules.go.settings]\ntest = ["!**"]\n' >> dagger.toml
cilock policy prove -p .pushgate/policy.json --step dagger-check -- dagger check
Local verify: REFUSED by dagger-check: no go/packages/tests/test check in the dagger run (are the tests filtered out in dagger.toml?)

Dagger reported ✔ PASSED for that run and exited 0, so the command-succeeded module alone would have admitted it. Put dagger.toml back before you go on (git checkout dagger.toml).

6. Activate the policy

prove ends with the handoff:

Next: hand .pushgate/policy.json to your human: open https://pushgate.dev/policy/new?mode=manual, choose Import or paste, press Import a file and pick it, then Save and Validate.

Your human imports, signs and activates the policy; an agent never does. After that the loop on each change is: commit, record the step as in step 3 but signed by your enrolled agent (drop --signer-file-key-path and --offline), and push. Pushgate admits the push when the evidence for that commit satisfies the policy. See the Pushgate documentation for enrolling the agent and the first signed push.

We have not yet pushed through Pushgate with this policy. The steps above stop at cilock policy prove.

Limits

  • A cache hit is admitted on Dagger's word. The policy accepts a passing Go test check with cached: true, which ran no tests in this run. Delete the not c.cached condition from the check ran but reported no passing test rule to require that the tests ran every time, at the cost of a full test run on every push.
  • The trace is the evidence. The signature binds what this machine's Dagger reported. Test results come from inside the test container, so code under test can write them.
  • Dagger 1.0 is a beta. The attestor reads span names and attributes of v1.0.0-beta.19. Check the record (step 3) again after you upgrade Dagger.
  • The policy pins one check by name. It requires go/packages/tests/test. A module that names its test check differently needs its own rule.

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