Skip to content

feat(den-api): add a kubernetes worker provisioner - #4680

Draft
nathanielheitsch wants to merge 6 commits into
different-ai:devfrom
nathanielheitsch:feat/kubernetes-provisioner
Draft

feat(den-api): add a kubernetes worker provisioner#4680
nathanielheitsch wants to merge 6 commits into
different-ai:devfrom
nathanielheitsch:feat/kubernetes-provisioner

Conversation

@nathanielheitsch

Copy link
Copy Markdown

Summary

Adds a kubernetes worker-provisioner mode to Den. Instead of requiring a
Daytona/Render account, Den can provision each cloud worker as an in-cluster
Deployment + Service + token Secret + two PVCs, using a namespaced
Role/RoleBinding (no ClusterRole). This lets self-hosted Den deployments
run workers entirely inside their own cluster.

Mirrors the existing daytona.ts provider surface exactly:

  • Provision — create PVCs, token secret, Deployment, Service; poll pod
    readiness via the Service DNS name.
  • Wake — patch replicas back to 1 and reconcile the image (adopt-image
    path for workers created on an older image).
  • Stop — scale to 0 (objects retained, PVCs preserved).
  • Deprovision — delete all five objects, tolerating already-gone ones.
  • Preview / inspect / health-timeout — TTL preview record, replica
    state, and health-timeout diagnostics that redact worker-printed tokens.

No new runtime dependencies: kubernetes-client.ts is a ~210-line REST
client on undici (already a direct dependency).

Changes

  • ee/apps/den-api/src/env.tsPROVISIONER_MODE=kubernetes enum value +
    KUBERNETES_* config vars (apiUrl, apiToken, apiCaFile, workerNamespace,
    workerImage, workerPort, workerStorageClass, workerResources, TTL, approval
    mode, imagePullPolicy).
  • ee/apps/den-api/src/workers/kubernetes-client.ts — minimal K8s REST client
    (get/create/patch/delete/list, namespaced, bearer + optional CA).
  • ee/apps/den-api/src/workers/kubernetes.ts — the provider (744 lines),
    mirroring daytona.ts.
  • Dispatch edits in provisioner.ts, cloud-lifecycle.ts,
    worker-access.ts, routes/workers/shared.ts, cloud-failure.ts.
  • Gate fixes so kubernetes is accepted by remote-session-capabilities.ts
    and cloud-agent-executor.ts.
  • packaging/helm/openwork-ee/ — chart support: worker SA + namespaced
    Role/RoleBinding (kubernetes-workers.yaml), den-api SA wiring, configmap
    • values, and a chart test (tests/kubernetes-workers.sh).
  • evals/ — a mock K8s apiserver witness
    (mock-kubernetes-apiserver.ts) + a journey spec
    (kubernetes-worker-provisioning.test.ts) that exercises the full
    provision/wake/stop/deprovision cycle against the mock.
  • docs/kubernetes-workers.md — operator-facing docs.

Testing

Unit / eval / chart (all passing)

  • ee/apps/den-api/test/kubernetes-provisioning.test.ts — 19 tests (name
    mapping, fresh create, adopt, 409 tolerance, token upsert, image reconcile,
    stop, deprovision, preview, inspect, health-timeout redaction).
  • evals/packages/labs/test/mock-kubernetes-apiserver.test.ts — 125 lines.
  • evals/specs/kubernetes-worker-provisioning.test.ts — 264-line journey.
  • packaging/helm/openwork-ee/tests/kubernetes-workers.sh — 159 lines.
  • tsc clean; full den-api suite green.

Live cluster test (k3s, openwork-dev / openwork-workers-dev)

Deployed the fork chart against a real k3s cluster and exercised the
provisioner end-to-end:

  • POST /v1/workers → 202 → den-api created Deployment + Service + Secret +
    2 PVCs in openwork-workers-dev; worker pod reached /health via Service
    DNS; DB status → healthy.
  • DELETE /v1/workers → 204; all five objects deleted.
  • RBAC verified verb-for-verb (no permission errors).
  • LLM backend (Bifrost): configured a custom Den LLM provider pointing at
    an in-cluster Bifrost gateway (http://bifrost.bifrost.svc:80) fronting
    vLLM. Materialized the provider + key into a running worker and ran a real
    prompt through the worker's opencode → routed to Bifrost → vLLM
    qwen3.8-27b → returned the expected response. Confirms the worker can
    reach an in-cluster inference backend directly (Den is not in the
    inference data path).

Notes / follow-ups

  • routes/cloud/index.ts cloud-browser-instance path stays daytona-only in
    this PR (deferred to a follow-up); the worker lifecycle + gates accept
    kubernetes.
  • DCO-signed commits; individual CLA basis for the ee/ contribution.

Adds PROVISIONER_MODE=kubernetes so self-hosted Den deployments can
auto-provision cloud workers as in-cluster workloads instead of relying
on the Daytona or Render providers.

- kubernetes-client.ts: lean Kubernetes REST client over undici (no new
  dependencies), resolving in-cluster ServiceAccount defaults with
  optional KUBERNETES_API_URL/TOKEN/CA_FILE overrides
- kubernetes.ts: per-worker Deployment, ClusterIP Service, token Secret,
  and workspace/data PVCs named from the worker id; provisioning adopts
  existing objects on conflict, wake patches replicas (and a stale
  image), stop scales to zero preserving PVCs, deprovision deletes the
  full object set
- env.ts: kubernetes config block mirroring the daytona block, with
  KUBERNETES_WORKER_IMAGE required when the mode is selected
- dispatch wiring in provisioner.ts, worker-access.ts, cloud-lifecycle.ts
  (idle stop now covers kubernetes), and routes/workers/shared.ts so the
  mode participates in cloud-instance routing like daytona
- cloud-failure.ts: generalize the health-timeout classifier so
  kubernetes health deadlines map to runtime_health_timeout

Signed-off-by: Nathaniel Heitsch <nathaniel@heitsch.dev>
- tolerate 409 AlreadyExists on PVC/Secret/Service creates in the fresh
  provision path by verifying the existing object and adopting it, so a
  partial create or half-completed deprovision cannot dead-end reprovision
- default KUBERNETES_WORKER_APPROVAL_MODE to auto; manual mode's gated
  writes time out 403 with no approvals responder in a sandbox
- accept the kubernetes provisioner in the remote-session capability and
  cloud-agent-executor runtime gates; routes/cloud stays daytona-gated
- pin the worker Deployment to Recreate strategy so image patches cannot
  MultiAttach-stall behind a rolling surge pod on ReadWriteOnce PVCs
- redact worker tokens from pod log tails embedded in health-timeout errors
- drop OPENWORK_CONNECT_HOST from the container env; the entrypoint binds
  openwork-server with --host 0.0.0.0 and only used CONNECT_HOST for banners
- substitute the DNS-safe worker name (not the raw worker id) into
  WORKER_URL_TEMPLATE and document the placeholder contract
- patch an adopted Deployment's stale container image before reporting
  healthy so the DB imageVersion matches the running pod
- drop the unused base64 helper and unused getPod client method; add the
  getSecret client method used by the conflict-adopt verify path
- expand provisioning tests: 409 adopt, secret delete/recreate, stale-image
  adopt patch, URL template substitution, deprovision 404 tolerance, and
  token redaction in health-timeout diagnostics

Signed-off-by: Nathaniel Heitsch <nathaniel@heitsch.dev>
…ourney, and docs

- values.yaml: config.kubernetes block + workers.kubernetes RBAC/namespace
  config; secret.kubernetesApiToken (KUBERNETES_API_TOKEN) in the release secret
- configmap.yaml: emit the KUBERNETES_* surface only in kubernetes mode, with
  the worker namespace pinned so rendered RBAC and API calls agree
- kubernetes-workers.yaml: ServiceAccount, optional worker Namespace, and a
  namespaced Role/RoleBinding (no cluster-scoped objects) for the worker
  namespace; den-api.yaml sets serviceAccountName in kubernetes mode
- tests/kubernetes-workers.sh: default/mode/custom/invalid renders, including
  an assert that no ClusterRole or ClusterRoleBinding is ever rendered
- evals: mock-kubernetes-apiserver witness (create/get/patch/delete, strategic
  merge, labelSelector, logs, readiness toggles) with unit tests, and the
  kubernetes-worker-provisioning journey spec covering provision, idle stop,
  wake, stale-image recycle, deprovision, and stub-mode inertia
- docs: README values section + docs/kubernetes-workers.md operator guide
  (architecture, RBAC, lifecycle, data-erasure semantics, troubleshooting)

Signed-off-by: Nathaniel Heitsch <nathaniel@heitsch.dev>
…ontent type, fail fast on missing worker image

- RBAC now matches the kubernetes client exactly: get/create/patch/delete
  on deployments (the client never uses update or list), get/create/delete
  on services/secrets/pvcs, and get+list on pods (get is required for the
  pods/log subresource reads used by health-timeout diagnostics)
- mock apiserver only applies strategic merge semantics for PATCH requests
  with application/strategic-merge-patch+json, returns 415 otherwise, and
  records the PATCH content type for journey assertions
- chart render fails loudly when config.provisioner.mode=kubernetes and
  config.kubernetes.workerImage is empty, mirroring the SA-name fail-fast

Signed-off-by: Nathaniel Heitsch <nathaniel@heitsch.dev>
Signed-off-by: Nathaniel Heitsch <nathaniel@heitsch.dev>
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openwork-landing Ready Ready Preview, v0 Sep 9, 2026 9:56pm UTC

@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

@nathanielheitsch is attempting to deploy a commit to the Different AI Team on Vercel.

A member of the Team first needs to authorize it.

Completes the deferred PR different-ai#2: routes/cloud/index.ts now dispatches
getSandboxRecord / inspectSandbox / refreshSignedPreview / stopCloudWorker
through the provisioner-mode-aware defaults (worker-access.ts) instead of
hard-coding the Daytona implementations, and hasDaytonaProvisioner accepts
kubernetes when KUBERNETES_WORKER_IMAGE is set. cloudAvailable therefore
returns true for kubernetes, so the desktop's GET /v1/cloud/instance
resolves and the auto-provisioned worker option appears.

- worker-access.ts: export defaultGetSandboxRecord / defaultInspectSandbox /
  defaultRefreshSignedPreview + the RefreshSignedPreview type
- cloud/index.ts: use those defaults, kubernetes-aware hasDaytonaProvisioner,
  cloudWorkerImage() for latestVersion / update detection

tsc clean; cloud-instance-route 40/40 pass; den-api suite no worse than
baseline (failures are pre-existing missing-local-MySQL).

Signed-off-by: Nathaniel Heitsch <nathaniel@heitsch.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant