dagger attestor
| Name | dagger |
|---|---|
| Predicate type | https://aflock.ai/attestations/dagger/v0.1 |
| Lifecycle | prematerial |
| Default binary? | No |
| Category | unit-test (primary) |
| Recommended trace | off — no syscall tracing needed |
| Auto-attaches when |
|
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:
| Variable | Value |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | http://127.0.0.1:<port> |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | http://127.0.0.1:<port>/v1/traces |
OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL | http/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 checkThe 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}
}| Field | Meaning |
|---|---|
daggerVersion | the Dagger CLI release that exported the trace (dagger.io/client.version); comma-joined if the command ran more than one |
traceReceived | at 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 |
doNotTrack | DO_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[].name | the check's full dag:// link; check is its path, params its other query parameters |
checks[].passed | Dagger's dagger.io/check.passed is true and the span status is not ERROR |
checks[].cached | affirmative 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[].processes | argv of each process the check started |
checks[].inputDigests, inputKey | sha256 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 |
summary | counts over checks and their tests |
receiver | trace 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.1at 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: truemeans 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.namespans. - The receiver needs loopback. A wrapped command run in a network namespace without access to the host's
127.0.0.1cannot reach it, and the record showstraceReceived: 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/protobufonly. An exporter forced togrpcor JSON sends nothing the attestor can read; JSON exports are refused and listed inreceiver.rejected.
See also
- Gate on Dagger checks: record
dagger check, prove the policy offline, hand it to Pushgate test-resultsattestor for JUnit or CTRF reports from other runnerscommand-runattestor for the wrapped command's argv and exit code
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.