{"protocol":"clank-doc/1","frameworkVersion":"0.9.4","slug":"provider-adapters","title":"Deployment provider adapters","description":"Clank separates durable deployment coordination from infrastructure mutations. The control plane owns desired generations, node identity, placement, operation leases, release provenance, and fencing. A deployment provider owns the host spec","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/provider-adapters","source":"docs/provider-adapters.md","headings":["Standard agent","Authenticated HTTP bridge","Packaged runner","Provider correctness contract","What is and is not portable yet"],"tableOfContents":[{"id":"standard-agent","title":"Standard agent","level":2},{"id":"authenticated-http-bridge","title":"Authenticated HTTP bridge","level":2},{"id":"packaged-runner","title":"Packaged runner","level":2},{"id":"provider-correctness-contract","title":"Provider correctness contract","level":2},{"id":"what-is-and-is-not-portable-yet","title":"What is and is not portable yet","level":2}],"markdown":"# Deployment provider adapters\n\nClank separates durable deployment coordination from infrastructure mutations. The control plane\nowns desired generations, node identity, placement, operation leases, release provenance, and\nfencing. A deployment provider owns the host-specific act of converging one project to a running\nrelease or a stopped state.\n\nThis boundary is intentionally small:\n\n```ts\nimport type { DeploymentProvider } from \"@clank.run/framework/provider\";\n\nconst provider: DeploymentProvider = {\n  kind: \"microvm\",\n  async reconcile(request) {\n    // Node and operation credentials are never passed to provider code.\n    // Persist the highest generation and fence before mutating infrastructure.\n    await reconcileMicroVm({\n      projectId: request.operation.projectId,\n      operationId: request.operation.id,\n      fence: request.operation.fence,\n      generation: request.desired.generation,\n      state: request.desired.state,\n      releaseId: request.desired.releaseId,\n      artifact: request.artifact,\n      signal: request.signal,\n    });\n  },\n};\n```\n\nFor a running generation, `request.artifact` contains the original compressed bytes, their\nSHA-256, and a decoded `clank-deploy/1` bundle. Clank independently checks the transport digest,\nevery file path, size, mode, checksum, deployment config, and aggregate bound before the provider\nruns. A stopped generation has no release or artifact.\n\n## Standard agent\n\n`openProviderDeploymentAgent()` layers this contract over the normal remote-node lifecycle:\n\n```ts\nimport {\n  createDeploymentCoordinatorClient,\n  fileDeploymentNodeCredentials,\n} from \"@clank.run/framework/runner\";\nimport {\n  openProviderDeploymentAgent,\n} from \"@clank.run/framework/provider\";\n\nconst agent = await openProviderDeploymentAgent({\n  client: createDeploymentCoordinatorClient({\n    baseUrl: \"https://clank.example.com\",\n  }),\n  node: {\n    id: \"runner-west-01\",\n    region: \"us-west\",\n    capacity: 20,\n    labels: { isolation: \"microvm\", architecture: \"amd64\" },\n  },\n  provider,\n  registrationToken: process.env.CLANK_RUNNER_REGISTRATION_TOKEN,\n  credentials: fileDeploymentNodeCredentials(\n    \"/var/lib/clank-runner/credentials.json\",\n  ),\n  concurrency: 4,\n});\n```\n\nThe wrapper accepts only canonical `reconcile` operations, validates the exact three-field desired\npayload, downloads and verifies an artifact only for a running state, calls the provider, then\nreports the matching generation as observed. A rejected stale observation fails the operation\ninstead of claiming success.\n\nThe operation result sent back to the control plane is fixed to provider kind, generation,\nrelease ID, and state. Provider return values, exception details, credentials, paths, and runtime\nmetadata do not become durable control-plane records.\n\n## Authenticated HTTP bridge\n\nProvider code can run in a separate private service. Clank includes both sides of one exact binary\nprotocol:\n\n```ts\nimport {\n  createDeploymentProviderHandler,\n  createHttpDeploymentProvider,\n} from \"@clank.run/framework/provider\";\n\nconst remoteProvider = createHttpDeploymentProvider({\n  baseUrl: \"https://runtime.internal.example\",\n  token: process.env.CLANK_PROVIDER_TOKEN!,\n});\n\nconst handler = createDeploymentProviderHandler(provider, {\n  token: process.env.CLANK_PROVIDER_TOKEN!,\n  maxArtifactBytes: 100 * 1024 * 1024,\n  onError(error) {\n    privateOperatorLog(error);\n  },\n});\n```\n\nThe fixed path is `POST /v1/clank/reconcile`. Running requests carry\n`application/vnd.clank.deploy+gzip` bytes directly, not base64 JSON. Bounded headers identify the\nproject, operation, fence, attempt, generation, desired state, release, and artifact SHA-256.\nStopped requests carry no body, release, or digest. A successful provider returns `204`.\n\nBoth sides require a separate 32–512 character bearer token. Non-loopback clients require HTTPS,\nrefuse redirects, apply deadlines and body limits, discard bounded failure bodies, and retry only\nthe same idempotent request after network, 408, 425, 429, or 5xx failure. The handler rehashes and\ndecodes the artifact again because the HTTP hop is a new trust boundary. Provider exceptions reach\nonly the private `onError` hook; callers receive a stable generic error.\n\nDo not expose this endpoint to browsers or reuse Clank account, CLI, enrollment, node, or operation\ncredentials for it. Prefer a private network plus narrowly scoped edge admission even though the\nrequest is authenticated.\n\n## Packaged runner\n\n`clank-runner` connects the coordinator to the HTTP bridge without application code:\n\n```sh\nexport CLANK_CONTROL_URL=https://clank.example.com\nexport CLANK_RUNNER_NODE_ID=runner-west-01\nexport CLANK_RUNNER_REGION=us-west\nexport CLANK_RUNNER_REGISTRATION_TOKEN=\"$(secret read clank-runner-enrollment)\"\nexport CLANK_RUNNER_CREDENTIALS=/var/lib/clank-runner/credentials.json\n\nexport CLANK_PROVIDER_URL=https://runtime.internal.example\nexport CLANK_PROVIDER_TOKEN=\"$(secret read clank-runtime-provider)\"\n\nclank-runner\n```\n\nAfter successful enrollment, remove `CLANK_RUNNER_REGISTRATION_TOKEN` from the long-running\nservice. The persisted node credential is sufficient for restart. `SIGINT` and `SIGTERM` drain the\nnode before exit. Run `clank-runner --help` for capacity, label, concurrency, timeout, retry, and\nartifact-limit settings.\n\nThis process requires no extra npm package or managed service. A systemd unit, container, VM, or\nexisting scheduler can supervise it. The control plane's normal single-host mode remains the\nzero-cost default.\n\n## Provider correctness contract\n\nEvery adapter must satisfy all of these rules:\n\n1. Store the latest accepted generation and monotonic fence per project in transactional durable\n   state. Reject older work.\n2. Treat the operation ID plus fence as an idempotency key. A response can be lost after the\n   provider commits, so retry is normal.\n3. Pass the abort signal to provider calls and stop local subprocess/container work when possible.\n   Abort does not roll back an already committed remote mutation.\n4. Stage a new release separately, health-check it, switch ingress atomically, then drain the old\n   generation. Never overwrite an active release in place.\n5. Keep application data outside immutable release directories. Mount or bind only the one\n   project's data into its runtime.\n6. Resolve secrets on the provider side from a project-scoped secret store. Release artifacts\n   intentionally contain no platform-managed secrets or application database.\n7. Use an isolated runtime boundary for mutually untrusted deployers. A process launcher is not a\n   sandbox; Docker is a minimum, while hostile workloads should use dedicated VMs or microVMs.\n8. Return success only after the requested state is externally usable. Keep private provider\n   errors in operator telemetry.\n9. Garbage-collect only releases no longer referenced by desired, observed, or rollback state.\n10. Test retry after commit, lease loss, stale fences, abort, health failure, partial ingress\n    switch, restart from durable state, and provider outage.\n\n## What is and is not portable yet\n\nThe provider contract, HTTP bridge, runner command, release transport, object storage, leases,\ncredentials, and fencing are implemented and package-supported. They remove provider SDKs from\nClank and give Docker, VM, microVM, Nomad, Kubernetes, or hosted adapters the same narrow surface.\n\nThe built-in control plane still activates its ordinary hosted projects through its local\nsupervisor. Enabling enrollment or starting `clank-runner` does not silently relocate existing\nprojects. A fully remote installation must deliberately connect desired-state creation, a\nprovider-side project secret source, application data, health routing, TLS/edge routing, log and\nmetric collection, and deletion/backup policy. Clank refuses to pretend those data and security\nboundaries are solved by uploading code alone.\n\nThe eventual provider integration should consume the same contract rather than adding\nprovider-specific logic to application code or the control database.\n"}