Heimdall/deployment/idunn/recipe.toml
Meta Vens 3eaa9ef265 Declare Joystick in the Idunn recipe and document the locked refresh
Recipe optional_environment gains the Joystick client id and secret file;
service-contract documents the row lock, rotation fail-closed and the 410/502
answers; the StreamPixels profile names Heimdall as sole holder of provider
refresh tokens (F5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 11:15:42 +02:00

213 lines
8 KiB
TOML

# The runtime contract below is implemented as of 2026-09-06.
#
# GAMECULT_IDUNN_CANDIDATE_BIND src/config.ts takes the candidate socket
# over HOST/PORT, requires loopback and a
# usable port, and refuses to start unless
# GC_ACCESS_BASE_URL is set, so an
# ephemeral port can never become the
# provider-registered OAuth callback.
# GAMECULT_IDUNN_PROCESS_WRITE_LEASE src/process-write-lease.ts. The single
# writer of heimdall.service.cc declines
# unless the lease names this incarnation.
# GAMECULT_IDUNN_RUNTIME_BUNDLE read for expected.cc, which is how the
# process learns which incarnation it is.
#
# Health reports `warming` until the lease is held and `active` after, because
# Idunn must observe warming presence before it fences the incumbent.
#
# Still untested against a live Idunn: nothing here has been through an actual
# `idunn up heimdall`, and the daemon on yggdrasil is still the previous
# generation. Treat the first deployment as the real verification.
schema = "gamecult.idunn.target_declaration.v1"
target = "heimdall"
source_stamp_environment = "HEIMDALL_BUILD_COMMIT"
required_gitlinks = ["vendor/CultLib"]
# CultLib's TypeScript packages resolve through main/types to dist/, which a
# fresh checkout does not have. They must be built before Heimdall's own
# typecheck can resolve cultcache-ts, cultnet-ts or cultmesh-ts. This used to
# live in a shell manifest on the host; it belongs here, where the source seal
# covers it.
[[steps]]
id = "install-cultlib"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "ci", "--ignore-scripts", "--prefix", "vendor/CultLib"]
[[steps]]
id = "build-cultcache-ts"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "run", "build", "--prefix", "vendor/CultLib", "--workspace=packages/cultcache-ts"]
[[steps]]
id = "build-cultnet-ts"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "run", "build", "--prefix", "vendor/CultLib", "--workspace=packages/cultnet-ts"]
[[steps]]
id = "build-cultmesh-ts"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["npm", "run", "build", "--prefix", "vendor/CultLib", "--workspace=packages/cultmesh-ts"]
[[steps]]
id = "install-heimdall"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "install", "--frozen-lockfile", "--config.node-linker=hoisted"]
[[steps]]
id = "test-heimdall"
phase = "test"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "test"]
[[steps]]
id = "build-heimdall"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "build"]
# The release is one self-contained executable. node_modules is a build
# input, not an artifact: pnpm lays it out as symlinks into .pnpm, which
# Idunn refuses to seal, and shipping a dependency tree to run a service is
# a larger liability than bundling it.
[[steps]]
id = "bundle-heimdall"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["corepack", "pnpm", "bundle"]
# The unit runs dist/src/index.js, and Node resolves its imports at runtime, so
# the release is the compiled output plus the production dependency tree. These
# are the two artifacts; nothing else from the build tree is installed.
[[steps]]
id = "stage-node"
phase = "build"
runner = "node-build"
required_environment = ["NPM_CONFIG_CACHE", "HOME", "COREPACK_HOME"]
argv = ["node", "-e", "require('fs').copyFileSync(process.execPath, 'dist/node')"]
# Idunn compares the running process's executable inode against the sealed
# artifact, so the executed inode must live inside the release. A shebang script
# cannot satisfy that: the kernel execs the interpreter, and the process's
# executable is /usr/bin/node rather than anything sealed. Shipping the runner
# image's own node as the executable brings the interpreter inside the seal and
# pins it to the runner image digest, which is a stronger guarantee than
# depending on whatever node the host happens to have.
[[artifacts]]
id = "node"
source_kind = "runner-output"
runner = "node-build"
source = "dist/node"
destination = "node"
executable = true
[[artifacts]]
id = "heimdall-bundle"
source_kind = "runner-output"
runner = "node-build"
source = "dist/heimdall"
destination = "heimdall.mjs"
executable = false
[service]
executable_artifact = "node"
arguments = [
{ kind = "literal", value = "heimdall.mjs" },
{ kind = "literal", value = "--state-root" },
{ kind = "binding", name = "state_root" },
]
transport = "http"
route_required = true
# A secret input is declared only as its `*_FILE` name. The binding binds it
# under [workload.secret_files] to a root:root 0400 credential file, so the value
# never sits in the binding, the unit or a plan. Idunn refuses a binding that
# sets an undeclared name, so a plaintext secret has nowhere to go.
optional_environment = [
"CULTMESH_URI_ODIN_RUDP",
"GC_ACCESS_APP_BIFROST_BACKEND_CALLBACK_URLS",
"GC_ACCESS_APP_BIFROST_SHARED_SECRET_FILE",
"GC_ACCESS_APP_GHOSTLIGHT_BACKEND_CALLBACK_URLS",
"GC_ACCESS_APP_GHOSTLIGHT_RUNTIME_IDS",
"GC_ACCESS_APP_GHOSTLIGHT_SHARED_SECRET_FILE",
"GC_ACCESS_APP_REPIXELIZER_SHARED_SECRET_FILE",
"GC_ACCESS_APP_STREAMPIXELS_SHARED_SECRET_FILE",
"GC_ACCESS_APPLY_SCHEMA_ON_STARTUP",
"GC_ACCESS_BIFROST_PATRON_SUPPORT_ENDPOINT",
"GC_ACCESS_BIFROST_PATRON_SUPPORT_SECRET_FILE",
"GC_ACCESS_COMPLETION_TTL_SECONDS",
"GC_ACCESS_IDUNN_HEALTH_CONTRACT",
"GC_ACCESS_IDUNN_RUDP_HEALTH",
"GC_ACCESS_ISSUER",
"GC_ACCESS_ODIN_CULTMESH_URI",
"GC_ACCESS_PRIVATE_COMMAND_HOST",
"GC_ACCESS_PRIVATE_COMMAND_PORT",
"GC_ACCESS_PROVIDER_DISCORD_CLIENT_ID",
"GC_ACCESS_PROVIDER_DISCORD_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_GITHUB_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_JOYSTICK_CLIENT_ID",
"GC_ACCESS_PROVIDER_JOYSTICK_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_PATREON_CLIENT_ID",
"GC_ACCESS_PROVIDER_PATREON_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_SPOTIFY_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_TWITCH_CLIENT_ID",
"GC_ACCESS_PROVIDER_TWITCH_CLIENT_SECRET_FILE",
"GC_ACCESS_PROVIDER_YOUTUBE_CLIENT_ID",
"GC_ACCESS_PROVIDER_YOUTUBE_CLIENT_SECRET_FILE",
"GC_ACCESS_SESSION_TTL_SECONDS",
"GC_ACCESS_SIGNING_KEY_ID",
"GC_ACCESS_SIGNING_PRIVATE_KEY_BOOTSTRAP",
"GC_ACCESS_SIGNING_PRIVATE_KEY_PATH",
"GC_ACCESS_STATE_TTL_SECONDS",
"GC_ACCESS_STORAGE_BACKEND",
"GC_ACCESS_TOKEN_ENCRYPTION_KEY_BASE64_FILE",
]
required_environment = [
"GAMECULT_IDUNN_RUNTIME_BUNDLE",
"GAMECULT_IDUNN_CANDIDATE_BIND",
"GAMECULT_IDUNN_PROCESS_WRITE_LEASE",
"GC_ACCESS_BASE_URL",
"GC_ACCESS_DATABASE_URL_FILE",
]
[service.health]
contract = "heimdall.cultnet-rudp-access-health"
[state]
schema_generation = "heimdall-v1"
[[state.slots]]
id = "service"
relative_path = "heimdall.service.cc"
kind = "cultcache-file"
schema = "heimdall.service_state.v1"
writer = "process-bound-single-writer"
recovery = "preserve"
startup = "create-or-open-after-write-lease"
[[provides]]
capability = "gamecult.shared-auth"
schema = "heimdall.access-discovery.v1"
compatibility = "v1"
# Heimdall publishes its presence to Odin over CultMesh RUDP and never answers
# an Idunn route challenge, so it declares the Odin dependency: Idunn admits it
# as Ready on Odin's correlation, not on a challenge it will not answer.
[[dependencies]]
kind = "shared-infrastructure"
capability = "odin.verse-rendezvous"
schema = "odin.verse-topology.v1"
compatibility = "v1"
startup = "before-promotion"