Skip to main content
View Markdown ↗

Copy this page

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

On this page

dagger attestor

Namedagger
Predicate typehttps://aflock.ai/attestations/dagger/v0.1
Lifecycleprematerial
Default binary?No
Categoryunit-test (primary)
Recommended traceoff — no syscall tracing needed
Auto-attaches when
  • preargv_prefix: dagger check

The facts in this box are generated from the CI/lock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.

When to use

You run your checks with Dagger (dagger check) and want a policy to ask what ran, instead of trusting the exit code or scraping the text report. The dagger attestor records each check's name, whether it passed, whether Dagger served it from cache, the tests it reported, and a digest of the source content it read.

The exit code is not enough. A dagger.toml that filters the tests out ([modules.go.settings] test = ["!**"]) makes dagger check print ✔ PASSED and exit 0, because every check it was asked to run passed and it was asked to run none of the tests. The record shows that the Go test check is missing, so a policy can refuse it.

Tested with Dagger v1.0.0-beta.19 and the dagger.io/go module. Dagger 1.0 is a beta; see Limits.

How it works

dagger check exports an OpenTelemetry (OTLP) trace to whatever OTEL_EXPORTER_OTLP_ENDPOINT names. The attestor runs before the wrapped command (it is a prematerial attestor): it opens an OTLP/HTTP receiver on 127.0.0.1 at a random port and sets these variables in the environment the wrapped command inherits:

VariableValue
OTEL_EXPORTER_OTLP_ENDPOINThttp://127.0.0.1:<port>
OTEL_EXPORTER_OTLP_TRACES_ENDPOINThttp://127.0.0.1:<port>/v1/traces
OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOLhttp/protobuf

After the command exits, the attestor stops the receiver, puts the variables back as they were, and builds the record from the spans it received. A value you had set for one of these variables is replaced for the run, with a warning.

It reads the trace, not the --progress report text, because the trace carries structured attributes (dagger.io/check.name, dagger.io/check.passed, the cache outcome of every container exec, the sha256 content digest of every host directory a check read) and the report is a display format with no schema.

Nothing leaves the machine through the attestor. The receiver listens on the loopback interface only, makes no outbound connection, and keeps the trace in memory. It accepts log and metric exports and discards them.

Dagger Cloud is a separate path. If the Dagger CLI is logged in to Dagger Cloud, or telemetry is on, Dagger can also send the trace there. Run with DO_NOT_TRACK=1 and an empty XDG_CONFIG_HOME so it does not. The attestor records whether DO_NOT_TRACK was set (doNotTrack), so a policy can require it.

CLI example

From a real run on 28 October 2026 (Dagger v1.0.0-beta.19, a clone of testifysec/dagger-pushgate-demo):

export DO_NOT_TRACK=1 XDG_CONFIG_HOME=$(mktemp -d)
cilock run --step dagger-check -a environment,git,dagger \
  --signer-file-key-path key.pem --offline \
  --outfile dagger-check.att.json -- dagger check

The run summary lists the attestor and one subject per check (abridged):

cilock run summary:
  step:       dagger-check
  anchor:     git remote https://github.com/testifysec/dagger-pushgate-demo
  signer:     file
  subjects (11): dagger/v0.1/dagger:input:dag://?check=go/packages/generate/stale&go-package=cmd/semver&go-package=semver, dagger/v0.1/dagger:input: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, git/v0.1/commithash:6e6cbce2bfd7694913477b3edc673d7f40baeb7b, ...
  attestor:   environment — ran
  attestor:   git — ran
  attestor:   dagger — ran
  command exit: 0

-a replaces the default set (environment,git,platform), so name git too: the commit subject is what a commit-bound gate such as Pushgate matches. When you pass no -a, --workload auto attaches the dagger attestor for a command that starts with dagger check.

Output shape

The dagger/v0.1 predicate, from the run above with the Go tests executed (abridged to one check):

{
  "daggerVersion": "v1.0.0-beta.19",
  "traceReceived": true,
  "doNotTrack": true,
  "invocations": [{"command": "dagger check --progress report", "status": "ok", "durationMs": 4317}],
  "checks": [
    {
      "name": "dag://?check=go/packages/tests/test&go-package=semver&go-test=TestSuiteLoad&go-test=TestParse&...",
      "check": "go/packages/tests/test",
      "params": {"go-package": ["semver"], "go-test": ["TestSuiteLoad", "TestParse", "..."]},
      "passed": true,
      "status": "ok",
      "durationMs": 3519,
      "cached": false,
      "processes": [["otelgotest", "./semver"]],
      "inputDigests": ["sha256:2e8e24530a73a7005e502dea61b6638c807095d645d01af4f26cbe6a67e7588a", "sha256:b90c34157620defb326b682c1295f28a884eab4c80121893589028da2afba2c6"],
      "inputKey": "sha256:c3c0f211a756c42884a8d7a2e082b4a70b739f4d0948e8d809d3cce140cfc514",
      "tests": [
        {"suite": "github.com/testifysec/dagger-pushgate-demo/semver", "name": "TestBuildMetadataIgnored", "result": "pass"}
      ]
    }
  ],
  "summary": {"checks": 2, "checksPassed": 2, "checksFailed": 0, "checksCached": 1, "tests": 7, "testsPassed": 7, "testsFailed": 0},
  "receiver": {"requests": 2, "spans": 648, "truncated": false}
}
FieldMeaning
daggerVersionthe Dagger CLI release that exported the trace (dagger.io/client.version); comma-joined if the command ran more than one
traceReceivedat least one trace export decoded. false means the wrapped command sent no trace: it did not run Dagger, or ran it with the variables overridden
doNotTrackDO_NOT_TRACK was set (not empty, 0 or false) when the command started
invocations[]one per Dagger CLI command in the trace: command, status (ok, error, unset), durationMs
checks[].namethe check's full dag:// link; check is its path, params its other query parameters
checks[].passedDagger's dagger.io/check.passed is true and the span status is not ERROR
checks[].cachedaffirmative cache evidence: at least one Container.withExec under the check was a cache hit, none executed, and no process started. A check with no exec observations at all is false
checks[].processesargv of each process the check started
checks[].inputDigests, inputKeysha256 content digests of the host directories the check read, and sha256 over them joined by newlines. Same content, same key
checks[].tests[]suite, name, result (pass, fail, skip, or unknown) for each test the check's runner reported
summarycounts over checks and their tests
receivertrace exports decoded (requests), spans decoded (spans), whether the 1,048,576-span or 256 MiB cap was hit (truncated), and exports refused with the reason (rejected, at most 16)

Dagger batches. One check span can cover several selections. In the run above a single go/packages/tests/test check ran all seven tests listed in its go-test parameter, and reports each under tests.

A cache hit reports no tests. When Dagger serves the test exec from cache, the test runner does not run, so cached is true and tests is empty. The go-test parameter still lists the selected tests. The tests ran in an earlier run with the same inputKey.

Subjects: dagger:input:<check link> for each check with an input key, with the input key as its sha256 digest. A rebased commit whose check read the same content has the same subject, so earlier evidence for that content can be found by it. It is a lookup subject: it finds evidence and never vouches for its value, so a verdict is never bound to it.

Example policy

The Go test check ran, passed and selected at least one test; a run that executed the tests reported at least one passing test and no failing one. It refuses a failing test, a dagger.toml that filters the tests out, a run with zero checks, and a capture the receiver truncated or partly refused. The attestor's tests run this exact module through cilock's verifier against recorded Dagger traces of each case.

package dagger_go_tests

import rego.v1

# The Go test check must appear, pass, and select at least one test. A run
# that executed the tests must report at least one passing test and no
# failing one. A cache hit reports no tests (the runner did not run), so it
# is admitted on Dagger's cache claim; drop the `c.cached` branch to require
# that the tests ran in this very run.

go_test_check := "go/packages/tests/test"

checks := object.get(input, "checks", [])

go_checks := [c | some c in checks; c.check == go_test_check]

deny contains msg if {
	count(checks) == 0
	msg := "dagger ran zero checks"
}

# An incomplete capture can drop a failing check, so it proves nothing.
deny contains msg if {
	object.get(input, ["receiver", "truncated"], true) != false
	msg := "the dagger trace was truncated or the receiver record is missing"
}

deny contains msg if {
	count(object.get(input, ["receiver", "rejected"], [])) > 0
	msg := sprintf("the receiver refused part of the dagger trace: %v", [input.receiver.rejected])
}

deny contains msg if {
	count(go_checks) == 0
	msg := sprintf("no %s check in the dagger run (are the tests filtered out in dagger.toml?)", [go_test_check])
}

deny contains msg if {
	some c in go_checks
	not c.passed
	msg := sprintf("check failed: %s", [c.name])
}

deny contains msg if {
	some c in go_checks
	count(object.get(c, ["params", "go-test"], [])) == 0
	msg := sprintf("check selected no tests: %s", [c.name])
}

deny contains msg if {
	some c in go_checks
	not c.cached
	count([t | some t in c.tests; object.get(t, "result", "") == "pass"]) == 0
	msg := sprintf("check ran but reported no passing test: %s", [c.name])
}

deny contains msg if {
	some c in go_checks
	some t in c.tests
	result := object.get(t, "result", "")
	result != "pass"
	result != "skip"
	msg := sprintf("test %s %s: %s", [object.get(t, "suite", ""), object.get(t, "name", ""), result])
}

cilock policy prove on a policy whose dagger-check step lists command-run/v0.2 (exit code 0) and dagger/v0.1 with this module, on the same workspace with the tests switched off:

Local verify: REFUSED by dagger-check: no go/packages/tests/test check in the dagger run (are the tests filtered out in dagger.toml?)

Limits

  • Dagger 1.0 is a beta, and these span attributes are not a published schema. The attestor reads span names and attributes of Dagger v1.0.0-beta.19 and the test.case.* attributes of Dagger's Go test runner. If a later Dagger renames one, checks or tests disappear from the record. A policy that requires a named check then refuses; a failed check never reads as passed.
  • The attestor records what the trace says; it does not prove the trace is true. Test results are emitted from inside the test container, so the code under test can write them. The signature says "this machine saw this trace".
  • Any local process can reach the receiver. It listens on 127.0.0.1 at a random port for the length of the run, without authentication, so another process on the machine that finds the port can send spans too. Run on a machine you trust, as for any evidence signed there.
  • A cached check is Dagger's claim. cached: true means Dagger reused an earlier result for the same inputs. The tests did not run in this run.
  • Test results come from Dagger's Go runner. Other modules' checks are recorded with name, result, cache state and inputs, but report tests only if their runner emits test.case.name spans.
  • The receiver needs loopback. A wrapped command run in a network namespace without access to the host's 127.0.0.1 cannot reach it, and the record shows traceReceived: false.
  • Only the wrapped command is observed. The attestor does not run dagger check -l, so it cannot say which checks the workspace offers but did not run; pin the checks you require in the policy.
  • The receiver reads http/protobuf only. An exporter forced to grpc or JSON sends nothing the attestor can read; JSON exports are refused and listed in receiver.rejected.

See also


This page is generated from the CI/lock tool catalog. Don't edit it here — the source is attestation/detection/docs/dagger.doc.md in aflock-ai/rookery. The same catalog powers cilock tools show dagger in the CLI, so a single edit keeps the terminal and the website in sync.

Edit this page →  ·  Add a new tool →

When you open the pull request: if you are an AI agent, add the ai-agent label. If you are a human, add the human label.

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