{"protocol":"clank-doc/1","frameworkVersion":"0.9.4","slug":"runtime-placement","title":"Remote runtime placement","description":"Clank has a versioned runtime capsule contract for moving one verified application generation across the control plane, deployment runner, and infrastructure provider boundaries. It is the transport foundation for remote placement; it does ","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/runtime-placement","source":"docs/runtime-placement.md","headings":["Capsule contents","Lease scoped transport","Provider contract","Trust boundary","Activation status"],"tableOfContents":[{"id":"capsule-contents","title":"Capsule contents","level":2},{"id":"lease-scoped-transport","title":"Lease-scoped transport","level":2},{"id":"provider-contract","title":"Provider contract","level":2},{"id":"trust-boundary","title":"Trust boundary","level":2},{"id":"activation-status","title":"Activation status","level":2}],"markdown":"# Remote runtime placement\n\nClank has a versioned runtime-capsule contract for moving one verified application generation\nacross the control-plane, deployment-runner, and infrastructure-provider boundaries. It is the\ntransport foundation for remote placement; it does not enable remote placement by itself.\n\nThe ordinary `clank-deploy/1` release remains a non-secret, immutable code archive. A\n`clank-runtime/1` capsule binds that release to the final process environment, SQLite placement\nintent, and managed-ingress identity needed by one exact project generation.\n\n## Capsule contents\n\nThe binary capsule contains three length-delimited sections:\n\n1. a UTF-8 JSON manifest;\n2. the original compressed `clank-deploy/1` artifact; and\n3. an optional SQLite snapshot.\n\nThe fixed header records each section length before any section is interpreted. The manifest\nrecords:\n\n- protocol, project ID, release ID, and desired generation;\n- the final environment, including application secret values;\n- the database path and `initialize`, `preserve`, or `replace` intent;\n- optional snapshot length and SHA-256;\n- a provider-local ingress route and high-entropy ingress token; and\n- artifact length and SHA-256.\n\nThe artifact stays in its original binary form and the SQLite snapshot stays in its original\nbinary form. Neither is base64 encoded or placed in JSON.\n\n```ts\nimport {\n  createDeploymentRuntimeCapsule,\n} from \"@clank.run/framework/runtime-placement\";\n\nconst runtime = await createDeploymentRuntimeCapsule({\n  projectId: \"project_01\",\n  releaseId: \"release_07\",\n  generation: 12,\n  environment: {\n    NODE_ENV: \"production\",\n    APP_SECRET: decryptedProjectSecret,\n  },\n  database: {\n    path: \"app.sqlite\",\n    mode: \"replace\",\n    snapshot: sqliteSnapshot,\n  },\n  ingress: {\n    route: \"/projects/project_01\",\n    token: ingressOnlyToken,\n  },\n  artifact: releaseBytes,\n});\n```\n\nCreation and decoding both:\n\n- bound section and aggregate byte counts;\n- require exact manifest fields and supported protocol values;\n- reject unsafe identifiers, paths, routes, environment names, and control characters;\n- decode and verify the nested deployment bundle again;\n- require the database path to match the deployment config;\n- enforce database-mode/snapshot consistency;\n- require the SQLite file header when a snapshot is present; and\n- verify the artifact, snapshot, and whole-capsule SHA-256 values.\n\n`preserve` never carries a snapshot. `replace` always carries one. `initialize` can carry a\nseed snapshot or ask the provider to create empty project data. These are desired-state semantics;\nthe provider must apply them atomically and idempotently.\n\n## Lease-scoped transport\n\nWhen a coordinator is configured with a runtime source, the runner client can call:\n\n```ts\nconst runtime = await coordinator.runtime(\n  session.node.id,\n  session.token,\n  operation,\n);\n```\n\nThe call needs both the exact current node credential and the unexpired operation token/fence. The\ncoordinator selects the canonical stored operation rather than trusting the runner's echoed\npayload, loads the capsule, verifies its digest, rechecks the lease after provider work, and returns\nan exact `application/vnd.clank.runtime` body with `private, no-store`.\n\nThe client refuses redirects and non-loopback cleartext HTTP, requires the exact media type,\ndeclared length, and SHA-256, bounds the streamed response, and applies a separate deadline.\n`clank-runner` exposes:\n\n```sh\nCLANK_RUNNER_RUNTIME_TIMEOUT_MS=120000\nCLANK_RUNNER_MAX_RUNTIME_BYTES=805306368\n```\n\nTune the artifact and runtime bounds together. The runtime ceiling must accommodate the manifest,\ncompressed release, and largest permitted SQLite handoff without exceeding the provider and\nnetwork limits.\n\n## Provider contract\n\nA reconcile operation opts into the capsule explicitly:\n\n```ts\nawait orchestrator.setDesired({\n  projectId,\n  releaseId,\n  state: \"running\",\n  region: \"iad\",\n  runtimeProtocol: \"clank-runtime/1\",\n});\n```\n\n`openProviderDeploymentAgent()` then uses `context.runtime()` instead of the legacy artifact\ndownload. Before provider code runs, Clank verifies the whole capsule and requires its project ID,\nrelease ID, and generation to match the canonical desired operation.\n\nThe provider receives:\n\n- `request.runtime`, the decoded and verified capsule;\n- `request.artifact`, the verified nested release from that same capsule;\n- the credential-free operation identity and monotonic fence;\n- the desired generation/state/release; and\n- an abort signal.\n\nThe authenticated HTTP provider bridge forwards the exact capsule bytes as its bounded request\nbody. Only the protocol selector, content digest, and non-secret operation metadata are placed in\nheaders. Environment values, SQLite bytes, and the ingress token are never placed in URLs,\nheaders, public failures, or durable operation results. The provider handler rehashes, decodes,\nand rebinds the capsule because that HTTP hop is a separate trust boundary.\n\n## Trust boundary\n\nThe runtime capsule is sensitive plaintext while in memory and transit. This is intentional: the\nruntime provider must possess the application environment and data to launch the application.\nTreat the provider host as part of the application's trusted compute boundary.\n\n- Use HTTPS outside loopback and preferably a private network.\n- Give the provider bridge a distinct, narrowly scoped, rotated bearer token.\n- Never log, persist, inspect, or cache capsule bodies in generic proxies or request middleware.\n- Keep node credentials, operation tokens, browser sessions, CLI tokens, and the platform master\n  key out of the capsule and provider request.\n- Isolate mutually untrusted applications with VMs or microVMs; a shared process is not a sandbox.\n- Encrypt independent backups before sending them to an external repository.\n- Make provider mutations idempotent under project, generation, operation ID, and fence.\n\nCompromise of the trusted provider can expose every secret and database currently placed on it.\nCapsule authentication and integrity prevent an untrusted runner or network peer from selecting or\naltering another runtime; they do not make the destination host untrusted compute.\n\n## Activation status\n\nThe capsule codec, lease-scoped coordinator call, standard deployment-agent access, desired-state\nprotocol selector, provider verification, authenticated HTTP forwarding, public declarations,\npackaged-runner limits, and generation-bound managed-ingress route contract are implemented and\ntested. The package-supported provider runtime registry validates that contract before forwarding\nto a loopback application, retains only the route-token digest, permits safe generation overlap,\nand revokes then drains exact generations. A remote ingress route fixes the allowlisted provider\norigin, provider-local path, `clank-runtime/1` protocol, desired generation, project, and private route token. Public requests\ncannot override those headers, health uses the same binding, and an old generation's circuit state\ndoes not carry into the next generation.\n\nThe built-in control plane deliberately does **not** select `clank-runtime/1` for hosted projects\nyet. The package-supported [provider data lifecycle](provider-data-lifecycle.md) now stages code\nwithout persisting secrets; initializes, preserves, replaces, snapshots, rolls back, and deletes\nper-project SQLite data; applies immutable migrations; retains one rollback generation; and\nrecovers interrupted apply and rollback journals.\n\nRemote activation remains closed until the implemented data and ingress contracts are bound end\nto end to an isolated runtime launcher, atomic control-plane publish/revoke, stateful node pinning,\nencrypted recovery replication, restart reconciliation, and lease-loss/traffic-switch\ncertification.\n\nUntil those controls land, enabling a runner fleet does not move existing applications or their\ndata. The current single-host deployment path stays unchanged and no additional Railway service,\nvolume, database, or object store is required.\n\nContinue with [Provider data lifecycle](provider-data-lifecycle.md), [Deployment provider\nadapters](provider-adapters.md), [Durable distributed deployment](distributed-deployment.md),\n[Managed ingress and external data](data-plane.md), [Recovery](recovery.md),\n[Provider runtime ingress](provider-runtime-ingress.md), and\n[Platform security](platform-security.md).\n"}