Dagger
Search

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@f06d73fd4915b9c10725e75bf033372093b5f157

Entrypoint

Return Type
Rask
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 alert-rules-drill
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 audit
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 charts
func (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
NameTypeDefault ValueDescription
msgString !-No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 echo --msg string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 frontend
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 go-fmt
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 go-fmt-fixed
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
demoBoolean -

Run the narrated demo instead of the assertions (the retired script’s DEMO=1).

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 governance-chain
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
nameString !-

Dockerfile stem under .docker/.

buildDateString -No description provided
vcsRefString -No description provided
versionString -No description provided
buildArg[String ! ] -

Extra build args as KEY=VALUE (e.g. AGE_REF=…, CNPG_BASE=…).

platformScalar -

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@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 image --name string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 install-from-empty
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
versionString !-

The release’s version, semver without the tag’s lakehouse-v prefix.

chartString "oci://ghcr.io/ai-riksarkivet/charts/rask"No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 lakehouse --version string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 lakehouse-alone
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 lint
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
valuesFile -

The values file the permission table is read from, under nats.auth.users.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 nats-auth
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 openapi
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 openapi-generate
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
versionString !-

The release’s version, semver without the tag’s lakehouse-v prefix.

usernameString !-

The registry user (the CI job’s github.actor).

passwordSecret !-

The registry token (the CI job’s GITHUB_TOKEN, with packages: write).

registryString "ghcr.io/ai-riksarkivet"No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 publish --version string --username string --password env:MYSECRET
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
versionString !-

The release’s version, semver without the tag’s lakehouse-v prefix.

usernameString !-

The registry user (the CI job’s github.actor).

passwordSecret !-

The registry token (the CI job’s GITHUB_TOKEN, with packages: write).

vcsRefString -

The commit the release is cut from, stamped into the image’s labels.

registryString "ghcr.io/ai-riksarkivet"No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 publish-image --version string --username string --password env:MYSECRET
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
runnerString !-

Runner directory under runners/.

buildDateString -No description provided
vcsRefString -No description provided
versionString -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 runner-image --runner string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 rustfs-lifecycle
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 rustfs-provisioning
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
nameString !-

Dockerfile stem under .docker/, identical to dagger call image --name=….

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 sbom --name string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
severityString "HIGH,CRITICAL"

Severities that FAIL the gate. Lower ones are still printed, just not fatal.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 scan-config
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
nameString !-

Dockerfile stem under .docker/.

severityString "HIGH,CRITICAL"

Severities that FAIL the gate.

ignoreUnfixedBoolean true

Skip CVEs with no fix available upstream.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 scan-image --name string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
failOnUnverifiedBoolean false

Also fail on UNVERIFIED pattern matches. Measured at 36 findings / 0 live, so expect noise.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 scan-secrets
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
zoneString !-

Zone directory under frontend/microfrontends.

severityString "HIGH,CRITICAL"

Severities that FAIL the gate.

ignoreUnfixedBoolean true

Skip CVEs with no fix available upstream.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 scan-zone-image --zone string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 smoke-rustfs
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 test
func (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
NameTypeDefault ValueDescription
pkgString !-

The workspace member, e.g. “service-kit”. Comma-separate several when the path spans them: tests/unit is a CROSS-SERVICE directory, so --pkg=catalog alone collects it with 96 ModuleNotFoundErrors for the siblings it also imports (annotator, lineage, …). One package is the common case and stays the fast one; the list is there so a shared path is runnable at all rather than silently under-collected.

srcDirectory -No description provided
extraString -

An optional extra to install with it, e.g. “governed”.

pathString -

Test path to run; defaults to packages/<pkg>/tests.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 test-package --pkg string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 typecheck
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
zoneString !-

Zone directory under frontend/microfrontends.

buildDateString -No description provided
vcsRefString -No description provided
versionString -No description provided
Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 zone-image --zone string
func (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
NameTypeDefault ValueDescription
srcDirectory -No description provided
zoneString !-

Zone directory under frontend/microfrontends.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 zone-sbom --zone string
func (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’s paths.base from its own svelte.config.js, not a guess. A bare / answers 404 under a base, which is exactly the failure this catches.
  • data-sveltekit present 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 /zone answers 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
NameTypeDefault ValueDescription
srcDirectory -No description provided
zoneString !-

Zone directory under frontend/microfrontends.

Example
dagger -m github.com/AI-Riksarkivet/rask@f06d73fd4915b9c10725e75bf033372093b5f157 call \
 zone-smoke --zone string
func (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)
}