Open Cowork Cloud¶
Open Cowork Cloud is the provider-neutral deployment shape for running the Open Cowork product layer as a web app. It keeps the same ownership boundary as the desktop app: OpenCode owns execution, while Open Cowork owns composition, profiles, durable projection, workflows, and deployment policy.
Choose a deployment topology before choosing a cloud provider. Cloud-only, Cloud Channel Gateway, Cloud Gateway edge, and full-hybrid deployments are defined in Deployment Topologies and the machine-readable deploy/topologies/topology-profiles.json contract. Product naming, release channels, image names, and Gateway product-mode policy are defined in Packaging and Gateway Product Modes.
The cloud entrypoint is one OCI image with role-based behavior:
| Role | Purpose | Scaling note |
|---|---|---|
all-in-one | Runs web, worker, and scheduler together. | Local demos, focused internal pilots, and single-process installs. |
web | Serves HTTP APIs and browser SSE from durable state. | Stateless; scale horizontally behind a load balancer. |
worker | Owns OpenCode execution and consumes durable session commands. | Long-running compute; scale by active-session capacity. |
scheduler | Atomically claims due workflows. | Safe to run multiple replicas, though one is enough for most deployments. |
Production deployments should prefer split roles. Cloud Run-style all-in-one deployments are useful for demos, but long-running OpenCode workers should run on dedicated Docker-capable hosts or reviewed provider integrations with predictable CPU/process lifetime and verified per-execution isolation.
Keep worker replicas conservative unless runtime/workspace checkpointing is enabled for your deployment. Multiple workers can claim fenced sessions, and checkpointing externalizes the proven OpenCode runtime/XDG and workspace portable set so a different worker can restore before resuming a session.
The managed worker service plane builds on the same split-role model. It adds explicit worker pools, worker identity, lifecycle controls, durable leases, fencing tokens, recovery rules, quotas, and operations runbooks for laptop-independent cloud execution. The architecture, threat model, and operations contract are in Managed Worker Service Plane; public deployment templates live under deploy/managed-workers/.
Workspace sync contract¶
The full cross-surface contract is documented in Product Contract. Cloud implements the cloud-workspace side of that contract.
The browser release contract and route/API matrix are documented in Cloud Web Workbench.
Cloud is the source of truth only for cloud workspaces. A user can start a cloud thread in desktop, continue it in the browser, and interact with the same thread through a gateway channel because all three clients read and write the same cloud session, command, event, projection, artifact, workflow, settings, and policy records.
Local desktop workspaces are separate. Cloud does not automatically ingest local threads, host project paths, local stdio MCP commands, machine-native runtime config, provider keys, OAuth tokens, or local-only artifacts. The cloud worker owns OpenCode execution for cloud threads; the desktop owns OpenCode execution for local threads; the gateway owns only channel I/O.
workspace.support() is the capability contract between cloud policy and clients. It returns supported, read_only, blocked_by_policy, not_supported, or deferred entries with user-facing verdict reasons. Web, desktop, and gateway clients should use those verdicts before enabling sends, project pickers, host-path diffs, custom content, artifacts, workflows, or gateway interactions.
Cloud Project Context¶
Cloud sessions do not read arbitrary desktop or server host paths. A cloud thread starts with an explicit project source that the worker restores into an isolated app-managed workspace before creating or prompting the OpenCode session.
Supported v1 sources:
- Git source: HTTPS repository URL, optional branch/tag/commit ref, optional subdirectory, and optional credential ref. Credentials must be stored as secret references (
env:,gcp-sm://,aws-sm://,azure-kv://, or Azure Key Vaulthttps://URLs); they must not be embedded in repository URLs. A credential secret may be a raw access token, or JSON withusernamepluspasswordortokenfor hosts that require a specific Git username. - Uploaded snapshot: the desktop inventories a selected directory, excludes secret-bearing files and dependency/build output, shows file count and bytes, uploads the bounded snapshot to object storage, and sends only the snapshot reference in the session create request.
Default policy:
{
"cloud": {
"projectSources": {
"git": {
"enabled": true,
"allowedHosts": ["github.com", "gitlab.com"],
"allowedRepositories": [],
"allowFileUrls": false
},
"uploadedSnapshots": {
"enabled": true,
"maxFiles": 2000,
"maxBytes": 26214400
},
"managedWorkspaces": {
"enabled": false
}
}
}
}
For GitHub Enterprise or GitLab self-managed, add the host to cloud.projectSources.git.allowedHosts. To restrict tenants to approved repos, set allowedRepositories to exact lowercase host/org/repo keys, for example git.internal.example/platform/api.
Deployments that cannot allow local uploads should set:
Managed workspace checkpointing is the v2 path for long-running or scheduled cloud work. Enable it only with object storage and checkpoint encryption configured:
Headless gateway agents can define a default project source on a channel binding via settings.defaultProjectSource, or through a profile-level defaultProjectSource. The gateway passes the binding to cloud; cloud workers still own restore and execution.
Local References¶
Use the all-in-one reference for quick local checks:
Use the Cloud Channel Gateway appliance references when a user should talk to Cloud from a headless channel:
docker compose -f docker-compose.gateway-remote.yml up --build
docker compose -f docker-compose.cloud-gateway.yml up --build
The remote Cloud Channel Gateway compose file connects to an existing Cloud URL and binds loopback by default. The Cloud + Gateway compose file runs a local all-in-one Cloud control plane, Postgres, MinIO, and Cloud Channel Gateway on one host. See docs/gateway-appliance.md for VPS, Mac mini, Raspberry Pi, systemd, launchd, Telegram webhook, TLS, firewall, upgrade, and rollback guidance. Provider launch tiers and contract tests are tracked in docs/gateway-provider-readiness.md.
Use the split-role reference when testing the scalable topology:
The split compose file starts web, worker, and scheduler services against shared Postgres and MinIO. The web role writes commands only; the worker role binds OpenCode runtime sessions and executes them.
All Compose references are local/demo manifests. They use local builds, loopback HTTP URLs, local Postgres/MinIO credentials, and explicit insecure auth overrides. They are not production isolation topologies. A production execution worker currently requires a dedicated Docker-capable host running the built-in provider, or a downstream composition that injects an external provider satisfying the same isolation contract. Pin the Cloud, Gateway, and per-execution runtime images to reviewed digests, replace every demo secret, use HTTPS origins, back checkpoints with provider object storage, and pass the required two-tenant proof before admitting traffic.
The cloud web role serves the browser web app at /. It uses the same HTTP/SSE contract as API clients: sessions are loaded from durable projections, prompts are written as commands, and session event streams reconnect from the last durable event sequence.
The workspace @open-cowork/cloud-client package provides the typed createHttpSseCloudTransportAdapter for clients built from this repo that need HTTP/SSE Cloud transport without Electron IPC.
Cloud Web Workbench¶
The cloud web role serves the Cloud Web Workbench at /. The workbench is an API client for the same cloud control plane used by desktop sync and gateway clients; it does not access Postgres or secret storage directly.
The browser app is the unified Open Cowork renderer (packages/app/src) — the same UI that runs on Electron Desktop. In the browser it runs against a typed CoworkAPI shim (packages/app/src/browser/cowork-api.ts) backed by the cloud HTTP + SSE API, and is served as a hashed-asset SPA under the server's CSP nonce and cookie-auth path. There is no separate Cloud Web codebase: the cloud serves the renderer's browser build (packages/app/dist-browser, produced by pnpm cloud:build and served from apps/desktop/dist/cloud/browser-renderer), so Desktop and Cloud Web cannot drift.
The information architecture is split into two surfaces:
- Workbench: Projects, Chat, Team, Tools & Skills, Playbooks, Knowledge, Channels, and Artifacts.
- Admin: Org, Members, Profiles & Policy, BYOK, Connections, Billing, Gateway, Audit, Usage, and Diagnostics.
The Projects and Chat panels are the first interactive workbench surfaces:
GET /api/sessionsloads the user-scoped durable session list. It acceptslimit, opaquecursor,status,profileName, and text query viaqorquery, and returnssessions,nextCursor, andtotalEstimateso large orgs never require unbounded list scans.GET /api/sessions/:id/viewhydrates the selected thread from its durable projection and sharedSessionView.POST /api/sessionscreates a browser-origin cloud thread with a chat-only, allowed Git, or uploaded snapshot project source.POST /api/sessions/:id/promptsends browser prompts through the same durable command path used by desktop sync and gateway clients.GET /api/sessions/:id/eventsandGET /api/eventskeep the selected thread and thread list live through SSE, resuming from durable event sequences after reconnect.- Operator-only
GET /api/sessions/:id/projection-statusandPOST /api/sessions/:id/projection-repairexpose projection lag and replay repair from the durable event log.
The Chat panel renders the full cloud runtime projection contract rather than a minimal message list. It includes user and assistant messages, streaming-safe assistant snapshots, task runs, compact tool traces, pending and resolved permission approvals, pending and resolved questions, artifacts, todos, cost and token totals, context/compaction state, and categorized errors. Browser approval and question actions call the same durable command routes used by desktop and gateway clients:
POST /api/sessions/:id/permission-respondPOST /api/sessions/:id/question-replyPOST /api/sessions/:id/question-reject
Artifact metadata is listed from the durable projection and GET /api/sessions/:id/artifacts. Artifact bodies are fetched only for explicit View or Download actions through GET /api/sessions/:id/artifacts/:artifactId; the browser does not keep artifact bodies, signed object-store URLs, bucket names, or object keys in long-lived state.
The rest of the Workbench uses the same cloud API contract:
- Team (coworkers) is derived from the current profile's allowed agent list plus the cloud-safe tool and skill catalog. Starting a coworker creates a cloud project chat and pins that agent into the composer; execution still flows through the cloud worker and OpenCode runtime.
- Tools & Skills browse
/api/capabilities, including source, scope, agent relationships, and policy notes. The browser renders cloud-safe metadata only; custom content that is synced but disabled by profile policy is shown as unavailable rather than exposed with local process details or secrets. - Playbooks use
/api/workflowsfor durable definitions and run history,POST /api/workflows/:id/runfor manual runs, and the pause/resume/archive routes for lifecycle changes. Manual runs can open the generated run chat so users can inspect the same projection shown in Chat. - Artifacts includes a selected-chat browser plus selected-chat history from the hydrated durable projection. Cross-chat artifact browsing remains deferred until Cloud exposes a durable artifact index API. Artifact bodies are still fetched only on explicit user action.
Thread list filtering is client-side over the current durable session snapshot: search text, status, profile, project kind, and tag/smart-filter metadata are supported. The browser paginates large org thread lists in fixed batches so a thousands-sized session list remains responsive without changing the cloud API.
Browser-created project context is explicit and policy checked. A Git source is validated by /api/project-sources/validate; an uploaded snapshot is first stored through /api/project-sources/snapshots, then referenced by session creation. The browser never receives or sends local desktop host paths, machine-native OpenCode config, local stdio MCP process details, or local provider secrets unless the user explicitly uploads bounded project files as a cloud snapshot.
Continuation is intentionally symmetric: a thread created by desktop sync or a gateway channel is just a cloud session and can be opened in the browser; a thread created in the browser appears to desktop and gateway clients through the same session list, projection, and SSE contracts.
Workflow webhook secret storage and rollout¶
Workflow webhook secrets are credentials, not workflow metadata. Cloud stores them in cloud_workflow_webhook_secrets as envelope-encrypted ciphertext; workflow definitions, list/detail responses, run events, logs, diagnostics, and desktop cache records do not contain the plaintext. Creating a webhook-enabled workflow returns its secret once in webhookSecretReveal. Rotating through POST /api/workflows/:id/rotate-webhook-secret revokes the old value atomically and returns the replacement once. If the envelope key provider is unavailable, create, rotate, migration, and webhook authentication fail closed; there is no plaintext fallback.
The public endpoint is <OPEN_COWORK_CLOUD_PUBLIC_URL>/webhooks/workflows/<workflow-id>. Its origin is read from trusted deployment configuration, not a request Host or forwarded header. Public ingress accepts timestamped HMAC authorization only and rejects raw bearer/shared-secret headers. The Playbooks UI copies a one-time signed curl command without rendering or retaining the secret. Archiving revokes the credential. Create replacement copies a new command and restores the playbook as one action; a clipboard failure leaves it archived and requires another rotation.
Use this sequence when upgrading a deployment that may contain legacy plaintext secrets inside workflow trigger JSON:
- Apply the Cloud schema migrations, including
003_cloud_workflow_webhook_secrets, before admitting the new application build. - Stop or drain old workflow writers and webhook readers. Old builds can put plaintext back into trigger JSON and cannot authenticate secrets created in the separate encrypted table, so mixed-version workflow traffic is not a supported steady state.
- Start the new build with a healthy envelope secret adapter. Startup migrates legacy values in bounded, resumable batches: ciphertext is written before the matching plaintext is removed in the same transaction. When both legacy plaintext and an older ciphertext exist, migration encrypts the plaintext with the current adapter and atomically replaces only the exact expected tenant/workflow/trigger envelope. It never decrypts or trusts the older envelope. If current protection or the compare-and-swap fails, both the original ciphertext and last usable plaintext remain unchanged. Decryption is reserved for signed webhook validation. A rolling-deploy compatibility read on the public webhook route can migrate only that exact trigger; it cannot start a database-wide batch.
- Verify migration reports zero remaining candidates and exercise create, rotate, old-secret rejection, and active-secret acceptance before restoring normal traffic. Verification output must use counts and record identifiers, never plaintext credentials.
The schema addition is safe to leave in place during rollback. Do not roll back to an old workflow writer or reader while workflow traffic is live. Drain that traffic and forward-fix the new build, or restore a database snapshot taken before any new encrypted-only workflow secrets were created. Key-provider outages are retried after recovery; the migration deliberately leaves the legacy value intact when encryption cannot complete.
Cloud Web Workbench readiness gates¶
The browser workbench has its own release gates because /livez only proves the server is alive. Cloud Web is the browser build of the desktop renderer, so its UI is covered by the renderer suite, while the cloud control-plane behavior is covered by the cloud HTTP and continuation suites. Run these before provider rollout and keep them in CI:
The renderer suite exercises the actual UI — signed-out, member, and admin states; thread create/prompt/continue, SSE refresh, approval/question, artifact, workflow, BYOK, gateway, billing, and diagnostics flows; policy/quota/billing blocked states — together with labelled controls, keyboard-reachable navigation, focus management, active route state, reduced-motion CSS, responsive layout, and contrast. The cloud suites verify large thread/capability fixtures, bounded pagination, cursor validation, and SSE reconnect against the real cloud API, and pnpm cloud:smoke builds the production cloud bundle (including the browser renderer) and runs an import smoke so the served GET / keeps shipping.
Signed-in member users can open the workbench and read allowed org/policy/usage state. Admin-only panels are hidden for members and all admin mutations remain server-authorized. Disabled controls in the browser are an ergonomic layer only.
Authenticated users land on an org-scoped dashboard backed by GET /api/workspace. That request also performs the configured first-login org bootstrap path. The workbench also bootstraps public deployment metadata from GET /api/config; both bootstrap responses are metadata-only and must not contain provider keys, OAuth tokens, API tokens, MCP secrets, or local host paths. Admin panels expose:
- member and invite operations through
/api/admin/members, with owner/admin RBAC enforced by the API and destructive member disable/revoke actions requiring explicit confirmation, - runtime profile, feature, project-source, signup-mode, and gateway policy visibility through
/api/admin/policy, - a queryable, exportable audit log through
/api/admin/audit, gated on theaudit:readpermission (owner/admin by default). The control plane audits both control-plane actions (members, roles, policy, worker credentials) and data-plane actions (session create/import/abort, command prompt/abort/ permission-respond, artifact upload/download, and worker lease claims). The query acceptsactorId,actorType,action(event-type prefix),targetType,targetId,result,from/to, and a stable keysetcursor(page sizes are bounded)./api/admin/audit/export?format=json|csvstreams a deterministic, redacted-by-default export (secrets are scrubbed at write time; local filesystem paths are scrubbed at export time);unredacted=trueis an org-admin-only mode whose disclosure is itself recorded as anaudit.exportedevent. Audit events also emit anopen_cowork_cloud_audit_events_totalmetric so operators can alert on audit volume without scraping logs. Retention reuses the event-retention scheduler and is off by default (OPEN_COWORK_CLOUD_RETENTION_AUDIT_EVENT_MS), so no event is ever dropped before an explicitly configured window, - per-org enterprise SSO (SAML 2.0 + OIDC) and SCIM 2.0 provisioning through
/api/admin/sso(config CRUD gated on thesso:managepermission) and the/scim/v2endpoints (authenticated by a per-org SCIM bearer token). IdP secrets (SAML certificate, OIDC client secret) and the SCIM token are stored encrypted with the existing envelope key (OPEN_COWORK_CLOUD_SECRET_KEY) — never plaintext. SSO config supports an SSO-only enforcement toggle, and SCIM deprovisioning suspends the membership and revokes the member's credentials immediately. A durable, store-backed sync-event queue retries with backoff and periodically reconciles directory ↔ membership drift. This feature adds no new environment variables — it is configured entirely per-org through the admin API. See the SSO and SCIM setup runbook for IdP-specific wiring (Okta, Microsoft Entra ID, Google Workspace), - BYOK provider status, plaintext key rotation, and KMS reference submission through metadata-only BYOK APIs,
- one-time API token issuance and revocation for desktop and gateway clients,
- headless gateway agent/channel setup through channel binding APIs, plus delivery backlog retry/dead-letter controls for admins,
- billing subscription, plan entitlement, checkout, and portal actions when billing is enabled,
- recent usage event summaries and current quota windows through
/api/usage/summary, - redacted support diagnostics through
/api/diagnosticsfor org admins and admin-scoped API tokens.
Role and policy decisions are enforced by the cloud APIs. The dashboard mirrors those decisions by disabling admin-only actions for member users, but disabled controls are only an ergonomic layer, not the authorization boundary. Token plaintext is shown only in the create response, and raw provider keys are never returned by read APIs.
The diagnostics bundle is intentionally a support artifact, not a secret export. It includes runtime role, billing mode, BYOK provider metadata, quota summaries, recent sampled gateway counters, and recent usage totals, but excludes raw provider keys, KMS refs, OAuth/API tokens, signed URLs, channel credentials, cookies, and local host paths.
Self-hosted deployments can run with the stub/no billing adapter. In that mode the dashboard keeps the billing panel non-blocking while BYOK, desktop token, gateway token, and usage surfaces remain available.
Downstream deployments can brand the public cloud surface through cloud.publicBranding or the Helm cloud.branding block. The same structure controls the dashboard product name, logo URL, theme tokens, support/privacy/ security/legal links, dashboard copy, and managed-org connection labels. The cloud API also returns this metadata from GET /api/config so desktop and gateway clients can show the same deployer identity.
Generic Docker: Cloud + Gateway¶
Use the combined self-host reference when validating the browser dashboard, desktop token flow, and headless gateway together:
That file starts:
- Open Cowork Cloud in
all-in-onemode on http://localhost:8787, - Postgres for the control plane,
- MinIO for artifacts and checkpoints,
- Open Cowork Gateway on http://localhost:8790.
The gateway requires at least one real provider by default. Telegram, Slack, email, and generic webhook providers can run together in one gateway process, or you can deploy one gateway per channel binding for tighter blast-radius control. For local-only smoke tests without real channel credentials, opt into the fake provider explicitly:
OPEN_COWORK_GATEWAY_ENABLE_FAKE_PROVIDER=true \
docker compose -f docker-compose.cloud-gateway.yml up --build
Do not expose the fake provider publicly. Real public deployments should configure Telegram, Slack, email, or a signed webhook provider instead. For a real self-hosted gateway:
- Open the dashboard at http://localhost:8787.
- Configure BYOK provider credentials if your profile requires them.
- Create a scoped gateway API token from the dashboard.
- Create a headless agent and channel binding.
- Restart the gateway with
OPEN_COWORK_GATEWAY_SERVICE_TOKENset to that one-time token and provider credentials such asOPEN_COWORK_GATEWAY_TELEGRAM_BOT_TOKEN,OPEN_COWORK_GATEWAY_SLACK_BOT_TOKEN, orOPEN_COWORK_GATEWAY_EMAIL_INBOUND_SECRET. - Set
OPEN_COWORK_GATEWAY_PRODUCT_MODE=cloud_channelfor clarity. The current daemon fails closed forstandaloneandhybridproduct modes. - Set
OPEN_COWORK_GATEWAY_ADMIN_TOKENfor metrics, diagnostics, readiness provider inventory, and delivery backlog controls. Only local loopback demos should useOPEN_COWORK_GATEWAY_ALLOW_LOOPBACK_OPERATOR_BYPASS=true. Compose references require this value explicitly and the gateway rejects placeholder tokens such aschange-me-*orreplace-with-*.
Provider requirements:
- Telegram can run polling for a private VPS or webhook mode when
OPEN_COWORK_GATEWAY_PUBLIC_URLis reachable by Telegram. - Slack requires
OPEN_COWORK_GATEWAY_SLACK_BOT_TOKENandOPEN_COWORK_GATEWAY_SLACK_SIGNING_SECRET; incoming requests are rejected unless Slack's timestamp/signature check passes. - Email requires an inbound webhook shared secret plus SMTP settings. Email preserves
In-Reply-To/Referencesthread IDs and uses command-token approval fallback rather than inline buttons. - Webhook requires a shared secret for public ingress and outbound bridge signing. Outbound bridge delivery is signed with timestamped HMAC headers by default; receivers should verify the raw body and reject stale timestamps.
For local demos the cloud compose file uses auth.mode=none and explicit insecure overrides. Public deployments must use OIDC or a trusted identity proxy for the cloud web role and a real gateway service token.
Gateway health endpoints:
GET /healthreports process liveness.GET /readyreports provider readiness.GET /metricsexposes Prometheus metrics when enabled. Public binds requireOPEN_COWORK_GATEWAY_ADMIN_TOKEN; local loopback bypass must be explicit and only applies when the bind host, socket peer, andHostheader are loopback with no forwarded headers.GET /diagnosticsreturns redacted gateway config, provider state, and counters for support. It requires the same admin token unless explicit local loopback bypass is enabled.GET /deliverieslists recent delivery backlog rows for operators.POST /deliveries/:id/retryschedules a failed/dead delivery for retry.POST /deliveries/:id/dead-lettermarks a poison delivery as dead. These delivery controls require the same gateway admin token.
The gateway image is separate from the cloud image because it owns channel secrets, long-polling or webhook connections, and a different scaling profile. It does not import the OpenCode SDK and does not own control-plane Postgres state. Gateway stream state is process-local today, so run one replica per channel-binding shard. For production, deploy one Gateway release per shard with replicaCount: 1. The Helm chart fails closed for replicaCount > 1 unless gateway.experimentalDistributedOwnership=true is set explicitly for an experimental deployment; that flag is a lab escape hatch, not a production distributed-ownership implementation.
The provider-neutral Helm chart remains useful for web, scheduler, Gateway, backing-service, and policy composition, but it is not currently a complete public-production execution topology. The stock Cloud image and chart do not provide a Docker daemon/CLI or inject a Kubernetes execution provider, and the pod security profile intentionally prevents a nested Docker boundary. A stock Helm worker therefore fails isolation readiness and must not receive execution traffic. Run workers on dedicated Docker-capable hosts that pass proof:cloud:tenant-isolation, or provide a reviewed downstream external provider before treating a Kubernetes deployment as production-capable.
Even a web/scheduler-only Helm release must pin the reviewed Cloud image while leaving the non-ready stock worker disabled:
helm upgrade --install open-cowork-cloud helm/open-cowork-cloud \
--set image.repository=ghcr.io/joe-broadhead/open-cowork-cloud \
--set image.digest=sha256:REPLACE_WITH_CLOUD_DIGEST \
--set roles.worker.enabled=false
Use cloud.existingSecret in production so database URLs, object-store credentials, and envelope keys come from your platform secret manager rather than from Helm values. Public production chart installs reject inline secret-bearing values such as cloud.controlPlaneUrl, cookie secrets, header auth secrets, object-store credentials, and OIDC client secrets. The external secret must provide the runtime environment keys the chart reads, such as OPEN_COWORK_CLOUD_CONTROL_PLANE_URL, OPEN_COWORK_CLOUD_SECRET_KEY_REF, OPEN_COWORK_CLOUD_COOKIE_SECRET_REF, and object-store credential keys. Use managed refs such as azure-kv://prod-vault/secrets/open-cowork-secret-key/current for envelope, cookie, header, and OIDC client secrets where the runtime supports them.
Install the gateway chart next to cloud when you want channel access:
helm upgrade --install open-cowork-gateway helm/open-cowork-gateway \
--set image.repository=ghcr.io/joe-broadhead/open-cowork-gateway \
--set gateway.cloudBaseUrl='https://cowork.example.com' \
--set gateway.existingSecret=open-cowork-gateway-secrets
For production, set gateway.existingSecret and inject OPEN_COWORK_GATEWAY_SERVICE_TOKEN, OPEN_COWORK_GATEWAY_ADMIN_TOKEN, OPEN_COWORK_GATEWAY_PROVIDERS, and provider-specific credentials through External Secrets, sealed secrets, or the platform secret manager. The cloud chart also exposes the gateway as an optional dependency under gateway.enabled=true for installations that prefer one parent release.
The chart fails closed when cloud.auth.mode=none is used without cloud.allowInsecureAuth=true. Keep that override for local demos only; use oidc or a trusted header identity proxy for shared clusters. Public header auth deployments must also set OPEN_COWORK_CLOUD_HEADER_AUTH_SECRET or OPEN_COWORK_CLOUD_HEADER_AUTH_SECRET_REF; the proxy must inject that value and sign the identity headers with x-open-cowork-header-auth-timestamp and x-open-cowork-header-auth-signature. Unsigned header auth is local/demo only. Public web deployments, including trusted-header deployments, must set OPEN_COWORK_CLOUD_PUBLIC_URL to the canonical HTTPS origin so redirects, cookies, and proxy handling never depend on forwarded headers from untrusted callers. The gateway chart also fails closed unless at least one provider is configured through gateway.providersJson, Telegram, Slack, email, generic webhook settings, or an existing secret. It also fails closed when replicaCount > 1 without gateway.experimentalDistributedOwnership=true, when operator auth is missing, when public URLs are not HTTPS, when public deployments use inline secret-bearing Helm values without gateway.existingSecret, when loopback operator bypass is combined with public exposure, when placeholder secrets are still present, or when generic webhook ingress is configured without a shared secret.
Kubernetes public production deployments force NetworkPolicy egress isolation. For Cloud, cloud.deploymentTier=public_production renders an Egress policy even when networkPolicy.egress.enabled=false; for Gateway, public exposure does the same. Public Cloud and public Gateway renders also fail when networkPolicy.ingress.from is empty, because Kubernetes treats an ingress rule without from as all sources. Production overlays should name the ingress controller or private caller explicitly:
networkPolicy:
ingress:
allowAllSourcesForLocalOnly: false
from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
For private/internal topologies, use the namespace and pod labels for the Gateway release, service mesh, or internal workload that is allowed to call the service. Cloud L7 controllers without in-cluster pods should use provider-approved ipBlock peers in a private overlay. An empty egress allowlist renders egress: []. Add one networkPolicy.egress.allow[] entry per approved dependency, with explicit to peers and ports, when the cluster needs outbound access to Cloud, object-store, identity, provider API, or telemetry endpoints.
The Helm chart still models an ephemeral worker runtime root and checkpoint storage for downstream provider integrations. Those storage settings do not establish process, user, mount, or network isolation by themselves. Do not use the stock Helm worker for execution until a downstream provider is injected and its boundary, restore, eviction, crash, and teardown behavior passes the same release proof. A single-worker PVC can be enabled for controlled non-execution pilots, but it is not an isolation boundary or a horizontal scaling model.
For workers that write runtime/workspace checkpoints, provide OPEN_COWORK_CLOUD_SECRET_KEY_REF from a provider secret manager in hosted production. OPEN_COWORK_CLOUD_SECRET_KEY is accepted for local/self-host pilots but production startup rejects weak or demo key material. The ref form can point at env:NAME, GCP Secret Manager (gcp-sm://projects/{project}/secrets/{secret}/versions/{version}), AWS Secrets Manager (aws-sm://{secret-id}?region={region}), or Azure Key Vault (azure-kv://{vault}/secrets/{secret}/{version}).
Production Readiness¶
Before exposing cloud or gateway publicly, use the deployment checklist in deployment-readiness.md. It covers auth, cookie secrets, Postgres, object storage, secret adapter/KMS references, public HTTPS origins, worker/scheduler scaling, gateway service tokens, provider webhook signing, quotas/rate limits, OTLP/logging, backups, and restore.
Session revocation boundary (known-accepted at the current claim tier). Cloud web sessions use stateless signed cookies with expiry and CSRF tokens; there is no server-side revocation list, so logout and membership removal do not invalidate an already-issued cookie before it expires. The mitigation is that every request re-checks tenant membership, so a removed user loses data access immediately even with a live cookie. This is acceptable for local-self-host-beta. Server-side session revocation is a precondition for any hosted or multi-tenant claim tier and must land before those claims are made.
Managed BYOK operators should also follow runbooks/managed-byok-saas.md. That runbook covers org signup mode, token TTLs, invite/domain controls, billing setup, BYOK validation, gateway operations, and incident response. Managed worker operators should use deploy/managed-workers/ for worker pool environment templates, Helm overlays, release evidence, and restore drill templates.
Run deployment manifest validation before rollout:
Smoke a running deployment after traffic is routed:
OPEN_COWORK_SMOKE_CLOUD_URL=https://cowork.example.com \
OPEN_COWORK_SMOKE_GATEWAY_URL=https://gateway.example.com \
pnpm deploy:smoke
Validate the Desktop cloud-workspace path against that deployment before calling desktop sync production-ready:
OPEN_COWORK_DESKTOP_SMOKE_CLOUD_URL=https://cowork.example.com \
OPEN_COWORK_DESKTOP_SMOKE_ADMIN_TOKEN=... \
pnpm deploy:desktop:smoke
The Desktop smoke uses the Electron main-process cloud adapter/cache code, issues a short-lived Desktop token when an admin token is provided, validates Desktop OIDC metadata when configured, exercises bearer-auth HTTP/SSE, checks Desktop-created and Web-created session continuation, verifies prompt/abort routing, confirms read-only offline cache behavior, proves the local workspace remains independent, and revokes the ephemeral token.
Validate the Gateway deployment path before calling channel access production-ready:
OPEN_COWORK_GATEWAY_SMOKE_CLOUD_URL=https://cowork.example.com \
OPEN_COWORK_GATEWAY_SMOKE_GATEWAY_URL=https://gateway.example.com \
OPEN_COWORK_GATEWAY_SMOKE_ADMIN_TOKEN=... \
OPEN_COWORK_GATEWAY_SMOKE_GATEWAY_ADMIN_TOKEN=... \
pnpm deploy:gateway:smoke
The Gateway smoke checks managed Gateway health/readiness and public operator endpoint protection, then runs a loopback self-host Gateway process with a fake provider against the deployed Cloud control plane. It issues and revokes a short-lived gateway-scoped service token, creates temporary channel state, proves least privilege, sends an inbound channel prompt, waits for session SSE rendering, routes an approval interaction, drains an async delivery, and exercises retry/dead-letter controls.
Run the continuation parity smoke after Cloud Web, Desktop sync, and Gateway smokes pass:
OPEN_COWORK_CONTINUATION_SMOKE_CLOUD_URL=https://cowork.example.com \
OPEN_COWORK_CONTINUATION_SMOKE_ADMIN_TOKEN=... \
OPEN_COWORK_CONTINUATION_SMOKE_REQUIRE_RICH_PROJECTION=true \
pnpm deploy:continuation:smoke
This #498 gate creates one deployment-level proof for the three-surface product promise: Cloud Web API, Desktop cloud workspace adapter, and Gateway channel adapter all continue the same tenant-scoped cloud sessions through durable commands, events, and projections. It also validates request-id correlation, permission/question resolution, artifact metadata, concurrent prompt ordering, stale cursor hydration, Gateway rendering, and smoke-token revocation.
Validation¶
The regular pnpm test suite covers the in-memory control-plane adapter and skips provider infrastructure. Before shipping a scalable cloud release, run the real Postgres concurrency gate against an isolated database:
Run the cloud continuation harness whenever touching desktop sync, cloud session projection, or gateway channel APIs:
That harness starts an in-process cloud, authenticates desktop, web, and gateway clients with bearer tokens, verifies SSE replay, proves approval and question resolution across all surfaces, and checks restart/offline desktop hydration from durable cloud projections.
Run pnpm deploy:continuation:smoke against local Compose or a deployed cloud environment before calling Web/Desktop/Gateway sync production-ready.
That gate proves Postgres row-lock behavior for worker leases, ordered event sequence writes, session command idempotency/reclaim, scheduler claims, and webhook replay claims. It also covers quota counter concurrency for prompt windows, concurrent session caps, and active worker caps when OPEN_COWORK_TEST_POSTGRES_URL is set.
CI runs the same cloud gates in the cloud-gates job: OpenCode portability proof, real Postgres concurrency tests, Compose config validation, cloud OCI image build, Cloud Web Workbench readiness gates, split-role Compose workbench/API smoke, and Helm lint/render validation.
Configuration¶
Set these environment variables in every role:
| Variable | Meaning |
|---|---|
OPEN_COWORK_CLOUD_ROLE | all-in-one, web, worker, or scheduler. |
OPEN_COWORK_CLOUD_PROFILE | Deployment profile such as full, focused-agent, or custom. |
OPEN_COWORK_CLOUD_CONTROL_PLANE_URL | Postgres connection URL for durable cloud state. |
OPEN_COWORK_CLOUD_PG_POOL_MAX | Max Postgres pool connections (default 10). The control plane saturates here first under load — size against your Postgres max_connections and replica count. |
OPEN_COWORK_CLOUD_PG_STATEMENT_TIMEOUT_MS | Per-statement timeout (default 30000); a non-zero default bounds an unbounded read from pinning a connection. DDL/migrations are exempt. |
OPEN_COWORK_CLOUD_PG_IDLE_TX_TIMEOUT_MS | idle_in_transaction_session_timeout (default 120000) so an abandoned transaction can't hold a connection forever. |
OPEN_COWORK_CLOUD_PG_CONNECTION_TIMEOUT_MS / OPEN_COWORK_CLOUD_PG_IDLE_TIMEOUT_MS | Pool connect timeout and idle-connection eviction window. |
OPEN_COWORK_CLOUD_PG_APP_NAME | application_name set on each connection for Postgres-side observability. |
OPEN_COWORK_CLOUD_OBJECT_STORE_KIND | filesystem, minio, s3, gcs, azure-blob, or digitalocean-spaces. |
OPEN_COWORK_CLOUD_OBJECT_STORE_BUCKET | Bucket/container name for artifacts and snapshots. |
OPEN_COWORK_CLOUD_OBJECT_STORE_ENDPOINT | Custom endpoint URL (MinIO / S3-compatible / DigitalOcean Spaces). |
OPEN_COWORK_CLOUD_OBJECT_STORE_REGION | Region for the S3/Spaces backend. |
OPEN_COWORK_CLOUD_OBJECT_STORE_PREFIX | Optional key prefix namespacing all objects. |
OPEN_COWORK_CLOUD_OBJECT_STORE_ACCESS_KEY_ID / OPEN_COWORK_CLOUD_OBJECT_STORE_SECRET_ACCESS_KEY / OPEN_COWORK_CLOUD_OBJECT_STORE_SESSION_TOKEN | S3/MinIO/Spaces static credentials (session token optional for STS). |
OPEN_COWORK_CLOUD_ARTIFACT_DIRECT_UPLOAD_MODE | Direct browser-to-store artifact upload gate. Defaults to off; enabled is accepted only when readiness proves an attested exact-size AWS S3 POST policy, durable lifecycle storage, and a live cleanup owner. The AWS credentials require s3:GetBucketVersioning and s3:GetBucketCORS. The bucket must never have had versioning enabled, must use a non-dotted virtual-host name, and its CORS rules must explicitly allow POST from the exact configured browser origin; path-style addressing, dotted bucket names, suspended versioning, wildcard/pattern origins, and missing CORS fail readiness closed. Leave the gate off to use bounded buffered upload with those providers. Provider attestations are cached only after success for 30 seconds; the scheduler's current cleanup-owner/provider lease is the authoritative cross-process gate around the local signer. Other providers remain on the bounded buffered path. |
OPEN_COWORK_CLOUD_ARTIFACT_DIRECT_UPLOAD_CLEANUP_BATCH_SIZE | Maximum reservations claimed by one reconciliation pass (default and cap 100). |
OPEN_COWORK_CLOUD_ARTIFACT_DIRECT_UPLOAD_CLEANUP_INTERVAL_MS | Interval between bounded abandoned-staging cleanup passes (default 60000). Startup runs the same reconciler before direct upload can become ready; each pass also prunes at most one batch of terminal reservation rows older than seven days. |
OPEN_COWORK_CLOUD_OBJECT_STORE_ACCOUNT_NAME / OPEN_COWORK_CLOUD_OBJECT_STORE_SAS_TOKEN | Azure Blob account name + SAS token. |
OPEN_COWORK_CLOUD_OBJECT_STORE_BEARER_TOKEN | GCS OAuth bearer token (when not using ambient ADC). |
OPEN_COWORK_CLOUD_SECRET_KEY | Envelope key for local/dev encrypted secret storage. |
OPEN_COWORK_CLOUD_SECRET_KEY_REF | Optional cloud secret-manager ref for the envelope key when the key is not injected directly. |
OPEN_COWORK_CLOUD_SECRET_KEY_PREVIOUS / OPEN_COWORK_CLOUD_SECRET_KEY_PREVIOUS_REF | Comma-separated retired envelope keys (raw values or secret-manager refs) kept in the decrypt keyring for rotation. New writes always use the current key and stamp its key id; already-stored ciphertexts decrypt with the matching retired key. Re-encrypt and drop the old key once migration completes. |
OPEN_COWORK_CLOUD_COOKIE_SECRET | HMAC key for signed browser session cookies (minimum 32 bytes, same floor as envelope secret keys). Falls back to OPEN_COWORK_CLOUD_SECRET_KEY for local demos only — public production requires a distinct cookie secret. |
OPEN_COWORK_CLOUD_COOKIE_SECRET_REF | Optional env secret ref for the cookie signing key when it is managed outside chart values. |
OPEN_COWORK_CLOUD_COOKIE_SECURE | Defaults to true; local HTTP compose references set it to false. |
OPEN_COWORK_CLOUD_PUBLIC_URL | Canonical public origin used for OIDC callback redirects and workflow webhook URLs behind proxies or ingress. Must be set to the canonical https:// origin for any HTTPS-fronted deployment so HSTS is emitted (see Cloud advanced / tuning). |
OPEN_COWORK_CLOUD_PUBLISHED_ADDR | Docker compose host-side bind address for local/demo references. Defaults to 127.0.0.1; when OPEN_COWORK_CLOUD_ALLOW_INSECURE_AUTH=true, non-loopback values fail startup. |
OPEN_COWORK_CLOUD_PUBLISHED_PORT | Docker compose host-side Cloud port. Defaults to 8787; use a free loopback port when another local runtime already owns that port. For interactive or OIDC use, keep OPEN_COWORK_CLOUD_PUBLIC_URL aligned with the externally visible port; the smoke URL override does not change callbacks or public-origin policy. |
OPEN_COWORK_CLOUD_PUBLIC_BRANDING_JSON | JSON object matching cloud.publicBranding; Helm renders this from cloud.branding. |
OPEN_COWORK_CLOUD_BRAND_NAME / OPEN_COWORK_CLOUD_BRAND_SHORT_NAME | Simple env overrides for the dashboard product name and short mark. |
OPEN_COWORK_CLOUD_BRAND_LOGO_URL | HTTPS logo URL for the browser dashboard. |
OPEN_COWORK_CLOUD_SUPPORT_URL / OPEN_COWORK_CLOUD_PRIVACY_URL / OPEN_COWORK_CLOUD_SECURITY_URL / OPEN_COWORK_CLOUD_LEGAL_URL | Optional public footer links. |
OPEN_COWORK_CLOUD_AUTH_MODE | none for loopback/local demos, header for a trusted identity proxy, or oidc for public browser/JWT auth. |
OPEN_COWORK_CLOUD_ALLOW_INSECURE_AUTH | Explicit local/demo override for unauthenticated Cloud demos. It only starts when the published address and public URL are loopback-local; use oidc or header auth before exposing Cloud beyond localhost. |
OPEN_COWORK_CLOUD_OIDC_ISSUER_URL | HTTPS OIDC issuer used for discovery and JWT verification. |
OPEN_COWORK_CLOUD_OIDC_CLIENT_ID | OIDC audience/client id expected in browser login and bearer tokens. |
OPEN_COWORK_CLOUD_OIDC_CLIENT_SECRET | Optional OIDC confidential-client secret; config clientSecretRef can point at a platform secret env var instead. |
OPEN_COWORK_CLOUD_OIDC_CLIENT_SECRET_REF | Optional env secret ref for the OIDC client secret. |
OPEN_COWORK_CLOUD_INTERNAL_TOKEN | Internal-only token for operational endpoints such as scheduler tick probes. Keep it in a platform secret store. |
OPEN_COWORK_CLOUD_INTERNAL_TOKEN_REF | Optional env secret ref for the internal operational token. |
OPEN_COWORK_CLOUD_OIDC_CALLBACK_PATH | OIDC callback path; defaults to /auth/callback. |
OPEN_COWORK_CLOUD_SIGNUP_MODE | Optional explicit org signup mode: disabled, invite, domain, or open. invite permits admin-created invited memberships; domain uses OPEN_COWORK_CLOUD_ALLOWED_EMAIL_DOMAINS; disabled allows only existing active memberships. Invalid values fail startup validation. |
OPEN_COWORK_CLOUD_ALLOWED_EMAIL_DOMAINS | Optional comma-separated email domain allowlist for OIDC identities. |
OPEN_COWORK_CLOUD_ORG_MODE | Deployment topology: multi-org (default) preserves multi-tenant behaviour; single-org funnels every principal into one auto-bootstrapped org and skips tenant switching, for single-tenant self-host installs. |
OPEN_COWORK_CLOUD_SINGLE_ORG_ID | Org/tenant id used as the single org when OPEN_COWORK_CLOUD_ORG_MODE=single-org (default default). Ignored in multi-org mode. |
OPEN_COWORK_CLOUD_SINGLE_ORG_NAME | Display name for the single org in single-org mode (default Default Organization). Ignored in multi-org mode. |
OPEN_COWORK_CLOUD_HEADER_AUTH_SECRET / OPEN_COWORK_CLOUD_HEADER_AUTH_SECRET_REF | Required for public header auth; trusted proxies must provide the same value in x-open-cowork-header-auth-secret. |
OPEN_COWORK_CLOUD_HEADER_AUTH_ALLOW_UNSIGNED | Local/demo escape hatch only. Public and public_production trusted-header deployments require signed headers. |
OPEN_COWORK_CLOUD_HEADER_AUTH_MAX_SIGNATURE_AGE_MS | Maximum accepted trusted-header signature age. Defaults to five minutes. |
OPEN_COWORK_CLOUD_ALLOW_SELF_SERVICE_SIGNUP | Explicitly allows first-login OIDC org membership creation. Keep disabled for invite-only managed deployments. |
OPEN_COWORK_CLOUD_API_TOKEN_DEFAULT_TTL_MS / OPEN_COWORK_CLOUD_API_TOKEN_MAX_TTL_MS | Default and maximum TTLs for desktop/gateway API tokens. Defaults are 90 days and 365 days. |
OPEN_COWORK_CLOUD_API_TOKEN_ALLOWED_SCOPES | Comma-separated API token scopes operators allow admins to mint. Defaults to desktop,gateway,admin,operator; worker-internal is reserved for managed worker credential flows. |
OPEN_COWORK_CLOUD_TRUST_PROXY_HEADERS | Allows Forwarded/x-forwarded-for for rate-limit attribution only when the request socket is in OPEN_COWORK_CLOUD_TRUSTED_PROXY_CIDRS; if both headers are present they must resolve to the same client or the socket address is used. |
OPEN_COWORK_CLOUD_TRUSTED_PROXY_CIDRS | Comma-separated CIDR/address allowlist for trusted reverse proxies whose forwarded client headers may be used. Leave empty unless OPEN_COWORK_CLOUD_TRUST_PROXY_HEADERS=true. |
OPEN_COWORK_CLOUD_SERVICE_NAME | Service name included in structured logs and OTLP resource attributes. |
OPEN_COWORK_CLOUD_SERVICE_VERSION | Optional version string included in structured logs and OTLP resource attributes. |
OPEN_COWORK_CLOUD_LOG_FORMAT | json, pretty, or silent; defaults to JSON for cloud logs. |
OPEN_COWORK_CLOUD_OTLP_ENDPOINT | Optional OpenTelemetry OTLP HTTP base endpoint; exports traces to /v1/traces and metrics to /v1/metrics. |
OPEN_COWORK_CLOUD_OTLP_HEADERS | Optional JSON object of OTLP HTTP headers, stored as a secret when it contains collector credentials. |
OPEN_COWORK_CLOUD_OTLP_FLUSH_INTERVAL_MS | Optional non-negative OTLP export interval in milliseconds; defaults to 30000; 0 disables the timer and relies on explicit flush/close. |
OPEN_COWORK_CLOUD_OTLP_MAX_QUEUE_SIZE | Optional positive maximum queued spans and metrics per OTLP record type before oldest records are dropped and counted; defaults to 1000. |
OPEN_COWORK_CLOUD_CHECKPOINTS_ENABLED | true enables worker runtime/workspace checkpoints in object storage. |
Data-retention variables (scheduler/all-in-one roles). The scheduler runs a bounded, batched retention sweep no more than once per OPEN_COWORK_CLOUD_RETENTION_INTERVAL_MS. Every window is in milliseconds and deletes rows older than the window, oldest-first; a window of 0/unset disables that prune (the scheduler does nothing destructive until you opt in).
Public production (JOE-841 / JOE-835): OPEN_COWORK_CLOUD_DEPLOYMENT_TIER=public_production fails closed unless both OPEN_COWORK_CLOUD_RETENTION_SESSION_EVENT_MS and OPEN_COWORK_CLOUD_RETENTION_WORKSPACE_EVENT_MS are set to positive values. Local and self-host tiers may leave event retention off, but unbounded cloud_session_events / cloud_workspace_events growth will hurt SSE replay latency, backup size, and cost — set windows as soon as a tenant is non-demo.
Recommended production starting points (adjust to compliance policy):
| Table | Recommended window | Notes |
|---|---|---|
| Session events | 1209600000 (14 days) | Durable session projection still covers the trimmed window for UI state. |
| Workspace events | 1209600000 (14 days) | Keep aligned with session events (written 1:1). |
| Audit events | policy-driven | Leave off until legal/compliance approves prune. |
| Usage events | after export | Leave off until billing aggregation/export is complete. |
| Variable | Meaning |
|---|---|
OPEN_COWORK_CLOUD_RETENTION_INTERVAL_MS | Minimum gap between retention sweeps. Defaults to one hour. |
OPEN_COWORK_CLOUD_RETENTION_BATCH_SIZE / OPEN_COWORK_CLOUD_RETENTION_MAX_BATCHES | Rows deleted per batch and max batches per table per sweep. Defaults 500 / 20. |
OPEN_COWORK_CLOUD_RETENTION_CHANNEL_DELIVERY_MS | Prune terminal (sent/dead) channel deliveries older than this. Off by default. |
OPEN_COWORK_CLOUD_RETENTION_CHANNEL_INTERACTION_MS | Prune expired one-shot channel-interaction tokens older than this. Off by default. |
OPEN_COWORK_CLOUD_RETENTION_STALE_THROTTLE_MS | Prune stale rate-limit windows + expired auth-backoff rows. Pure bookkeeping that grows one row per client IP, so this defaults on at one hour; 0 disables. |
OPEN_COWORK_CLOUD_RETENTION_SESSION_EVENT_MS | Prune cloud_session_events (the durable SSE replay log) older than this. Off by default for local/self-host; required for public_production. Durable session projection still covers the trimmed window. |
OPEN_COWORK_CLOUD_RETENTION_AUDIT_EVENT_MS | Prune cloud_audit_events older than this. Off by default — enable only if your compliance/retention policy permits deleting the audit trail. |
OPEN_COWORK_CLOUD_RETENTION_USAGE_EVENT_MS | Prune cloud_usage_events older than this. Off by default — enable only after the usage data has been exported/aggregated for billing. |
OPEN_COWORK_CLOUD_RETENTION_WORKSPACE_EVENT_MS | Prune cloud_workspace_events older than this. Written 1:1 with cloud_session_events; required for public_production. Durable projection still covers the trimmed window. |
OPEN_COWORK_CLOUD_CONCURRENCY_RECONCILE_MS | Optional interval for the scheduler to recompute the concurrency gauges (cloud_concurrency_counters) from their source tables. Off by default — the gauges are drift-free for normal activity; enable as a belt-and-suspenders correction (e.g. one hour). |
Managed billing variables:
| Variable | Meaning |
|---|---|
OPEN_COWORK_CLOUD_BILLING_ENABLED | Enables provider-neutral billing enforcement. Leave false for self-host OSS deployments. |
OPEN_COWORK_CLOUD_BILLING_PROVIDER | none, stub, or stripe. Public hosted deployments should not use none. |
OPEN_COWORK_CLOUD_BILLING_DEFAULT_PLAN | Default plan key used for checkout and provider webhook mapping. |
OPEN_COWORK_CLOUD_STRIPE_API_KEY / OPEN_COWORK_CLOUD_STRIPE_API_KEY_REF | Stripe API credential or env secret ref for checkout/portal calls. |
OPEN_COWORK_CLOUD_STRIPE_WEBHOOK_SECRET / OPEN_COWORK_CLOUD_STRIPE_WEBHOOK_SECRET_REF | Stripe webhook signing secret. Required for Stripe webhook handling. |
OPEN_COWORK_CLOUD_STRIPE_PRICE_ID | Default Stripe price id for the default plan. |
OPEN_COWORK_CLOUD_STRIPE_SUCCESS_URL / OPEN_COWORK_CLOUD_STRIPE_CANCEL_URL / OPEN_COWORK_CLOUD_STRIPE_PORTAL_RETURN_URL | Public URLs used by Stripe hosted checkout and portal flows. |
Optional, pluggable monetization (entitlements engine)¶
Billing is optional and must never gate administration or reads. An EntitlementResolver is the single typed seam the app consults for feature access and quotas (canUse(feature), checkQuota(resource, amount), describeEntitlements(org)). Feature/quota code asks the resolver — it never calls a payment provider directly. The payment-provider sync boundary (checkout/portal/webhook) stays behind the separate billing adapter.
| Variable | Meaning |
|---|---|
OPEN_COWORK_CLOUD_ENTITLEMENTS_ENABLED | Global kill switch for entitlement gating. Default false. When false, no gating is applied regardless of provider — reads, writes, and admin all pass. Existing orgs are grandfathered because gating is off until an operator opts in; flip back to false to instantly disable all gating during a rollout. |
OPEN_COWORK_CLOUD_ENTITLEMENTS_PROVIDER | Which resolver backs feature/quota decisions: none (default) → the unlimited resolver (complete, ungated product, no Billing UI signal); stripe → the plan/subscription metadata resolver, which decides purely from stored plan tiers + cloud_billing_subscriptions state (no live provider calls) and returns structured 402 denials on gated writes; custom → a downstream-registered resolver (see below). |
Gating discipline (enforced and tested): entitlement checks may gate writes/creation only (session/prompt/worker/workflow/artifact/BYOK/channel create paths, via assertEntitled) — never reads, exports, deletes, or any admin/org/RBAC/policy/audit action. With a lapsed or free plan, reads/exports and every admin action still succeed while a gated create is denied; with provider: none (or the kill switch off) nothing is ever denied.
The read-only GET /api/billing/entitlements endpoint returns the current plan status (provider, gatingEnabled, billingEnabled, planKey, features, limits). The admin plane reads billingEnabled to decide whether to surface a Billing section; it is never gated and carries no secrets.
Custom adapter seam: a downstream fork wires its own resolver with a small module — implement EntitlementResolver (exported from @open-cowork/cloud-server), call registerEntitlementResolverProvider('custom', factory) at startup, then set OPEN_COWORK_CLOUD_ENTITLEMENTS_PROVIDER=custom. No entitlement plan/subscription migration is required: state persists in the existing cloud_billing_subscriptions store, and the kill switch defaults off so no existing org breaks.
BYOK provider keys are never supplied through environment variables. Users add them through /api/byok; records remain pending_validation until a provider validator passes, or an org admin uses the audited override endpoint with a redacted reason.
For OpenRouter BYOK, Cloud workers compose managed OpenCode config the same way as desktop: Cowork app provider id stays openrouter, but the OpenCode runtime provider id is or with @ai-sdk/openai-compatible, the public OpenRouter base URL, the revealed API key (options + auth.json), and an explicit models pin for the org default and featured catalog ids. Session turns reload that config from the session XDG tree for the full managed-server lifetime. See docs/configuration.md (OpenRouter special-case).
Credentialless OpenCode-native desktop providers, including GitHub Copilot, are not normal Cloud BYOK providers. The upstream managed BYOK path admits only provider descriptors with declared secret credential fields, so Copilot is blocked by default for cloud workers and gateway sessions unless a deployer adds an explicit cloud-safe profile and policy for it. Do not route Copilot OAuth or device-code tokens through gateway providers, renderer state, cache, logs, or BYOK payloads.
Gateway variables:
| Variable | Meaning |
|---|---|
OPEN_COWORK_CLOUD_BASE_URL | Cloud web base URL used by the gateway HTTP/SSE client. |
OPEN_COWORK_GATEWAY_SERVICE_TOKEN | Scoped cloud API token with gateway access. Store it as a secret. |
OPEN_COWORK_GATEWAY_ADMIN_TOKEN | Required for operator endpoints in shared/public deployments. Send as bearer auth or x-open-cowork-gateway-admin-token. |
OPEN_COWORK_GATEWAY_ALLOW_LOOPBACK_OPERATOR_BYPASS | Explicit local-only bypass for operator endpoints when bound to loopback. Runtime rejects public URLs, public/forwarded Host headers, proxy-forwarded requests, and public binds for this bypass. |
OPEN_COWORK_GATEWAY_ALLOW_INSECURE_HTTP | Allows non-loopback HTTP cloud URLs for local Docker networks only. |
OPEN_COWORK_GATEWAY_HOST / OPEN_COWORK_GATEWAY_PORT | Gateway HTTP bind address and port. |
OPEN_COWORK_GATEWAY_PUBLIC_URL | Public HTTPS gateway URL for channel webhook registration. Non-HTTPS and loopback values fail closed. |
OPEN_COWORK_GATEWAY_INSTANCE_ID | Optional stable gateway instance/shard id used as Cloud delivery claimant. Kubernetes sets it from the pod name. |
OPEN_COWORK_GATEWAY_MAX_REQUEST_BODY_BYTES | Maximum inbound webhook/body size. Bridge/email file limits default to this cap. |
OPEN_COWORK_GATEWAY_TRUST_PROXY_HEADERS | Allows Forwarded/x-forwarded-for for webhook abuse controls only when the request socket is in OPEN_COWORK_GATEWAY_TRUSTED_PROXY_CIDRS; if both headers are present they must resolve to the same client or the socket address is used. |
OPEN_COWORK_GATEWAY_TRUSTED_PROXY_CIDRS | Comma-separated CIDR/address allowlist for reverse proxies whose forwarded client headers may be used. Leave empty unless OPEN_COWORK_GATEWAY_TRUST_PROXY_HEADERS=true. |
OPEN_COWORK_GATEWAY_CLOUD_REQUEST_TIMEOUT_MS | Deadline for cloud HTTP API calls made by the gateway. |
OPEN_COWORK_GATEWAY_WEBHOOK_DELIVERY_TIMEOUT_MS | Deadline for outbound bridge/webhook delivery and Slack API calls. |
OPEN_COWORK_GATEWAY_SMTP_TIMEOUT_MS | Deadline for SMTP connection/read/write operations. |
OPEN_COWORK_GATEWAY_SHUTDOWN_DRAIN_TIMEOUT_MS | Maximum time to drain in-flight deliveries before provider shutdown. |
OPEN_COWORK_GATEWAY_PRODUCT_MODE | cloud_channel for the current Cloud Channel Gateway daemon. standalone and hybrid are separate/future product modes and fail closed here. |
OPEN_COWORK_GATEWAY_MODE | self-host or managed; affects diagnostics and deployment labeling. |
OPEN_COWORK_GATEWAY_PUBLIC_BRANDING_JSON | JSON object matching the public branding contract; Helm renders this from gateway.branding. |
OPEN_COWORK_GATEWAY_BRAND_NAME / OPEN_COWORK_GATEWAY_BRAND_SHORT_NAME | Simple env overrides for gateway health/readiness and setup metadata. |
OPEN_COWORK_GATEWAY_SUPPORT_URL / OPEN_COWORK_GATEWAY_PRIVACY_URL / OPEN_COWORK_GATEWAY_SECURITY_URL / OPEN_COWORK_GATEWAY_LEGAL_URL | Optional public links returned by gateway health/readiness metadata. |
OPEN_COWORK_GATEWAY_METRICS_ENABLED | Enables /metrics; public binds require OPEN_COWORK_GATEWAY_ADMIN_TOKEN. |
OPEN_COWORK_GATEWAY_DIAGNOSTICS_ENABLED | Enables /diagnostics; public binds require OPEN_COWORK_GATEWAY_ADMIN_TOKEN. |
OPEN_COWORK_GATEWAY_ENABLE_FAKE_PROVIDER | Enables the fake local/demo provider when no real provider is configured. Do not expose it publicly. |
OPEN_COWORK_GATEWAY_PROVIDERS | JSON provider array for multi-provider deployments. Treat as secret when it carries credentials. |
OPEN_COWORK_GATEWAY_TELEGRAM_BOT_TOKEN | Telegram bot token for the Telegram provider. |
OPEN_COWORK_GATEWAY_TELEGRAM_WEBHOOK_SECRET | Telegram webhook secret token when running webhook mode. |
OPEN_COWORK_GATEWAY_SLACK_BOT_TOKEN | Slack bot token for Slack channel access. |
OPEN_COWORK_GATEWAY_SLACK_SIGNING_SECRET | Required Slack signing secret for webhook verification. |
OPEN_COWORK_GATEWAY_SLACK_CHANNEL_BINDING_ID / OPEN_COWORK_GATEWAY_SLACK_TEAM_ID | Cloud channel binding id and Slack team/workspace id. |
OPEN_COWORK_GATEWAY_EMAIL_INBOUND_SECRET | Required shared secret for inbound email webhook delivery. |
OPEN_COWORK_GATEWAY_EMAIL_MAX_ATTACHMENT_BYTES | Optional email attachment cap; defaults to OPEN_COWORK_GATEWAY_MAX_REQUEST_BODY_BYTES. |
OPEN_COWORK_GATEWAY_EMAIL_FROM / OPEN_COWORK_GATEWAY_EMAIL_ADDRESS | Outbound sender and inbound address shown in channel binding setup. |
OPEN_COWORK_GATEWAY_EMAIL_SMTP_HOST / OPEN_COWORK_GATEWAY_EMAIL_SMTP_PORT / OPEN_COWORK_GATEWAY_EMAIL_SMTP_SECURE | SMTP transport settings for email replies. |
OPEN_COWORK_GATEWAY_EMAIL_SMTP_USERNAME / OPEN_COWORK_GATEWAY_EMAIL_SMTP_PASSWORD | Optional SMTP auth credentials. |
OPEN_COWORK_GATEWAY_WEBHOOK_DELIVERY_URL | Outbound URL for the generic webhook provider. |
OPEN_COWORK_GATEWAY_WEBHOOK_MAX_ATTACHMENT_BYTES | Optional generic webhook attachment cap; defaults to OPEN_COWORK_GATEWAY_MAX_REQUEST_BODY_BYTES. |
OPEN_COWORK_GATEWAY_WEBHOOK_SHARED_SECRET | Required shared secret for generic webhook ingress HMAC signatures and outbound bridge authentication. Inbound and outbound generic webhook requests include x-open-cowork-gateway-webhook-timestamp and x-open-cowork-gateway-webhook-signature over the raw body. |
OPEN_COWORK_GATEWAY_WEBHOOK_CHANNEL_BINDING_ID | Cloud channel binding id mapped to the generic webhook provider (default webhook). |
OPEN_COWORK_GATEWAY_WEBHOOK_DELIVERY_ALLOWED_HOSTS | Comma-separated host allowlist for outbound webhook/bridge delivery URLs. Constrains SSRF blast radius; leave empty to allow any public host. |
OPEN_COWORK_GATEWAY_WEBHOOK_ALLOW_PRIVATE_DELIVERY | Allows outbound webhook/bridge delivery to private/loopback addresses. Off by default (SSRF guard); enable only for trusted internal bridges. |
OPEN_COWORK_GATEWAY_TELEGRAM_CHANNEL_BINDING_ID | Cloud channel binding id mapped to the Telegram provider (default telegram). |
OPEN_COWORK_GATEWAY_TELEGRAM_MODE | polling (private VPS) or webhook (when OPEN_COWORK_GATEWAY_TELEGRAM_PUBLIC_URL is reachable by Telegram). |
OPEN_COWORK_GATEWAY_TELEGRAM_PUBLIC_URL | Public HTTPS base URL Telegram calls in webhook mode; falls back to OPEN_COWORK_GATEWAY_PUBLIC_URL. |
OPEN_COWORK_GATEWAY_TELEGRAM_RESPOND_IN_GROUPS | Whether the Telegram bot responds to non-mention group messages. |
OPEN_COWORK_GATEWAY_SLACK_DEFAULT_CHANNEL_ID | Optional default Slack channel id for outbound posts. |
OPEN_COWORK_GATEWAY_SLACK_API_BASE_URL | Optional Slack API base URL override (testing / Slack-compatible gateways). |
OPEN_COWORK_GATEWAY_EMAIL_CHANNEL_BINDING_ID | Cloud channel binding id mapped to the email provider (default email). |
OPEN_COWORK_GATEWAY_EMAIL_DOMAIN | Inbound email domain advertised in channel binding setup. |
OPEN_COWORK_GATEWAY_LOG_LEVEL | Gateway log level: debug, info, warn, error, or silent (default info). |
OPEN_COWORK_GATEWAY_BRAND_LOGO_URL | HTTPS logo URL returned in gateway health/readiness and setup metadata. |
OPEN_COWORK_GATEWAY_MAX_DELIVERY_CONCURRENCY | Maximum cloud→channel deliveries dispatched at once (default 8, clamped 1–256). Caps outbound fan-out so a backlog drain cannot storm providers into rate limits. |
OPEN_COWORK_GATEWAY_MAX_DELIVERY_QUEUE_DEPTH | Hard cap on locally queued deliveries (default 512, clamped 16–100000). Beyond it deliveries are shed back to the cloud to re-serve instead of growing the heap. |
OPEN_COWORK_GATEWAY_ALLOW_PUBLIC_FAKE_PROVIDER | Local/demo escape hatch that permits the fake provider on a public bind. Keep off; real public deployments must use a real provider. |
OPEN_COWORK_GATEWAY_FAKE_CHANNEL_BINDING_ID / OPEN_COWORK_GATEWAY_FAKE_WORKSPACE_ID | Channel binding id and external workspace id for the local/demo fake provider. |
OPEN_COWORK_GATEWAY_CLI_ENABLED | Enables the local CLI channel provider for development/testing. |
OPEN_COWORK_GATEWAY_CLI_CHANNEL_BINDING_ID / OPEN_COWORK_GATEWAY_CLI_WORKSPACE_ID | Channel binding id and external workspace id for the CLI provider. |
Bridge providers (discord / whatsapp / signal)¶
Discord, WhatsApp, and Signal are outbound-bridge providers: Open Cowork signs and POSTs messages to an external bridge process that owns the platform connection, and the bridge POSTs inbound messages back through the signed generic-webhook ingress path. Each provider is configured from a parallel env family (substitute DISCORD, WHATSAPP, or SIGNAL for <KIND>). A provider is only created when its _DELIVERY_URL is set, and it then requires a _SHARED_SECRET.
| Variable | Meaning |
|---|---|
OPEN_COWORK_GATEWAY_DISCORD_DELIVERY_URL / OPEN_COWORK_GATEWAY_WHATSAPP_DELIVERY_URL / OPEN_COWORK_GATEWAY_SIGNAL_DELIVERY_URL | Outbound URL of the external bridge for that platform. Setting it enables the provider. |
OPEN_COWORK_GATEWAY_DISCORD_SHARED_SECRET / OPEN_COWORK_GATEWAY_WHATSAPP_SHARED_SECRET / OPEN_COWORK_GATEWAY_SIGNAL_SHARED_SECRET | Required HMAC shared secret for that bridge's signed inbound ingress and outbound delivery. |
OPEN_COWORK_GATEWAY_<KIND>_CHANNEL_BINDING_ID | Cloud channel binding id mapped to the bridge provider (defaults to the kind). |
OPEN_COWORK_GATEWAY_<KIND>_WORKSPACE_ID | Optional external workspace id for the bridge binding. |
OPEN_COWORK_GATEWAY_<KIND>_DELIVERY_ALLOWED_HOSTS | Comma-separated host allowlist for outbound bridge delivery (SSRF guard). |
OPEN_COWORK_GATEWAY_<KIND>_ALLOW_PRIVATE_DELIVERY | Allows bridge delivery to private/loopback addresses. Off by default. |
OPEN_COWORK_GATEWAY_<KIND>_MAX_ATTACHMENT_BYTES | Optional attachment cap; defaults to OPEN_COWORK_GATEWAY_MAX_REQUEST_BODY_BYTES. |
Gateway config JSON from OPEN_COWORK_CONFIG_PATH, OPEN_COWORK_CONFIG_DIR, OPEN_COWORK_DOWNSTREAM_ROOT, OPEN_COWORK_GATEWAY_CONFIG, or OPEN_COWORK_GATEWAY_CONFIG_JSON is intentionally not trusted for the cloud endpoint, service token, or cloud-client timeout/insecure-HTTP policy. Set OPEN_COWORK_CLOUD_BASE_URL, OPEN_COWORK_GATEWAY_SERVICE_TOKEN, OPEN_COWORK_GATEWAY_CLOUD_REQUEST_TIMEOUT_MS, and OPEN_COWORK_GATEWAY_ALLOW_INSECURE_HTTP through env or your deployment secret manager instead.
Hosted/public deployments should keep abuse controls enabled. The defaults are conservative and can be tuned per deployment; set an individual numeric quota to 0 to disable that quota for self-hosted/private installs.
| Variable | Meaning |
|---|---|
OPEN_COWORK_CLOUD_ABUSE_ENABLED | Enables quota, rate-limit, usage, and auth-backoff enforcement. |
OPEN_COWORK_CLOUD_MAX_CONCURRENT_SESSIONS_PER_ORG | Maximum non-closed cloud sessions per org. |
OPEN_COWORK_CLOUD_MAX_ACTIVE_WORKERS_PER_ORG | Maximum active worker leases per org. |
OPEN_COWORK_CLOUD_MAX_PROMPTS_PER_HOUR | Per-org prompt enqueue quota. |
OPEN_COWORK_CLOUD_MAX_GATEWAY_PROMPTS_PER_HOUR | Per-org prompt quota for channel-gateway-originated prompts. |
OPEN_COWORK_CLOUD_MAX_WORKFLOW_RUNS_PER_HOUR | Per-org workflow-run start quota. |
OPEN_COWORK_CLOUD_MAX_CONCURRENT_WORKFLOW_RUNS_PER_ORG | Maximum simultaneously-running workflow runs per org. |
OPEN_COWORK_CLOUD_MAX_QUEUED_COMMANDS_PER_ORG | Maximum pending session commands queued per org. |
OPEN_COWORK_CLOUD_MAX_QUEUE_AGE_MS | Maximum age of the oldest pending command before new enqueues are rejected (backpressure). |
OPEN_COWORK_CLOUD_MAX_WORKER_MINUTES_PER_HOUR | Per-org worker-minute quota used to block new execution after hourly usage is exhausted. |
OPEN_COWORK_CLOUD_MAX_GATEWAY_DELIVERIES_PER_HOUR | Per-org gateway delivery claim quota. |
OPEN_COWORK_CLOUD_MAX_GATEWAY_CHANNEL_BINDINGS_PER_ORG | Maximum active gateway channel bindings per org. |
OPEN_COWORK_CLOUD_MAX_ARTIFACT_BYTES_PER_DAY | Per-org artifact upload byte quota. |
OPEN_COWORK_CLOUD_HTTP_RATE_LIMIT_ENABLED | Enables HTTP request rate limiting. |
OPEN_COWORK_CLOUD_HTTP_RATE_LIMIT_WINDOW_MS | HTTP rate-limit window. |
OPEN_COWORK_CLOUD_HTTP_RATE_LIMIT_MAX_REQUESTS | Maximum HTTP requests per IP, org, and API token per window. |
OPEN_COWORK_CLOUD_AUTH_BACKOFF_ENABLED | Enables backoff after repeated rejected authentication attempts. |
OPEN_COWORK_CLOUD_AUTH_BACKOFF_WINDOW_MS | Auth failure counting window. |
OPEN_COWORK_CLOUD_AUTH_BACKOFF_MAX_FAILURES | Failures before auth backoff starts. |
OPEN_COWORK_CLOUD_AUTH_BACKOFF_MS | Backoff duration returned through Retry-After. |
Quota and rate-limit failures return 429 with Retry-After, retryAfterMs, and a machine-readable policy code such as quota.prompts_per_hour_exceeded or rate_limit.http_exceeded. Billing or plan enforcement can layer separate 402 policy responses on top of this control-plane surface.
Role-specific knobs:
| Variable | Roles | Meaning |
|---|---|---|
OPEN_COWORK_CLOUD_HOST / OPEN_COWORK_CLOUD_PORT | web, all-in-one | HTTP bind address. |
OPEN_COWORK_CLOUD_AUTO_PROCESS_COMMANDS | all-in-one | Process queued commands inline for local/demo use. |
OPEN_COWORK_CLOUD_WORKER_ID | worker, all-in-one | Stable worker identity for leases and heartbeats. |
OPEN_COWORK_CLOUD_WORKER_POLL_MS | worker, all-in-one | Durable command polling interval. |
OPEN_COWORK_CLOUD_WORKER_SESSION_CONCURRENCY | worker, all-in-one | How many distinct sessions one worker tick processes concurrently (default 4, clamped 1–32). Sessions are independent, so this stops one long-running command from head-of-line-blocking other tenants on the worker; set to 1 for the previous strictly-serial behaviour. Each concurrent session can run its own runtime, so size it against the worker's CPU/memory and the per-org worker entitlement. |
OPEN_COWORK_CLOUD_WORKER_MAX_COMMANDS_PER_SESSION_PER_TICK | worker, all-in-one | How many commands a single session drains before yielding its lane back to the pool (default 50, clamped 1–10000). Bounds a session with a large backlog from monopolising a lane; the session is re-surveyed on the next pass while it still has pending commands. |
OPEN_COWORK_CLOUD_WORKER_MAX_LEASES | worker, all-in-one | Maximum worker lease records retained in the in-process LRU (default 4096). This is a memory budget, not an execution-capacity override. |
OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_MODE | worker, all-in-one | Runtime progress watchdog mode: off (default), observe, or enforce. Missing or invalid configuration resolves to off; observe emits decisions but never interrupts work. |
OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_SUSPECT_MS / OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_STALLED_MS / OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_SWEEP_MS | worker, all-in-one | Monotonic suspect, stalled, and single-loop sweep intervals. Defaults are 120000, 300000, and 5000; safe minima are 1000, 2000, and 250, and suspect must remain below stalled. Smaller or inverted values fail closed to off. Polls, health checks, duplicate events, and bare lease renewals do not reset either threshold. |
OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_MAX_ENTRIES / OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_MAX_SNAPSHOT_ENTRIES | worker, all-in-one | Hard bounds for in-memory tracked executions and privacy-safe /progressz samples. Snapshots contain only state, age, bounded source, mode, and generation—never session/run/tenant IDs, prompts, output, paths, or lease tokens. |
OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_OBSERVE_EVIDENCE_REF / OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_OPERATOR_OWNER / OPEN_COWORK_CLOUD_PROGRESS_WATCHDOG_ROLLBACK_MODE | worker, all-in-one | Mandatory enforcement gate: a canary observe-evidence reference, named operator owner, and rollback mode (off or observe). If any field is missing, enforce resolves to off. |
OPEN_COWORK_CLOUD_EXECUTION_ISOLATION_MODE | worker, all-in-one | sandbox, external-provider, or development-process. An omitted value always defaults to the fail-closed sandbox, including for local deployments. Unsafe shared-process execution therefore requires the explicit development-process value and is still rejected outside the local tier. external-provider requires an explicitly injected provider and does not impose Docker image settings, but its live capability and per-boundary attestations must satisfy the same verified process/user/mount/runtime-home/network/cleanup contract. A missing provider fails closed. |
OPEN_COWORK_CLOUD_ISOLATION_ENGINE | worker, all-in-one | Sandbox engine. The built-in long-lived Cloud provider currently supports docker; unknown engines and apple-container fail closed for Cloud admission. A host may inject a separately attested provider through the server API. |
OPEN_COWORK_CLOUD_ISOLATION_IMAGE | worker, all-in-one | Immutable OpenCode runtime image reference used for one execution boundary. Required outside local development. |
OPEN_COWORK_CLOUD_ISOLATION_IMAGE_SHA256 / OPEN_COWORK_CLOUD_ISOLATION_IMAGE_SIGNATURE | worker, all-in-one | The SHA-256 digest is required: the provider rewrites mutable tags to a digest-qualified reference and verifies the local image ID/RepoDigest before reporting capability. A signature is optional supplementary provenance and never substitutes for the inspected digest. Unpinned or mismatched production boundaries are rejected. |
OPEN_COWORK_CLOUD_ISOLATION_OPENCODE_BIN | worker, all-in-one | Absolute in-image path to the OpenCode executable. Defaults to the direct production dependency binary in the standard open-cowork-cloud image (/app/node_modules/.bin/opencode). Relative and parent-traversing paths fail closed. |
OPEN_COWORK_CLOUD_ISOLATION_NETWORK_POLICY | worker, all-in-one | deny-all (default) or restricted. This is the runtime boundary's egress policy, separate from pod/service ingress policy. |
OPEN_COWORK_CLOUD_ISOLATION_NETWORK_NAME | worker, all-in-one | Existing Docker internal network used when the policy is restricted. The provider does not create or mutate networks. Allowed upstreams must be attached to this network directly or exposed through a policy-enforcing proxy attached to it. |
OPEN_COWORK_CLOUD_ISOLATION_EGRESS_POLICY_ID | worker, all-in-one | Expected immutable egress-policy identifier. A restricted Docker network must have Internal=true and carry open-cowork.isolation=true plus open-cowork.egress_policy=<this value> labels or readiness fails. Labels identify operator intent; they do not replace the enforceable internal-network check. |
OPEN_COWORK_CLOUD_SHUTDOWN_GRACE_MS | worker, scheduler, all-in-one | Grace window used during process shutdown to let an active worker/scheduler loop finish after a drain or termination signal. |
OPEN_COWORK_CLOUD_SCHEDULER_ID | scheduler, all-in-one | Stable scheduler identity for heartbeats. |
OPEN_COWORK_CLOUD_SCHEDULER_POLL_MS | scheduler, all-in-one | Workflow scheduler polling interval. |
Watchdog decisions and recovery outcomes emit only the closed metric dimensions watchdog_state (suspect or stalled) and watchdog_outcome. The worker also writes an idempotent system audit event to the session tenant's resolved org; its metadata is limited to mode, state, outcome, and source, with no tenant, session, run, lease, prompt, output, or path identifiers.
Cloud advanced / tuning¶
Most deployments never set these; they carry safe defaults. They exist so an operator can tune storage location, connection caps, the runtime cache, and Postgres safety timeouts per deployment.
| Variable | Meaning |
|---|---|
OPEN_COWORK_CLOUD_ROOT | Filesystem root for cloud runtime working data (logs, ephemeral workspaces, and the filesystem object-store adapter). Compose references set /data/open-cowork-cloud. |
OPEN_COWORK_CLOUD_DEPLOYMENT_TIER | Deployment tier that gates production safety checks: local for demos, public_production for hosted deployments (rejects insecure auth, weak secrets, ephemeral storage, and other unsafe defaults). |
OPEN_COWORK_CLOUD_CORS_ORIGIN | Single allowed CORS origin for the cloud HTTP/SSE API. Leave unset for same-origin browser access; set it when a separate first-party origin must call the API. |
OPEN_COWORK_CLOUD_RUN_MIGRATIONS | Defaults to true for local/self-host development. Public production fails closed unless this is explicitly false; run the exact pinned image's cloud:migrate:start entrypoint separately with a short-lived migrator credential. |
OPEN_COWORK_CLOUD_RUNTIME_DATABASE_ROLE | Migrator-only PostgreSQL role name for the least-privilege runtime role that should receive the application DML grants. Set only on the separately credentialed cloud:migrate:start job, together with OPEN_COWORK_CLOUD_RUNTIME_DATABASE_PRINCIPAL; do not set either variable on long-running roles. |
OPEN_COWORK_CLOUD_RUNTIME_DATABASE_PRINCIPAL | Migrator-only PostgreSQL login/IAM principal that receives membership in OPEN_COWORK_CLOUD_RUNTIME_DATABASE_ROLE. The principal must already exist; the migration job validates that neither identity is privileged before granting runtime access. |
OPEN_COWORK_CLOUD_ALLOW_EPHEMERAL_STORAGE | Explicit operator acknowledgement that non-durable control-plane/object storage is acceptable. Required to start a public_production tier on ephemeral storage; keep unset so accidental ephemeral storage fails closed. |
OPEN_COWORK_CLOUD_LIVENESS_PORT | Optional dedicated port for a minimal liveness server (separate from the main HTTP port). 0/unset disables it; the main /livez route remains available. |
OPEN_COWORK_CLOUD_RUNTIME_CACHE_MAX_ENTRIES | Hard maximum for idle cached, active, and creating OpenCode runtime boundaries on one worker (default 100; the conservative Helm worker default is 2). The permit stays held through teardown, so cleanup debt cannot silently admit a replacement. |
OPEN_COWORK_CLOUD_RUNTIME_CACHE_IDLE_TTL_MS | Idle eviction window for cached worker runtimes (default 1800000, 30 minutes). |
OPEN_COWORK_CLOUD_RUNTIME_ADMISSION_QUEUE_MAX_ENTRIES | Maximum distinct session creations waiting for a runtime permit (default 100; the Helm worker default is 8). Concurrent misses for one session share one queue/provision attempt. A full queue returns a typed retryable capacity error. |
OPEN_COWORK_CLOUD_RUNTIME_ADMISSION_TIMEOUT_MS | Maximum time a runtime creation waits for capacity (default 30000). Expiry returns a typed retryable capacity error and removes the waiter. |
OPEN_COWORK_CLOUD_RUNTIME_PROVISION_TIMEOUT_MS | End-to-end deadline for preparing and creating one runtime boundary (default 120000). Expiry aborts the provider contract and returns retryable backpressure; the permit remains held until any late boundary is closed. |
OPEN_COWORK_CLOUD_RUNTIME_TEARDOWN_TIMEOUT_MS | Maximum time one adapter-close pass waits for active work, provisioning, and provider cleanup (default 30000). A timed-out pass fails closed without discarding capacity debt and can be retried. |
OPEN_COWORK_CLOUD_ISOLATION_MEMORY_LIMIT_BYTES | Per-sandbox memory ceiling in bytes (default 2147483648). Must be a positive safe integer. |
OPEN_COWORK_CLOUD_ISOLATION_CPU_LIMIT | Per-sandbox Docker CPU ceiling (default 2; fractional CPU values such as 0.75 are supported). |
OPEN_COWORK_CLOUD_ISOLATION_PIDS_LIMIT | Per-sandbox process-tree ceiling (default 512). This bounds OpenCode and all descendant tools together. |
OPEN_COWORK_CLOUD_PG_LOCK_TIMEOUT_MS | Postgres lock_timeout per connection (default 0, disabled). Set a non-zero value to bound how long a statement waits on a row/table lock before failing fast. |
OPEN_COWORK_CLOUD_MAX_CONNECTIONS | Maximum simultaneous TCP connections accepted by the cloud HTTP server (default 10000). A connection-exhaustion guard above the Node default of unbounded. |
OPEN_COWORK_CLOUD_MAX_SSE_CONNECTIONS_PER_ORG | Maximum concurrent browser/desktop SSE streams per org per web pod (default 200). Enforced by the SSE stream registry on every session, workspace, and channel-delivery stream; excess subscriptions for one org receive an SSE error and are dropped so a single tenant cannot exhaust stream slots. Size this to concurrent interactive users × open tabs, not historical sessions. |
OPEN_COWORK_CLOUD_SSE_POLL_INTERVAL_MS | SSE read-poll cadence in milliseconds (default 1000). Each open stream polls the control plane at this interval for new events; lower it to cut delivery latency at the cost of more control-plane queries, raise it to shed query load at the cost of latency. |
OPEN_COWORK_CLOUD_SSE_PG_NOTIFY | Postgres LISTEN/NOTIFY accelerator for SSE delivery (default false, Postgres control plane only). Production multi-web-pod topologies should set this to true. When enabled, the worker write path emits a best-effort NOTIFY (identifiers only, no event bodies) after each session/workspace event commit, and each web pod opens one dedicated LISTEN connection that wakes the matching SSE topic for an immediate read instead of waiting for the next poll. The poll loop always keeps running as the guaranteed backstop, so a missed or duplicate notification is harmless and a listener failure degrades to pure polling. Leave unset (the default) only for single-process demos that want byte-for-byte poll-only delivery. |
OPEN_COWORK_CLOUD_SSE_NOTIFY_BACKSTOP_POLL_MS | Backstop read-poll cadence (default 15000, 15 seconds) applied only to NOTIFY-addressable SSE topics when OPEN_COWORK_CLOUD_SSE_PG_NOTIFY is enabled. With the accelerator on, LISTEN/NOTIFY drives low-latency delivery, so the per-topic poll relaxes to this longer backstop cadence (bounded by max(OPEN_COWORK_CLOUD_SSE_POLL_INTERVAL_MS, this)), cutting steady-state control-plane queries. Has no effect when the accelerator is off — poll cadence stays at OPEN_COWORK_CLOUD_SSE_POLL_INTERVAL_MS. |
OPEN_COWORK_CLOUD_SSE_MAX_LIFETIME_MS | Hard ceiling on a single SSE stream's lifetime (default 1800000, 30 minutes). A wedged/half-open stream cannot pin a slot indefinitely; EventSource clients reconnect transparently. |
The Helm worker values runtimeCapacity, admissionQueueCapacity, admissionQueueTimeoutMs, runtimeProvisionTimeoutMs, runtimeTeardownTimeoutMs, isolationMemoryLimitBytes, isolationCpuLimit, isolationPidsLimit, sessionConcurrency, maxCommandsPerSessionPerTick, and maxLeases map to these worker budgets. The chart also supplies nonzero CPU, memory, and ephemeral-storage requests and limits for workers. Override them together from measured workload evidence. At minimum, runtimeCapacity × isolationMemoryLimitBytes and runtimeCapacity × isolationCpuLimit must fit inside the worker pod limits with headroom for the worker process and checkpoint I/O. The hard runtime cap is never raised automatically in response to memory, CPU, event-loop, or queue pressure, and sandbox plus pod/container cgroup limits remain the enforcement boundaries for OpenCode descendants.
The sandbox provider sets OPENCODE_DISABLE_PROJECT_CONFIG=1 and masks tenant workspace opencode.json, opencode.jsonc, and .opencode paths with boundary-owned read-only empty artifacts. The mask is required because pinned OpenCode 1.18.1's native V2 API does not apply the classic project-config flag while discovering its location config. Both API generations therefore see the same managed policy, and tenant plugins or agent definitions cannot widen it. When the workspace contains a regular AGENTS.md file, the provider preserves those intended project instructions by adding the explicit container path /workspace/AGENTS.md to the managed OpenCode instructions list. These sandbox controls do not change Desktop project behavior.
Sandbox environments are built from a deterministic in-image baseline (PATH, C.UTF-8, and UTC), managed runtime-home/auth values, and the two per-boundary Knowledge values when enabled. Worker proxy variables, proxy credentials, host user/PATH values, and host certificate paths never cross the execution boundary; restricted egress is enforced by the declared network policy instead.
Knowledge is registered inside an isolated runtime only when the feature, tool, and MCP policies all allow it and the declared network is restricted. deny-all boundaries receive no Knowledge script mount, token, or MCP registration. Before admitting an enabled boundary, the provider sends a non-mutating invalid proposal to the exact token-auth route and requires its expected 400 validation response; DNS, TLS, routing, token, feature, or route failures therefore keep the session out of service. Shared-process local development alone may use the co-located loopback origin.
Multi-pod SSE load guidance¶
Split-role production Cloud keeps durable events in Postgres. Every open SSE stream still needs a delivery path from the worker that wrote the event to the web pod holding the browser connection:
| Topology | Recommended SSE settings | Why |
|---|---|---|
Single all-in-one demo | defaults (SSE_PG_NOTIFY off, poll 1s) | One process; poll loop is enough. |
| Single web pod + separate workers | OPEN_COWORK_CLOUD_SSE_PG_NOTIFY=true | Workers and web do not share memory; NOTIFY cuts end-to-end latency without depending on pod-local buses. |
| Multiple web pods behind a load balancer | OPEN_COWORK_CLOUD_SSE_PG_NOTIFY=true (required for scale) | Cross-pod wake; poll-only multiplies control-plane reads by streams × pods × (1000 / pollMs). |
| Large interactive orgs | raise OPEN_COWORK_CLOUD_MAX_SSE_CONNECTIONS_PER_ORG only with evidence; keep OPEN_COWORK_CLOUD_MAX_CONNECTIONS above web_replicas × max_per_org × orgs_per_pod | Caps are per pod; global capacity is replicas × per-org cap under sticky routing assumptions. |
Load-planning rule of thumb for multi-pod web:
- Enable
OPEN_COWORK_CLOUD_SSE_PG_NOTIFY=trueon web and worker roles (worker emits NOTIFY; web LISTENs). - Keep
OPEN_COWORK_CLOUD_SSE_POLL_INTERVAL_MSat1000(or higher) and rely on NOTIFY for low latency; tuneOPEN_COWORK_CLOUD_SSE_NOTIFY_BACKSTOP_POLL_MS(default 15s) as the safety net. - Size
OPEN_COWORK_CLOUD_MAX_SSE_CONNECTIONS_PER_ORGto concurrent tabs per org (default 200). Rejected streams reconnect transparently after a slot frees. - Include SSE in launch load gates (
OPEN_COWORK_LOAD_INCLUDE_SSE=true) so reconnect storms and per-org caps appear in evidence before production traffic.
OPEN_COWORK_CLOUD_PUBLIC_URL doubles as the HSTS switch: the server only emits Strict-Transport-Security when OPEN_COWORK_CLOUD_PUBLIC_URL is an HTTPS, non-loopback origin. Any HTTPS-fronted deployment — including self-host tiers behind a TLS-terminating reverse proxy or ingress — must set OPEN_COWORK_CLOUD_PUBLIC_URL to the canonical https:// origin so HSTS is emitted (and so redirects/cookies never depend on untrusted forwarded headers).
Profiles are enforced server-side and in generated OpenCode config. Cloud profiles default to app-managed runtime config and deny arbitrary host project directories, arbitrary local stdio MCPs, and machine-native runtime config unless a deployer explicitly allows a controlled image or volume-backed environment.
Desktop Sync Configuration¶
Desktop-to-cloud sync is configured separately from the cloud service roles. Downstream desktop builds can preconfigure managed cloud orgs without changing renderer code by setting cloudDesktop in open-cowork.config.json or a managed config layer:
{
"cloudDesktop": {
"enabled": true,
"allowUserAddedConnections": false,
"requireManagedOrg": true,
"cacheMode": "metadata-only",
"cacheEncryptionFallback": "disabled",
"preconfiguredConnections": [
{
"baseUrl": "https://cowork.acme.example",
"label": "Acme Cloud"
}
]
}
}
enabled: false hides cloud workspaces and keeps the desktop fully local. requireManagedOrg: true limits desktop sync to the configured org list and blocks user-added cloud URLs. cacheMode controls the local cloud cache:
fullcaches encrypted session projections and metadata.metadata-onlycaches lists, cursors, and metadata but strips message bodies and full projections.disabledavoids durable cloud cache state.
When cacheMode is full, the desktop requires OS-backed encrypted storage or uses cacheEncryptionFallback to degrade to metadata-only, disabled, or fail startup. OAuth access and refresh tokens are stored in OS secure storage, not in the cloud cache. The cache is keyed by cloud connection, tenant, user, profile, and workspace identity so two orgs cannot share cached projections or event cursors. Upgrading the cache from the unversioned v1 format to v2 preserves session metadata, settings, and artifact metadata, but purges all opaque transcript views and workflow projections because historic values cannot be safely field-scrubbed for credentials.
Cloud workspace logout is an authentication boundary, not a data wipe. It removes the workspace access and refresh tokens, closes active cloud subscriptions, and leaves any existing cloud cache as a local read-only fallback until the user signs in again or removes the workspace. Removing a user-added cloud workspace is the wipe boundary: it removes the registry record, credentials, active subscriptions, and that workspace's cached metadata, cursors, artifact metadata, and encrypted projections. Managed preconfigured workspaces cannot be removed by the user; downstream builds that require a stronger wipe policy should configure cacheMode: "disabled" or expose an admin-managed cache clear flow.
Every workspace-scoped API also has a typed support matrix exposed through workspace.support(). Cloud-only clients should use it to distinguish supported, blocked_by_policy, not_supported, and later-phase surfaces instead of assuming local desktop APIs such as host-path diffs or local stdio MCPs are available in cloud workspaces.
Provider Mapping¶
Provider-specific recipes should remain thin compositions of the same images, roles, Postgres control plane, object-store adapter, and secret adapter.
| Provider | Web | Worker/Scheduler | Gateway | Control plane | Object store | Secret store |
|---|---|---|---|---|---|---|
| Kubernetes | Deployment + Service/Ingress | Downstream isolated provider required; stock Helm worker is not execution-ready | Separate Deployment + Service/Ingress for webhook channels | Managed or in-cluster Postgres | S3-compatible, GCS, Azure Blob, or MinIO | External Secrets or sealed secrets |
| GCP | Cloud Run or GKE | Dedicated Docker-capable worker host or reviewed GKE provider integration | Cloud Run or GKE Deployment | Cloud SQL for PostgreSQL | Cloud Storage | Secret Manager |
| AWS | ECS/Fargate or EKS | Dedicated Docker-capable worker host or reviewed ECS/EKS provider integration | ECS service or EKS Deployment | RDS PostgreSQL | S3 | Secrets Manager |
| Azure | Container Apps or AKS | Dedicated Docker-capable worker host or reviewed Container Apps/AKS provider integration | Container Apps service or AKS Deployment | Azure Database for PostgreSQL | Azure Blob Storage | Key Vault |
| DigitalOcean | App Platform for demos; DOKS for web/scheduler | Dedicated Docker-capable worker host or reviewed DOKS provider integration | App Platform component or DOKS Deployment | Managed PostgreSQL | Spaces | App Platform/DOKS secrets |
The app core should not contain provider-specific branches. Provider behavior belongs in config, adapters, and deployment recipes.
Recipe starting points live under deploy/gcp, deploy/aws, deploy/azure, and deploy/digitalocean. Each one maps provider services back to the same role, Postgres, object-store, and secret-manager contract.
The GCP reference recipe includes a split-role GKE values file, External Secrets example, managed certificate example, Cloud Run all-in-one demo manifest, and provider smoke commands. Run pnpm deploy:gcp:preflight before rollout to check the active gcloud project, region, required APIs, and reference files without mutating resources. Run pnpm deploy:gcp:smoke after rollout to combine the Cloud Web smoke with Cloud Storage and Secret Manager checks.
Managed operators should also keep the runbook in docs/runbooks/cloud-managed-operations.md current for readiness, rollback, gateway backlog, secret rotation, and diagnostic export procedures.
Focused Agent Deployments¶
Use the focused-agent profile when a downstream wants a single-purpose cloud app, such as a data-analyst agent. The deployer should configure:
- one allowlisted agent name,
- the minimum tool/MCP allowlist required by that agent,
- disabled custom skills, custom agents, custom MCPs, workflows, and settings unless the use case requires them,
- OIDC or reverse-proxy authentication before exposing the service publicly.
The web API enforces profile allowlists directly, so hiding a feature in the browser is not the only protection. The worker also compiles the selected profile into the final OpenCode permission and MCP configuration for every new runtime. An omitted tools or mcps field inherits the complete configured catalog; an explicit empty array denies that capability class. Unknown identifiers fail runtime provisioning instead of being ignored. MCP-backed tools require both their tool id and MCP namespace to be allowed, and the Knowledge MCP is injected only when its feature, tool id, and MCP namespace are all enabled.
Operational Checks¶
GET /livezreports process liveness and the active cloud role/profile.GET /readyzreports dependency readiness for the control plane, object store, secret adapter, billing adapter, auth config, and role-specific worker dependencies. Kubernetes readiness probes should use this route.GET /api/runtime/statusreports the active role/profile, whether this process can execute runtime commands, and whether commands are handled inline or by durable worker polling.GET /api/workers/heartbeatsexposes worker and scheduler heartbeat state for authenticated operators.GET /api/metricsexposes operator-scoped Prometheus metrics from the in-process cloud observability adapter. API-token callers needoperatorscope; desktop/gateway tokens and non-operator users are rejected. Use OTLP for provider-managed tracing/metrics and this endpoint for scrape-based dashboards.- Cloud HTTP requests emit structured request logs with request ids, role, profile, status, and duration. When
OPEN_COWORK_CLOUD_OTLP_ENDPOINTis set, the same request observations are exported as OTLP HTTP traces and duration metrics without adding provider-specific code paths. - OTLP export is best-effort and bounded: collector failures do not block Cloud requests or worker commands, overflow drops the oldest queued records, and
open_cowork_cloud_otlp_dropped_records_totalreports queue drops by record type. - Browser event streams use ordered durable events, so web replicas do not need sticky sessions for session replay.
- Browser sessions use signed
HttpOnly,SameSite=Laxcookies and requireX-CSRF-Tokenon mutating API calls when authenticated by cookie. Bearer API clients remain supported without CSRF. - OIDC browser login is exposed through
/auth/loginand the configured callback path. The flow uses signed state cookies, PKCE, nonce validation, and the same verified identity mapping as bearer JWT auth. - Public workflow webhooks require HMAC timestamp signatures. Direct
nonemode is loopback/local-only.headermode is only for a reverse proxy that strips caller-supplied identity headers and injects trustedx-open-cowork-*headers.
See also Worker-scoped runtime adapter.