rask
Functions live in sibling .go files by concern:- test.go : the offline pytest suite (the app-DB pg legs died at P7a —
the batches table and its Alembic lineage are gone)
All callable via `dagger call `.
Installation
dagger install github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1Entrypoint
Return Type
Rask Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
func (m *MyModule) Example() *dagger.Rask {
return dag.
Rask()
}@function
def example() -> dagger.Rask:
return (
dag.rask()
)@func()
example(): Rask {
return dag
.rask()
}Types
Rask 🔗
alertRulesDrill() 🔗
AlertRulesDrill replays every shipped alert rule against a REAL GreptimeDB, hermetically.
make alert-rules-check proves the LOGIC on synthetic series with promtool; this proves the
production ENGINE will accept the expression at all, and the two do not substitute for each other —
two rules once shipped that promtool called SUCCESS and GreptimeDB answered HTTP 400 and 500, so
they could never fire and nothing said so. Until now the only way to run this half needed a live
cluster, so CI ran alert-rules-check on every commit and the engine proof whenever someone
remembered.
AN EMPTY STORE IS ENOUGH, which is what makes it hermetic: the drill asserts the endpoint answers
status: success, not that any series exists. A fresh standalone GreptimeDB evaluates every
expression to an empty vector and refuses the malformed ones exactly as a populated one would.
The SCRIPT is called rather than make alert-rules-drill, on purpose: that target maps exit 2 (no
reachable datasource) to success so a laptop with no cluster is not a red build. Here the datasource
is bound as a service, so an unreachable one is a broken lane and must fail rather than skip.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
alert-rules-drillfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Alertrulesdrill(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.alertrulesdrill()
)@func()
async example(): Promise<string> {
return dag
.rask()
.alertRulesDrill()
}audit() 🔗
Audit scans every lockfile in the workspace against the OSV database (the CI supply-chain gate).
Exit code 1 means “vulnerabilities found” and fails the call — that IS the gate. Note there is NO
severity threshold to soften it with: osv-scanner exposes --config and --licenses, and nothing
resembling –fail-on-severity, so ANY finding at ANY severity is fatal. That is why ci.yml runs this
leg report-only today (38 findings on the first run, 0 of them Critical) — a gate that goes red the
hour it lands teaches everyone to ignore it.
The way OUT of report-only is not a threshold, it is osv-scanner.toml at the repo root: it is
auto-discovered for the scanned directory and each ignore entry carries a reason and a review date.
Baseline the known 38 there and the gate can block on the 39th — a suppression becomes a recorded
decision with an expiry instead of a deleted CI job.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
auditfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Audit(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.audit()
)@func()
async example(): Promise<string> {
return dag
.rask()
.audit()
}charts() 🔗
Charts runs the CI test job’s non-Python hermetic gates that aren’t daggerized elsewhere: helm lint,
the chart render + --set invariant gates (networkPolicy / security service-accounts / dapr resiliency),
the prod-overlay HA/security render check, and the promtool alert-rules validate-and-fire gate. Each
WithExec fails the whole function on the first non-zero exit (Dagger surfaces the container error), so a
green dagger call charts == that whole subset of CI green. Byte-for-byte equal to the ci.yml shell.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
chartsfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Charts(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.charts()
)@func()
async example(): Promise<string> {
return dag
.rask()
.charts()
}echo() 🔗
Echo returns whatever string you pass it (smoke-test for the module).
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| msg | String ! | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
echo --msg stringfunc (m *MyModule) Example(ctx context.Context, msg string) string {
return dag.
Rask().
Echo(ctx, msg)
}@function
async def example(msg: str) -> str:
return await (
dag.rask()
.echo(msg)
)@func()
async example(msg: string): Promise<string> {
return dag
.rask()
.echo(msg)
}frontend() 🔗
Frontend runs the frontend job’s bun gates as ONE turbo invocation fanned across the workspace graph:
svelte-check (check), TypeScript 7 via tsgo (check:tsgo), the unit tests, and lint + fmt:check — which
are package tasks now, so they parallelise and cache instead of running once repo-wide. The Playwright
e2e leg (test:e2e) is browser-bound and lives in its own function; it is not part of this hermetic
subset.
── --continue IS LOAD-BEARING, and its absence cost the estate a whole gate ────────────────────
Turbo fail-fasts by default: the first non-zero task cancels every task not already started. For a
BUILD that is right — there is no point compiling against a broken dependency. For a GATE it is
exactly wrong, because a gate’s job is to report the state of the estate, not to stop at the first
bad news.
Measured on this workspace, same command, warm cache:
without --continue: 45 of 61 tasks ran, 1 failure reported
with --continue: 70 of 74 tasks ran, 4 failures reported
So three of the four real failures were invisible — including @rask/api#test, which has collected
ZERO tests since 2026-08-05 (its live.svelte.ts imports svelte, which @rask/api never declared)
and was masked the whole time by whichever task happened to fail first. annotator:test (422) and
explorer:test (116) did not run at all. A gate reporting one failure of sixty-one is not a gate; it
is a sample.
The exit code is unchanged — turbo still exits non-zero if ANY task failed, so Dagger still surfaces
the container error and CI still goes red. --continue buys completeness of the report, not leniency.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
frontendfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Frontend(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.frontend()
)@func()
async example(): Promise<string> {
return dag
.rask()
.frontend()
}goFmt() 🔗
GoFmt is the Go plane’s formatting gate, and it exists because there wasn’t one.
.dagger/ implements every CI gate in this estate, and until 2026-08-22 nothing checked its own
formatting — gofmt -l . reported two tracked, hand-written files unformatted (images.go,
main.go). The Python plane has had ruff format --check on the merge path the whole time, so this
was not a policy decision about Go, it was a plane nobody pointed a gate at.
The narrow scope is deliberate. The broader claim this finding was filed under — “1,047 lines
implementing every CI gate, tested by nothing” — is false and worth not re-litigating: .dagger/go.mod
IS enumerated by osv-scanner (scan.go), and every dagger call compiles and type-checks the whole
module, so a type error cannot reach main. Formatting was the one real hole.
It runs in a container rather than assuming a local toolchain, which is not merely convenient: there is no Go on the developer PATH here, and the repository rule is absolute — every container goes through Dagger. A gate that needed a hand-installed toolchain would be a gate most people never run.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
go-fmtfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Gofmt(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.gofmt()
)@func()
async example(): Promise<string> {
return dag
.rask()
.goFmt()
}goFmtFixed() 🔗
GoFmtFixed returns the .dagger tree with gofmt applied — the write half of GoFmt.
The estate’s Python plane has had both halves forever (make fmt writes, ruff format --check
gates), and a gate with no paired fixer is a gate people work around. There is no Go toolchain on
the developer PATH here, so without this the only way to satisfy GoFmt would be to install one —
which the repository’s Dagger rule exists to make unnecessary.
Usage: dagger call go-fmt-fixed export --path=.dagger
Return Type
Directory !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
go-fmt-fixedfunc (m *MyModule) Example() *dagger.Directory {
return dag.
Rask().
Gofmtfixed()
}@function
def example() -> dagger.Directory:
return (
dag.rask()
.gofmtfixed()
)@func()
example(): Directory {
return dag
.rask()
.goFmtFixed()
}governanceChain() 🔗
GovernanceChain proves the dataops loop end to end: authorization, provenance AUTHORSHIP, and medallion lineage — with real Dex id_tokens and not one hand-written tuple.
It replaces scripts/governance_e2e.sh, which layered FOUR compose files (base + auth + lineage +
governance) to assemble the same thing. Everything it needs beyond the auth stack is the lineage
service, which keeps its tables in the catalog it is wired to, so it is the auth stack plus one service.
The catalog runs with emission ON, as the chart renders it (catalogService): authorization alone passes with
emission off, so a run without it would prove the authz half and skip the provenance half.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| demo | Boolean | - | Run the narrated demo instead of the assertions (the retired script’s DEMO=1). |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
governance-chainfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Governancechain(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.governancechain()
)@func()
async example(): Promise<string> {
return dag
.rask()
.governanceChain()
}image() 🔗
Image builds ANY deployable from its own dockerfile under .docker/.
name is the dockerfile stem: rest-catalog, compute, media, ray-cluster,
runner, assist-runner, cnpg-age-ext.
Return Type
Container !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| name | String ! | - | Dockerfile stem under .docker/. |
| buildDate | String | - | No description provided |
| vcsRef | String | - | No description provided |
| version | String | - | No description provided |
| buildArg | [String ! ] | - | Extra build args as KEY=VALUE (e.g. AGE_REF=…, CNPG_BASE=…). |
| platform | Scalar | - | The platform to build for (linux/amd64, linux/arm64); the engine’s own by default. A foreign one runs under the engine’s built-in emulation, so no host setup is needed. |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
image --name stringfunc (m *MyModule) Example(name string) *dagger.Container {
return dag.
Rask().
Image(name)
}@function
def example(name: str) -> dagger.Container:
return (
dag.rask()
.image(name)
)@func()
example(name: string): Container {
return dag
.rask()
.image(name)
}installFromEmpty() 🔗
InstallFromEmpty installs rask onto an empty cluster the way a platform team does ([[XC-133]], ADR 0153): an empty k3s, the development prerequisites and stand-ins from deploy/fixtures, then the product chart, until every workload is Ready (scripts/install_from_empty.sh). The images the render names are built from this source and served to the cluster by a registry inside the session, so the same call runs on a laptop and in CI with nothing but Dagger.
Returns the install’s output; a failed install returns its diagnostics in the error.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
install-from-emptyfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Installfromempty(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.installfromempty()
)@func()
async example(): Promise<string> {
return dag
.rask()
.installFromEmpty()
}lakehouse() 🔗
Lakehouse stands a published lakehouse release up for another repository’s e2e: an empty k3s, the development
prerequisites and stand-ins from this module’s deploy/fixtures, and the chart oci://ghcr.io/ai-riksarkivet/charts/rask
at version, until every workload is Ready (scripts/install_from_empty.sh). The release’s catalog image is copied by
the digest its chart pins into a registry inside the session, which the cluster mirrors, so the cluster runs the
published bytes as the local estate it is (image.localImages); nothing is built.
Returns the lakehouse as one service serving the catalog on 2333, Dex on 5556 and the object store on 9000. Bind it
under the in-cluster names, rask-catalog, rask-dex and rask-rustfs, so a token’s issuer
(http://rask-dex:5556/dex) and a vended endpoint resolve as the catalog names them. The estate admin is
alice@example.com (password password), whom the Dex fixture serves.
Cached per session: the services it starts live as long as the session, so a result cached across sessions would
name a cluster nobody installed (measured on engine v0.21.8: by default a second dagger call returns the first
call’s result).
Return Type
Service !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| version | String ! | - | The release’s version, semver without the tag’s |
| chart | String | "oci://ghcr.io/ai-riksarkivet/charts/rask" | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
lakehouse --version stringfunc (m *MyModule) Example(version string) *dagger.Service {
return dag.
Rask().
Lakehouse(version)
}@function
def example(version: str) -> dagger.Service:
return (
dag.rask()
.lakehouse(version)
)@func()
example(version: string): Service {
return dag
.rask()
.lakehouse(version)
}lakehouseAlone() 🔗
LakehouseAlone runs the lakehouse’s own gates in a tree with every client deleted (XC-188): the import contracts
(make lint-imports, which runs a client’s own contract only where that client’s tree is present) and the offline
suite the Test lane runs, on a workspace re-locked without the clients. It goes red when a lakehouse module, test,
contract or gate needs a client’s tree.
dagger call lakehouse-alone
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
lakehouse-alonefunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Lakehousealone(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.lakehousealone()
)@func()
async example(): Promise<string> {
return dag
.rask()
.lakehouseAlone()
}lint() 🔗
Lint runs ruff check + format –check over the WHOLE tree — the same estate make lint walks.
It named services tests until 2026-08-22, which left 39,244 tracked lines outside the merge-path
gate: packages/ (24,580 — all seven first-party libraries, including the authorization kernel),
scripts/ (5,667 — among them the ray_*_job.py entrypoints the cluster image BAKES, so production
code) and runners/ (8,997). make lint has always run ruff check ., so the local gate and the CI
gate measured different estates and nothing asserted they agree — which is how a lint-clean local
run and a lint-clean CI run could both be true of different code.
uv run --no-sync, not uvx: uvx ruff resolves the LATEST ruff while the Makefile runs the LOCKED
one from the root dev group. Widening the scope while keeping two ruff versions would trade one
divergence for another, and this estate has already been bitten by exactly that — an unpinned
uvx ty drifting until its pre-commit hook blocked every commit. base() already runs
uv sync --all-packages, so --no-sync is correct and free.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
lintfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Lint(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.lint()
)@func()
async example(): Promise<string> {
return dag
.rask()
.lint()
}natsAuth() 🔗
NatsAuth proves the chart’s NATS permission table (nats.auth.users) on a real broker: the NATS fixture’s
nats-server in operator mode with a MEMORY resolver and JetStream, test-only keys minted with nsc, one user
per table row, and the real daprd the Dapr fixture injects, running the chart’s own Components against it
(tests/e2e-py/test_nats_admits_each_client_only_to_its_own_subjects.py says what it asserts).
dagger call nats-auth
dagger call nats-auth --values=<a values file carrying nats.auth.users>
THE BINARIES ARE COPIED OUT OF THE IMAGES THE CHART DEPLOYS and run as processes beside pytest, not as
Dagger services. The test writes the broker’s config from keys it mints inside the run, reads the
broker’s own log to judge every refusal, and starts one daprd per Component shape against a resources
directory it renders; a service binding gives the test none of the three. Every binary here is a static
Go build, so it runs in the uv image unchanged. images.json names the image each came from, and the test
refuses a lane whose images the chart does not deploy, so these pins cannot drift from the chart silently.
--values replaces only the file the table is read from; the chart itself is always the source tree’s.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| values | File | - | The values file the permission table is read from, under |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
nats-authfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Natsauth(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.natsauth()
)@func()
async example(): Promise<string> {
return dag
.rask()
.natsAuth()
}openapi() 🔗
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
openapifunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Openapi(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.openapi()
)@func()
async example(): Promise<string> {
return dag
.rask()
.openapi()
}openapiGenerate() 🔗
Openapi runs the CI test job’s OpenAPI drift gate (make openapi-check): the committed specs
(docs/*-openapi.json) are the canonical, reviewable contract of every endpoint we serve, and they are
GENERATED from the live FastAPI apps — so a route added or changed without refreshing the spec must fail
here, not surprise a client. base() excludes .git, so instead of make openapi-check’s git diff this
snapshots the two committed specs, regenerates them from the apps, then diffs the regenerated files back
against the snapshots — same fail-on-drift contract, no repo needed. A non-zero diff fails the function.
OpenapiGenerate regenerates the specs from the live FastAPI apps and RETURNS docs/, so the refresh
half of the contract is reachable without a local Python environment:
dagger call openapi-generate export --path=docs
Openapi (below) only ever CHECKS. That asymmetry was invisible while every developer could run
make openapi, and became a wall the moment one could not: uv cannot build this workspace on
macOS/x86_64 at all, because lancedb ships no wheel for that platform (measured 2026-08-09 — the
only macOS wheel is macosx_11_0_arm64). On such a machine an endpoint could be added but its spec
could never be refreshed, so the drift gate would fail with no sanctioned way to satisfy it.
Same container and same generator as the gate, so what this writes is exactly what that compares.
Return Type
Directory !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
openapi-generatefunc (m *MyModule) Example() *dagger.Directory {
return dag.
Rask().
Openapigenerate()
}@function
def example() -> dagger.Directory:
return (
dag.rask()
.openapigenerate()
)@func()
example(): Directory {
return dag
.rask()
.openapiGenerate()
}publish() 🔗
Publish releases the lakehouse at version: it joins the per-architecture images PublishImage pushed into one
index, <registry>/lance-rest-catalog:<version>, and pushes the product chart pinned to that index’s digest as
oci://<registry>/charts/rask:<version>. Nothing is built or run here, so any runner joins any architecture.
Returns what was published, as a client pins it. Never cached: a call is a push.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| version | String ! | - | The release’s version, semver without the tag’s |
| username | String ! | - | The registry user (the CI job’s |
| password | Secret ! | - | The registry token (the CI job’s GITHUB_TOKEN, with packages: write). |
| registry | String | "ghcr.io/ai-riksarkivet" | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
publish --version string --username string --password env:MYSECRETfunc (m *MyModule) Example(ctx context.Context, version string, username string, password *dagger.Secret) string {
return dag.
Rask().
Publish(ctxversion, username, password)
}@function
async def example(version: str, username: str, password: dagger.Secret) -> str:
return await (
dag.rask()
.publish(version, username, password)
)@func()
async example(version: string, username: string, password: Secret): Promise<string> {
return dag
.rask()
.publish(version, username, password)
}publishImage() 🔗
PublishImage builds the catalog image for the engine’s own platform and pushes it as
<registry>/lance-rest-catalog:<version>-<arch>, returning that reference by digest. The release workflow runs it
once per release platform, each on a runner of that architecture; Publish then joins them. Never cached: a call is a push.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| version | String ! | - | The release’s version, semver without the tag’s |
| username | String ! | - | The registry user (the CI job’s |
| password | Secret ! | - | The registry token (the CI job’s GITHUB_TOKEN, with packages: write). |
| vcsRef | String | - | The commit the release is cut from, stamped into the image’s labels. |
| registry | String | "ghcr.io/ai-riksarkivet" | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
publish-image --version string --username string --password env:MYSECRETfunc (m *MyModule) Example(ctx context.Context, version string, username string, password *dagger.Secret) string {
return dag.
Rask().
Publishimage(ctxversion, username, password)
}@function
async def example(version: str, username: str, password: dagger.Secret) -> str:
return await (
dag.rask()
.publishimage(version, username, password)
)@func()
async example(version: string, username: string, password: Secret): Promise<string> {
return dag
.rask()
.publishImage(version, username, password)
}runnerImage() 🔗
RunnerImage builds ONE workload’s Ray image: the agnostic platform environment plus one sealed runner, from the single parametrized .docker/ray-runner.dockerfile.
The sibling of ZoneImage, and for the same reason: one definition, N images, no file per member.
It replaced .docker/ray-htr.dockerfile, which opened FROM ray-cluster:dev — a tag in the HOST
daemon. This module’s builds run inside the Dagger engine and BuildKit resolves that against a
registry, so that image could not be built by the only seam this repo builds through, and nothing
said so. Dagger’s DockerBuild exposes no build-context option, so there is no way to hand it a
locally-built base; a self-contained dockerfile is the fix that needs no registry.
runner is a directory under runners/ that ships its own uv.lock — the lock is what seals the
workload’s environment away from the root one.
Return Type
Container !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| runner | String ! | - | Runner directory under runners/. |
| buildDate | String | - | No description provided |
| vcsRef | String | - | No description provided |
| version | String | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
runner-image --runner stringfunc (m *MyModule) Example(runner string) *dagger.Container {
return dag.
Rask().
Runnerimage(runner)
}@function
def example(runner: str) -> dagger.Container:
return (
dag.rask()
.runnerimage(runner)
)@func()
example(runner: string): Container {
return dag
.rask()
.runnerImage(runner)
}rustfsLifecycle() 🔗
RustfsLifecycle runs the catalog lifecycle e2e with the bytes on a RustFS service bound to this one exec,
proving the catalog speaks plain S3 through packages/storage rather than one store’s dialect.
It replaces scripts/rustfs_e2e.sh, which drove docker compose up -d --build over
docker-compose.yml + docker-compose.rustfs.yml. That script was invisible to the estate’s own
docker gate for most of a day: the gate required up on the same LINE as compose, and the script
wraps it (compose() { docker compose -f "$BASE" -f "$RUSTFS" "$@"; }).
ORDERING IS THE INTERESTING PART, and it needs BOTH halves below. Compose expressed it
declaratively — depends_on: {createbuckets-rustfs: {condition: service_completed_successfully}}
— and Dagger has no such condition on a service binding, so Sync supplies the sequencing: the
bootstrap is forced to finish before the catalog service is constructed.
That alone is NOT enough, and the failure is worth knowing about because it looks like a catalog
bug. Dagger services are reference-counted: Sync completing drops rustfs to zero references, so
it stops and takes the bucket with it, and the catalog then starts a fresh empty one. The first
run of this function died on NoSuchBucket: Volume not found from inside pylance. The cache volume
on the service is what makes the bootstrap durable across that restart — the same job compose’s
named rustfs-data volume was doing.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
rustfs-lifecyclefunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Rustfslifecycle(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.rustfslifecycle()
)@func()
async example(): Promise<string> {
return dag
.rask()
.rustfsLifecycle()
}rustfsProvisioning() 🔗
RustfsProvisioning proves the scoped-users hook’s provisioning script against a RustFS service bound to this one
exec: tests/e2e-py/test_a_policy_the_store_refuses_fails_the_scoped_users_hook.py runs scripts/rustfs_admin.py
through the CLI the hook runs, and holds a policy the store refuses to failing the run before it binds anyone.
dagger call rustfs-provisioning
The store boots as the fixture boots it (deploy/fixtures/rustfs): the root pair from files through
RUSTFS_ACCESS_KEY_FILE and RUSTFS_SECRET_KEY_FILE, never env, with a secret made for this run, which reaches the
test only as the file the script reads.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
rustfs-provisioningfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Rustfsprovisioning(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.rustfsprovisioning()
)@func()
async example(): Promise<string> {
return dag
.rask()
.rustfsProvisioning()
}sbom() 🔗
Sbom builds a deployable from .docker/.dockerfile and emits its CycloneDX SBOM.
dagger call sbom --name=rest-catalog export --path=./sbom-rest-catalog.cdx.json
Return Type
File !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| name | String ! | - | Dockerfile stem under .docker/, identical to |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
sbom --name stringfunc (m *MyModule) Example(name string) *dagger.File {
return dag.
Rask().
Sbom(name)
}@function
def example(name: str) -> dagger.File:
return (
dag.rask()
.sbom(name)
)@func()
example(name: string): File {
return dag
.rask()
.sbom(name)
}scanConfig() 🔗
ScanConfig runs trivy’s misconfiguration and secret scanners over the deploy surface.
Scoped to .docker/ and chart/ deliberately: trivy config on the whole repo also parses every
docker-compose file and test fixture, and the resulting noise buries the findings that matter.
Needs no build, so it belongs in the fast PR gate next to Audit.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| severity | String | "HIGH,CRITICAL" | Severities that FAIL the gate. Lower ones are still printed, just not fatal. |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
scan-configfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Scanconfig(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.scanconfig()
)@func()
async example(): Promise<string> {
return dag
.rask()
.scanConfig()
}scanImage() 🔗
ScanImage builds a deployable from .docker/.dockerfile and scans that exact image.
name is the dockerfile stem, identical to dagger call image --name=…: rest-catalog, compute, runner,
ray-cluster, …
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| name | String ! | - | Dockerfile stem under .docker/. |
| severity | String | "HIGH,CRITICAL" | Severities that FAIL the gate. |
| ignoreUnfixed | Boolean | true | Skip CVEs with no fix available upstream. |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
scan-image --name stringfunc (m *MyModule) Example(ctx context.Context, name string) string {
return dag.
Rask().
Scanimage(ctxname)
}@function
async def example(name: str) -> str:
return await (
dag.rask()
.scanimage(name)
)@func()
async example(name: string): Promise<string> {
return dag
.rask()
.scanImage(name)
}scanSecrets() 🔗
ScanSecrets runs trufflehog over the repository’s FULL GIT HISTORY.
── Why this exists when ScanConfig already detects secrets ── Trivy’s secret scanner reads the WORKING TREE. A credential that was committed and later deleted is invisible to it — and that is precisely the leak that matters, because rask is a public repo: once a secret is in a pushed object it is public whether or not a later commit removed the file. Every gate in this file was blind to that until this function existed.
── Why trufflehog and not gitleaks ── Both were run over this repo’s history on 2026-08-05, and the comparison is the argument:
gitleaks 8.30.1 17 findings no verification → all 17 triaged BY HAND
trufflehog 3.96.0 19 findings 0 VERIFIED → triaged automatically
They overlap on almost nothing — gitleaks matched generic-api-key/curl-auth-user patterns, trufflehog
matched 17 Postgres DSNs and 2 Stripe keys. Neither is a superset, so this is not “the better tool”,
it is the tool with the property that makes a secrets gate survivable: trufflehog CALLS THE PROVIDER
to ask whether the credential actually works. sk_live_abc123secret456def789 in a deleted test
harness looks alarming in a grep and is confirmed dead by an API call.
── Why the gate is –results=verified ── 36 pattern matches across both tools, ZERO live credentials. A gate that failed on pattern matches would have been red on arrival with a 100% false-positive rate, which is how the audit and scan-config legs ended up report-only. Verified-only inverts that: it stays green until someone commits a key that genuinely works, and then it is not a suggestion. Nothing to baseline, nothing to suppress, no continue-on-error — so unlike the other two legs this one lands BLOCKING.
── And why “verified” is only as good as the provider that answers ──
Verification is an API call, so a provider that accepts ANY string of its key shape turns every
match into a “verified” credential. Measured 2026-09-24 on this repo’s history: the gating pass
returned 620 findings and ALL 620 were Lob — whose test keys are test_-prefixed, which is the
name of every pytest function ever written here. Raw results included pytest function names. Not
one was a credential, and the gate had failed every run since it landed on 2026-08-05: fifty days
of a red build that gated nothing and taught everyone to look past it. The 24 Postgres and 3
Stripe matches in the same scan stayed UNVERIFIED, which is the mechanism working — those
hostnames answer nobody.
So a detector is excluded when its verification carries no information, and excluded from BOTH passes: leaving it in the report-only pass buries the findings a reviewer needs under hundreds it must ignore. The list is narrow and each entry earns its place by measurement, never by being noisy.
The unverified findings are still PRINTED (the first pass below) so a reviewer can see them; they just do not fail the build. Pass –fail-on-unverified=true to gate on those too.
NOTE the +ignore list here deliberately KEEPS .git — the other functions strip it, and with it
stripped this function has nothing to scan.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| failOnUnverified | Boolean | false | Also fail on UNVERIFIED pattern matches. Measured at 36 findings / 0 live, so expect noise. |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
scan-secretsfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Scansecrets(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.scansecrets()
)@func()
async example(): Promise<string> {
return dag
.rask()
.scanSecrets()
}scanZoneImage() 🔗
ScanZoneImage builds one micro-frontend zone and scans it, mirroring ScanImage for the frontend plane.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| zone | String ! | - | Zone directory under frontend/microfrontends. |
| severity | String | "HIGH,CRITICAL" | Severities that FAIL the gate. |
| ignoreUnfixed | Boolean | true | Skip CVEs with no fix available upstream. |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
scan-zone-image --zone stringfunc (m *MyModule) Example(ctx context.Context, zone string) string {
return dag.
Rask().
Scanzoneimage(ctxzone)
}@function
async def example(zone: str) -> str:
return await (
dag.rask()
.scanzoneimage(zone)
)@func()
async example(zone: string): Promise<string> {
return dag
.rask()
.scanZoneImage(zone)
}smokeRustfs() 🔗
SmokeRustfs proves packages/storage works against a REAL S3 backend rather than moto.
It REPLACES three make targets — rustfs-up, smoke-rustfs, rustfs-down — and collapsing them
is what makes the Dagger form strictly better rather than a trade. The old shape started a DETACHED
container, ran a script against localhost:9000, and left the operator to remember the teardown;
its docker run was one of the estate’s last three, and the objection to converting it was that
as-service up holds a terminal where up -d does not.
That objection only applies to a two-step shape. As ONE function the service is bound for exactly
the life of the exec: nothing is detached, nothing leaks if the smoke fails, no host port is taken
(so it cannot collide with a make dev-* stack or a second developer), and there is no teardown to
forget. dagger call smoke-rustfs is the whole thing.
The credentials are env-driven on purpose — the estate’s storage layer must swap
RustFS, a platform’s S3 and AWS with environment variables alone, and hard-coding them here would quietly test a
narrower contract than the one packages/storage promises.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
smoke-rustfsfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Smokerustfs(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.smokerustfs()
)@func()
async example(): Promise<string> {
return dag
.rask()
.smokeRustfs()
}test() 🔗
Test runs the offline pytest suite: every root-workspace testpath (packages, services,
tests/unit, tests/integration — and tests/e2e-py stays collectable so the collection
gate can see it), excluding the live-stack e2e marker and the model-bound slow
marker. This is the pytest leg of the old dagger call ci composition (lint →
typecheck → openapi → test); the sealed runners/htr project runs its own suite outside
the workspace (make test’s second leg) and is not covered here.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
testfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Test(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.test()
)@func()
async example(): Promise<string> {
return dag
.rask()
.test()
}testPackage() 🔗
TestPackage runs ONE workspace member’s suite against a SCOPED sync — the fast lane.
dagger call test-package --pkg=service-kit --extra=governed
Test (below) is the whole-estate gate and pays for it: uv sync --all-packages (200 packages,
including pylance/lancedb), an apt layer, a helm download and make k3s-deps’s ten subchart
fetches — measured at ~54 minutes cold. That is the right price for the full suite and the wrong
one for a change to a single package.
The authz plane is the case that forced this. service_kit.governed.fga needs only the governed
extra (openfga-sdk, pyjwt, aiohttp, tenacity, httpx, lance-namespace); it does not need lancedb,
pyarrow, helm or the chart. Locally that distinction is not a nicety — lancedb ships no
macOS/x86_64 wheel, so on an Intel Mac the full sync cannot complete at all and the scoped one
finishes in seconds.
pytest is injected with --with rather than declared: it belongs to the ROOT dev group, which a
--package sync deliberately does not install. --no-cov because the root addopts turn coverage
on for the whole workspace, which is meaningless for one member’s paths.
THE BOUNDARY, measured rather than guessed: a single file or one package’s tests/ dir runs
cleanly (verified across four suites). A whole cross-service directory does not — tests/unit
collected 96 ModuleNotFoundErrors on --pkg=catalog, and naming all seven services it spans takes
that to 3, the last being root DEV-GROUP deps (respx, lance_ray) that a --package sync omits
by design. Injecting those here would be re-implementing the dev group one --with at a time, and
the thing it would reimplement already exists: dagger call test. This is the inner loop; that is
the gate. Reach for it when the answer must cover everything.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| pkg | String ! | - | The workspace member, e.g. “service-kit”. Comma-separate several when the path spans them:
|
| src | Directory | - | No description provided |
| extra | String | - | An optional extra to install with it, e.g. “governed”. |
| path | String | - | Test path to run; defaults to |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
test-package --pkg stringfunc (m *MyModule) Example(ctx context.Context, pkg string) string {
return dag.
Rask().
Testpackage(ctx, pkg)
}@function
async def example(pkg: str) -> str:
return await (
dag.rask()
.testpackage(pkg)
)@func()
async example(pkg: string): Promise<string> {
return dag
.rask()
.testPackage(pkg)
}typecheck() 🔗
Typecheck runs the ty type-checker (the CI type gate), at the version the root lock pins: the same
uv run --no-sync reasoning as Lint, so the hook, make typecheck and CI judge with one ty.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
typecheckfunc (m *MyModule) Example(ctx context.Context) string {
return dag.
Rask().
Typecheck(ctx)
}@function
async def example() -> str:
return await (
dag.rask()
.typecheck()
)@func()
async example(): Promise<string> {
return dag
.rask()
.typecheck()
}zoneImage() 🔗
ZoneImage builds one micro-frontend zone from the single parametrized frontend dockerfile.
zone is a directory under frontend/microfrontends and becomes the APP build arg, exactly as
make frontend-images passed it to docker buildx before this module took the build over.
Return Type
Container !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| zone | String ! | - | Zone directory under frontend/microfrontends. |
| buildDate | String | - | No description provided |
| vcsRef | String | - | No description provided |
| version | String | - | No description provided |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
zone-image --zone stringfunc (m *MyModule) Example(zone string) *dagger.Container {
return dag.
Rask().
Zoneimage(zone)
}@function
def example(zone: str) -> dagger.Container:
return (
dag.rask()
.zoneimage(zone)
)@func()
example(zone: string): Container {
return dag
.rask()
.zoneImage(zone)
}zoneSbom() 🔗
ZoneSbom mirrors Sbom for a micro-frontend zone, as ScanZoneImage mirrors ScanImage.
Return Type
File !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| zone | String ! | - | Zone directory under frontend/microfrontends. |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
zone-sbom --zone stringfunc (m *MyModule) Example(zone string) *dagger.File {
return dag.
Rask().
Zonesbom(zone)
}@function
def example(zone: str) -> dagger.File:
return (
dag.rask()
.zonesbom(zone)
)@func()
example(zone: string): File {
return dag
.rask()
.zoneSbom(zone)
}zoneSmoke() 🔗
ZoneSmoke builds ONE zone’s image, serves it, and proves the SSR shell actually renders at that zone’s own base path.
It replaces the docker run -d --name smoke-$z loop in ci.yml, which was the last docker
invocation in the workflow. Three assertions carry over unchanged, because each was earned:
- HTTP 200 at
${base}/— the zone’spaths.basefrom its own svelte.config.js, not a guess. A bare/answers 404 under a base, which is exactly the failure this catches. data-sveltekitpresent in the body — a 200 alone can be an error page or a redirect target.- the marker is read from the FINAL response after redirects, since
/zoneanswers 308 and/zone/answers 200.
What does NOT carry over is the docker image inspect web-$z:dev pre-check, which existed to catch
a stale Makefile ZONES list. That is not lost: @rask/zone-contract’s manifest test already pins
the roster across all five places that declare it, and it fails at unit speed rather than after a
full image build. Dagger builds the zone here from its directory, so there is no separately-tagged
artefact to be missing in the first place.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| src | Directory | - | No description provided |
| zone | String ! | - | Zone directory under frontend/microfrontends. |
Example
dagger -m github.com/AI-Riksarkivet/rask@53c400b3a96f4d431bb9b67da73305b2078f28c1 call \
zone-smoke --zone stringfunc (m *MyModule) Example(ctx context.Context, zone string) string {
return dag.
Rask().
Zonesmoke(ctxzone)
}@function
async def example(zone: str) -> str:
return await (
dag.rask()
.zonesmoke(zone)
)@func()
async example(zone: string): Promise<string> {
return dag
.rask()
.zoneSmoke(zone)
}