源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)

This commit is contained in:
agent-radar
2026-09-02 01:05:41 +08:00
commit 04bac1f6e2
2200 changed files with 489415 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: openwork-ee
description: OpenWork EE Den control plane, web app, and optional inference service
type: application
version: 0.1.0
appVersion: "0.1.0"
+986
View File
@@ -0,0 +1,986 @@
# OpenWork EE Helm Chart
Initial Helm chart for the OpenWork EE Den stack:
- `den-api` control plane on port `8788`
- `den-web` web app on port `3005`
- optional `inference` service on port `8791`
- shared ConfigMap and Secret templating
- optional Ingress for web and API hosts
- pre-install/pre-upgrade migration Job scaffold
## Install
Published releases are available as an OCI Helm chart:
```bash
helm upgrade --install openwork-ee oci://ghcr.io/different-ai/charts/openwork-ee \
--version REPLACE_OPENWORK_VERSION \
-f values.prod.yaml
```
Use the matching image tag in `values.prod.yaml`:
Create a values file for the target environment:
```yaml
image:
tag: "REPLACE_OPENWORK_VERSION"
config:
tenancy:
# Default chart behavior is single-org for private/self-hosted installs.
# Hosted OpenWork Cloud should set this to "multi_org" explicitly.
mode: "single_org"
singleOrgName: "OpenWork"
singleOrgSlug: "default"
ownerEmails: "admin@example.com"
allowPublicSignup: "false"
requireEmailVerification: "false"
public:
# Single public Den web origin. The chart renders this as DEN_BASE_URL and
# den-api derives Better Auth, CORS/trusted origins, web-app hosts, API
# defaults, and MCP resource defaults from it.
webOrigin: "https://openwork.example.com"
# Leave these blank unless you intentionally expose a split Den API origin
# or need migration compatibility with an older topology.
apiOrigin: ""
mcpResourceUrl: ""
mcpClaimNamespace: "https://openwork.example.com"
desktopDenBaseUrl: ""
corsOrigins: ""
betterAuthTrustedOrigins: ""
webAppHosts: ""
bootstrapAdminEmails: "admin@example.com"
# Self-hosted default: every organization gets install downloads.
installLinksGatingEnabled: "false"
authCallbackUrl: "https://openwork.example.com"
githubConnector:
appId: ""
clientId: ""
secret:
values:
# Transitional/smoke TLS only: sslaccept=accept encrypts without certificate verification.
# For production verification, use customCa plus sslmode=verify-full or verify-ca.
databaseUrl: "mysql://openwork:REPLACE_ME@mysql.example.internal:3306/openwork_den?sslaccept=accept"
betterAuthSecret: "REPLACE_WITH_AT_LEAST_32_CHARACTERS"
denDbEncryptionKey: "REPLACE_WITH_AT_LEAST_32_CHARACTERS"
emailFrom: "OpenWork <no-reply@example.com>"
smtpHost: "smtp.example.com"
smtpPort: "587"
smtpUser: "openwork@example.com"
smtpPass: "REPLACE_ME"
smtpSecure: "false"
githubConnectorAppClientSecret: ""
githubConnectorAppPrivateKey: ""
githubConnectorAppWebhookSecret: ""
ingress:
enabled: true
className: nginx
web:
host: openwork.example.com
api:
host: api.openwork.example.com
```
### Upgrade note: public URL values
Current chart versions make `config.public.webOrigin` the primary public URL.
It renders as `DEN_BASE_URL`, and Den derives these values from it unless you
set explicit compatibility overrides:
- `BETTER_AUTH_URL`
- `CORS_ORIGINS`
- `DEN_BETTER_AUTH_TRUSTED_ORIGINS`
- `DEN_WEB_APP_HOSTS`
- `DEN_API_PUBLIC_URL`
- `DEN_MCP_RESOURCE_URL`
When upgrading an existing release, inspect your values file for the older
split-origin keys under `config.public`. If your deployment is single-origin,
remove `apiOrigin`, `mcpResourceUrl`, `desktopDenBaseUrl`, `corsOrigins`,
`betterAuthTrustedOrigins`, and `webAppHosts` or set them to `""` so Den can
derive them from `webOrigin`. Keep `apiOrigin`/`mcpResourceUrl` only when you
intentionally expose a separate API origin for install-link exchange or external
MCP clients.
For private GHCR packages, authenticate before installing:
```bash
helm registry login ghcr.io
kubectl create secret docker-registry ghcr-pull-secret \
--docker-server=ghcr.io \
--docker-username=<github-user> \
--docker-password=<github-token>
```
Then add:
```yaml
imagePullSecrets:
- name: ghcr-pull-secret
```
For local development from a repository checkout, render or install directly:
```bash
helm template openwork-ee ./packaging/helm/openwork-ee -f values.prod.yaml
helm upgrade --install openwork-ee ./packaging/helm/openwork-ee -f values.prod.yaml
```
### Automations rollout
The Helm chart advertises Automations as unavailable by default for self-hosted
and customer-managed deployments. Availability and server shutdown are
separate so a Den upgrade cannot remove routes beneath an older published
Desktop:
| `automationsEnabled` | `automationsRuntimeEnabled` | Behavior |
| --- | --- | --- |
| `"false"` | `"true"` | New Desktops hide Automations; legacy routes and scheduling remain available during the upgrade window. |
| `"false"` | `"false"` | Automations are hard-disabled: routes, MCP resources, and scheduler startup are omitted. |
| `"true"` | `"true"` | Automations are available and execute normally. |
| `"true"` | `"false"` | The runtime shutdown wins and Desktop receives `automationsEnabled: false`. |
Set both values explicitly when the deployment is ready to run Automations:
```yaml
config:
public:
automationsEnabled: "true"
automationsRuntimeEnabled: "true"
```
These render `DEN_AUTOMATIONS_ENABLED=true` and
`DEN_AUTOMATIONS_RUNTIME_ENABLED=true` for Den. An entirely unconfigured Den
keeps availability fail-closed while preserving the legacy runtime. When using
raw environment variables, an explicit `DEN_AUTOMATIONS_ENABLED` value also
becomes the runtime default: `false` is therefore a complete shutdown unless
`DEN_AUTOMATIONS_RUNTIME_ENABLED=true` explicitly selects mixed-version
compatibility. The chart always renders both values to make that choice
unambiguous. Hosted OpenWork Cloud explicitly enables availability.
Desktop v0.18.35 and newer consume the value from `/v1/me/desktop-config`, hide
the Automation surface, and do not register a runner unless the value is
explicitly true. Older clients predate that contract, so the runtime flag must
remain true while they are in use even when availability is false.
For an existing deployment, stage the upgrade so independently released Den
and Desktop versions never observe an unintended flag state:
1. Keep `config.public.automationsRuntimeEnabled: "true"` while any connected
Desktop is older than v0.18.35.
2. Upgrade Den. Legacy Desktops retain their existing routes and scheduling;
compatible Desktops honor `automationsEnabled` from desktop config.
3. Roll out Desktop v0.18.35 or newer to the whole deployment.
4. To keep Automations, set both values to true. To disable them, set both
values to false only after the Desktop rollout is complete.
New installations with no legacy Desktop clients can hard-disable Automations
immediately by setting both values to false.
### Dashboards rollout
The organization-managed Dashboard is unavailable by default. Enable it for a
self-hosted or customer-managed deployment with:
```yaml
config:
public:
dashboardsEnabled: "true"
```
The chart renders this value as `DEN_DASHBOARDS_ENABLED`. Raw environment-based
deployments can set the same variable directly. Hosted environments use that
same flag, so Dashboard availability does not depend on a per-device preference.
Desktop reads `dashboardEnabled` from `/v1/me/desktop-config` and hides both the
sidebar entry and route unless the server explicitly returns `true`.
### OpenWork Web rollout
OpenWork Web is unavailable by default for self-hosted and customer-managed
deployments. OpenWork Cloud enables it in its deployment values with:
```yaml
config:
public:
openworkWebEnabled: "true"
```
The chart renders this value as `DEN_OPENWORK_WEB_ENABLED`. A raw environment
deployment can set the same variable directly. Missing, blank, false, or an
unrecognized value fails closed: Den omits Web from its advertised
capabilities, the sidebar and Billing offer stay hidden, and Web billing routes
return the deployment-unavailable response. Availability is deployment-wide;
it does not depend on organization mode, Stripe-variable presence, or mutable
organization metadata.
Enabling the flag advertises the hosted product. The deployment must also
configure `STRIPE_SECRET_KEY` and `STRIPE_OPENWORK_WEB_PRICE_ID` before the
purchase action becomes available. This separate billing-readiness check keeps
a partially configured hosted rollout visible but non-purchasable instead of
mistaking secrets for an availability signal.
Provider-specific starter guides:
- AWS EKS:
[guide](../../../docs/aws-eks-helm.md),
[`examples/values.aws-load-balancer.yaml`](examples/values.aws-load-balancer.yaml),
[`examples/values.aws-load-balancer-http-smoke.yaml`](examples/values.aws-load-balancer-http-smoke.yaml).
The recommended first AWS path is EKS Auto Mode plus `LoadBalancer` Services,
which provisions AWS Network Load Balancers without installing an ingress
controller.
- Azure AKS:
[guide](../../../docs/azure-aks-helm.md),
[`examples/values.azure-ingress.yaml`](examples/values.azure-ingress.yaml).
The recommended first Azure path is VNet-first AKS application routing plus
Azure Database for MySQL Flexible Server private access, with
`ingress.enabled=true`.
- Google Cloud GKE:
[guide](../../../docs/gcp-gke-helm.md),
[`examples/values.gcp-ingress.yaml`](examples/values.gcp-ingress.yaml).
The recommended first GCP path is GKE Ingress with a reserved global IP,
Google-managed certificate, and BackendConfig health checks.
`ingress.enabled=true` only emits Kubernetes `Ingress` resources; it does not
install an ingress controller. Use it only when the cluster already has a
compatible provider ingress controller.
Published self-host planning pages:
- [Private network deployment](../../../packages/docs/start-here/private-network-deployment.mdx)
- [Air-gapped deployment](../../../packages/docs/start-here/air-gapped-deployment.mdx)
- [Installer delivery](../../../packages/docs/start-here/installer-delivery.mdx)
- [Certificate trust and proxies](../../../packages/docs/start-here/certificate-trust-and-proxies.mdx)
## Secrets
The chart can create an Opaque Secret from `secret.values`, or consume an existing Secret:
```yaml
secret:
create: false
existingSecret: openwork-ee-secrets
```
The existing Secret must contain the keys listed under `secret.keys`, especially:
- `DATABASE_URL`
- `BETTER_AUTH_SECRET`
- `DEN_DB_ENCRYPTION_KEY`
Set optional `DATABASE_REDIS_URL` to enable Den API Redis-backed session and query caching. Set `DAYTONA_API_KEY` when `config.provisioner.mode` is `daytona`. Set `POLAR_ACCESS_TOKEN` when Polar feature gating is enabled. Set `OPENROUTER_MANAGEMENT_API_KEY` when enabling OpenWork Models management.
Redis cache examples:
```yaml
secret:
values:
databaseRedisUrl: "rediss://redis-master.openwork.svc.cluster.local:6379"
```
Prefer `rediss://`. For hosting platforms that only provide a private internal
`redis://` URL, such as Render internal Redis, explicitly acknowledge the trust
boundary with `redis.allowInsecureInternal=true`. Use this only when the Redis
endpoint is non-public and reachable only from trusted services in the private
network.
```yaml
redis:
allowInsecureInternal: true
secret:
values:
databaseRedisUrl: "redis://red-...:6379"
```
## Custom CA certificates
For higher-level planning across desktop, sidecar, Den, and MySQL trust
surfaces, see the published
[Certificate trust and proxies](../../../packages/docs/start-here/certificate-trust-and-proxies.mdx)
page. This section remains the authoritative chart values reference.
Use `customCa` when OpenWork must trust a private certificate authority for
strict TLS verification, such as a MySQL endpoint signed by an internal or cloud
private CA. The chart does not accept PEM material in values and does not create
the CA resource for you; create the Kubernetes Secret or ConfigMap in the
release namespace before running `helm install` or `helm upgrade`.
Secret example:
```bash
kubectl create secret generic openwork-custom-ca \
--namespace openwork \
--from-file=ca.crt=./corp-root-ca.pem
```
```yaml
customCa:
enabled: true
existingSecret: openwork-custom-ca
existingConfigMap: ""
key: ca.crt
```
ConfigMap example:
```bash
kubectl create configmap openwork-custom-ca \
--namespace openwork \
--from-file=ca.crt=./corp-root-ca.pem
```
```yaml
customCa:
enabled: true
existingSecret: ""
existingConfigMap: openwork-custom-ca
key: ca.crt
```
When enabled, set exactly one of `existingSecret` or `existingConfigMap`, and set
`key` to the data key containing the CA bundle. The chart mounts only that key as
`/etc/openwork/custom-ca/ca-bundle.pem` and sets `NODE_EXTRA_CA_CERTS` to that
file for `den-api`, `den-web`, enabled `inference`, and the migration Job. Do
not also set `denApi.env.NODE_EXTRA_CA_CERTS`, `denWeb.env.NODE_EXTRA_CA_CERTS`,
or `inference.env.NODE_EXTRA_CA_CERTS`; Helm rejects those conflicts while
`customCa.enabled=true`.
For strict MySQL TLS verification, pair the mounted CA with a verifying
`DATABASE_URL`, for example:
```yaml
secret:
values:
databaseUrl: "mysql://openwork:REPLACE_DB_PASSWORD@mysql.example.internal:3306/openwork_den?sslmode=verify-full"
```
`sslmode=verify-ca`, `sslmode=verify-full`, and `sslaccept=strict` enable strict
certificate verification. `sslaccept=accept` keeps TLS enabled but does not
verify the certificate chain, so use it only for smoke tests or while preparing
the CA bundle.
The custom CA is release-wide for Node.js processes in this chart. Treat it as a
global trust decision for outbound TLS from those workloads, and include only CA
roots your OpenWork deployment should trust. On CA rotation, update the existing
Secret or ConfigMap and restart the running workloads so Node reloads the CA
file, for example:
```bash
kubectl rollout restart deployment/openwork-ee-den-api --namespace openwork
kubectl rollout restart deployment/openwork-ee-den-web --namespace openwork
kubectl rollout restart deployment/openwork-ee-inference --namespace openwork
```
The next migration hook Job will mount the current CA data; rerun a failed
upgrade after the CA resource is corrected.
## Observability
The chart exposes first-class runtime observability settings for `den-api` and
`den-web` only. `observability.backend` defaults to `none`; set it to `otel` or
`sentry` to enable the matching runtime environment. The chart injects distinct
`OTEL_SERVICE_NAME` values directly into each Deployment, so the shared
ConfigMap is not used for service identity or auth-like observability values.
OpenTelemetry uses OTLP over `http/protobuf`, with a shared endpoint and
optional per-signal endpoint overrides. Per-signal exporters default to `otlp`,
and trace sampling defaults to the standard parent-based always-on sampler.
### OpenTelemetry quick start
Before starting, you need:
- An OpenTelemetry Collector or vendor endpoint reachable from the Kubernetes
cluster over OTLP HTTP. Port `4318` is the usual Collector port.
- The endpoint's authentication token or headers, if it requires
authentication.
- `kubectl` and Helm configured for the target cluster.
The chart configures telemetry export from OpenWork; it does not install an
OpenTelemetry Collector. For an in-cluster Collector, use its Kubernetes DNS
name, for example
`http://otel-collector.observability.svc.cluster.local:4318`. Do not use
`localhost`, because that would refer to the OpenWork container itself.
First create the namespace used by this example:
```bash
kubectl create namespace openwork
```
If the Collector does not require authentication, skip the Secret and leave
`observability.otel.headers.existingSecret` empty.
If it requires a bearer token, create the header Secret in the **same
namespace as OpenWork**:
```bash
kubectl create secret generic openwork-otel-headers \
--namespace openwork \
--from-literal=OTEL_EXPORTER_OTLP_HEADERS='Authorization=Bearer <token>'
```
Replace `<token>` with the real token. Keep the single quotes so your shell
passes the complete header as one value. To update an existing Secret without
deleting it first, use:
```bash
kubectl create secret generic openwork-otel-headers \
--namespace openwork \
--from-literal=OTEL_EXPORTER_OTLP_HEADERS='Authorization=Bearer <token>' \
--dry-run=client -o yaml | kubectl apply -f -
```
Multiple OTLP headers use the standard comma-separated `key=value` format:
```bash
kubectl create secret generic openwork-otel-headers \
--namespace openwork \
--from-literal=OTEL_EXPORTER_OTLP_HEADERS='Authorization=Bearer <token>,x-scope-orgid=<tenant>'
```
Do not put tokens directly in a values file. Kubernetes Secrets are not
encrypted by default unless your cluster enables encryption at rest, so use
your organization's external-secret or secret-management system in production
when available.
Create `values-observability.yaml`:
```yaml
observability:
backend: otel
serviceNames:
denApi: openwork-den-api
denWeb: openwork-den-web
otel:
endpoint: "http://otel-collector.observability.svc.cluster.local:4318"
tracesEndpoint: ""
metricsEndpoint: ""
logsEndpoint: ""
exporters:
traces: otlp
metrics: otlp
logs: otlp
tracesSampler: parentbased_always_on
tracesSamplerArg: ""
headers:
existingSecret: openwork-otel-headers
key: OTEL_EXPORTER_OTLP_HEADERS
```
For a Collector without authentication, use:
```yaml
headers:
existingSecret: ""
key: OTEL_EXPORTER_OTLP_HEADERS
```
Install or upgrade OpenWork with the values file:
```bash
helm upgrade --install openwork-ee ./packaging/helm/openwork-ee \
--namespace openwork \
--create-namespace \
--values values-observability.yaml
```
`observability.otel.headers.existingSecret` must name an existing Kubernetes
Secret. Its key is exposed as `OTEL_EXPORTER_OTLP_HEADERS` only on `den-api` and
`den-web`; it is not added to inference pods or migration Jobs.
### Verify the OpenTelemetry setup
The commands below assume the Helm release is named `openwork-ee`. If you use a
different release name, run `kubectl get deployments,services --namespace
openwork` to find the generated resource names.
Confirm that the workloads are ready:
```bash
kubectl get pods --namespace openwork
kubectl rollout status deployment/openwork-ee-den-api --namespace openwork
kubectl rollout status deployment/openwork-ee-den-web --namespace openwork
```
Inspect the rendered environment references without printing the Secret's
value:
```bash
kubectl describe deployment/openwork-ee-den-api --namespace openwork
kubectl describe deployment/openwork-ee-den-web --namespace openwork
```
Look for `DEN_OBSERVABILITY_BACKEND=otel`, distinct `OTEL_SERVICE_NAME` values,
the OTLP endpoint, and an `OTEL_EXPORTER_OTLP_HEADERS` reference to
`openwork-otel-headers`.
Generate a request that crosses both services. Keep this port-forward running:
```bash
kubectl port-forward service/openwork-ee-den-web 3005:3005 --namespace openwork
```
In another terminal:
```bash
curl --fail --silent --show-error \
http://127.0.0.1:3005/api/den/openapi.json >/dev/null
```
Your observability backend should show `openwork-den-web` and
`openwork-den-api`, with one connected trace for the request. Logs from both
services carry trace and span IDs. Den API also exports Hono request-duration
and active-request metrics.
### Endpoint and troubleshooting notes
- `observability.otel.endpoint` is a base endpoint. OpenWork appends
`/v1/traces`, `/v1/metrics`, and `/v1/logs`.
- Signal-specific endpoints are used exactly as written. Include the full
signal path, such as `https://collector.example.com/v1/traces`.
- Only OTLP HTTP/protobuf is supported. Port `4317` is normally OTLP gRPC and
will not work; use the HTTP receiver, usually port `4318`.
- The Secret must be in the OpenWork release namespace, and its key must match
`observability.otel.headers.key` exactly.
- A `401` or `403` exporter error usually means the token or header syntax is
wrong. A connection error usually means the endpoint is not reachable from
the pod or a NetworkPolicy blocks it.
- After changing an externally managed Secret, restart the deployments if your
secret controller does not trigger a rollout:
```bash
kubectl rollout restart deployment/openwork-ee-den-api --namespace openwork
kubectl rollout restart deployment/openwork-ee-den-web --namespace openwork
```
- For lower production trace volume, use
`tracesSampler: parentbased_traceidratio` with `tracesSamplerArg: "0.1"` to
sample approximately ten percent of root traces.
For Sentry runtime capture, configure the DSN directly or through an existing
Secret. Helm runtime pods intentionally do not receive `SENTRY_AUTH_TOKEN`,
`SENTRY_ORG`, `SENTRY_PROJECT`, or `SENTRY_URL`; those are build-time source-map
upload settings, not runtime settings.
```yaml
observability:
backend: sentry
sentry:
dsnSecret:
existingSecret: openwork-sentry-runtime
key: SENTRY_DSN
tracesSampleRate: "0.01"
environment: production
release: "2026.07.11"
```
Sentry Logs default to warning-and-error only through `SENTRY_LOG_LEVEL=warn`.
Set `SENTRY_LOG_LEVEL=info` only during short debugging windows if you need
successful request logs in Sentry; stdout JSON logs remain available either way.
Sentry source-map upload is build-time behavior. Helm configures runtime pods
after images already exist, so it cannot retroactively upload source maps for
Vercel or CI builds. Set `SENTRY_AUTH_TOKEN`, `SENTRY_ORG`, `SENTRY_PROJECT`,
and `SENTRY_URL` in the build environment that creates the image (for example,
Vercel project build environment variables), not in Helm values or the chart
ConfigMap. The generic published images cannot upload source maps after they
are built; build your own image with CI/BuildKit source-map secrets when you
need uploaded artifacts. `packaging/docker/Dockerfile.den-web` accepts optional
BuildKit secret IDs `sentry_auth_token`, `sentry_org`, `sentry_project`,
`sentry_url`, `sentry_release`, and `sentry_dist`; the EE image publish workflow
wires these IDs from GitHub Secrets when present.
## GitHub Connector
The GitHub repository connector uses a GitHub App. It is separate from GitHub
OAuth social sign-in. Follow the full setup guide in
[`packages/docs/start-here/github-connector-helm.mdx`](../../../packages/docs/start-here/github-connector-helm.mdx).
Use these public URLs when creating the GitHub App:
- Setup URL: `https://openwork.example.com/dashboard/integrations/github`
- Webhook URL: `https://api.openwork.example.com/v1/webhooks/connectors/github`
Then set the chart values:
```yaml
config:
githubConnector:
appId: "123456"
clientId: "Iv1.example"
secret:
values:
githubConnectorAppClientSecret: "github-app-client-secret-if-used"
githubConnectorAppPrivateKey: |-
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
githubConnectorAppWebhookSecret: "replace-with-the-github-webhook-secret"
```
The chart exposes these to Den API as:
- `GITHUB_CONNECTOR_APP_ID`
- `GITHUB_CONNECTOR_APP_CLIENT_ID`
- `GITHUB_CONNECTOR_APP_CLIENT_SECRET`
- `GITHUB_CONNECTOR_APP_PRIVATE_KEY`
- `GITHUB_CONNECTOR_APP_WEBHOOK_SECRET`
If `secret.create=false`, add the three secret-backed keys to the existing
Secret referenced by `secret.existingSecret`. The app ID and client ID come from
the chart ConfigMap.
## Transactional Email
Den API can send transactional email through SMTP. Configure the SMTP values in
the chart Secret:
```yaml
secret:
values:
emailFrom: "OpenWork <no-reply@example.com>"
smtpHost: "smtp.example.com"
smtpPort: "587"
smtpUser: "openwork@example.com"
smtpPass: "REPLACE_ME"
smtpSecure: "false"
```
These values are exposed to Den API as:
- `EMAIL_FROM`
- `SMTP_HOST`
- `SMTP_PORT`
- `SMTP_USER`
- `SMTP_PASS`
- `SMTP_SECURE`
If `secret.create=false`, add those keys to the existing Secret referenced by
`secret.existingSecret`. SMTP delivery requires both `EMAIL_FROM` and
`SMTP_HOST`; leave `smtpHost` blank only when SMTP-backed transactional email
should be disabled.
## Tenancy Mode
The chart defaults to a private single-org deployment:
```yaml
config:
tenancy:
mode: "single_org"
singleOrgName: "OpenWork"
singleOrgSlug: "default"
ownerEmails: "admin@example.com"
allowPublicSignup: "false"
requireEmailVerification: "false"
```
These values are exposed to both `den-api` and `den-web` as:
- `DEN_ORG_MODE`
- `DEN_SINGLE_ORG_NAME`
- `DEN_SINGLE_ORG_SLUG`
- `DEN_SINGLE_ORG_OWNER_EMAILS`
- `DEN_SINGLE_ORG_ALLOW_PUBLIC_SIGNUP`
- `DEN_REQUIRE_EMAIL_VERIFICATION`
In the implemented target state, blank or unset `DEN_ORG_MODE` is treated as `single_org`. The Helm chart sets it explicitly to make rendered manifests clear. Hosted or cloud-style multi-organization deployments should set:
```yaml
config:
tenancy:
mode: "multi_org"
requireEmailVerification: "true"
```
`config.tenancy.ownerEmails` controls who can claim ownership of the singleton deployment organization. `config.public.bootstrapAdminEmails` is separate: it seeds platform/admin allowlist access and does not by itself make a user the singleton organization owner.
## Initial Organization Setup
For self-hosted installs, configure the singleton organization before the first
user signs in:
```yaml
config:
tenancy:
mode: "single_org"
singleOrgName: "Acme"
singleOrgSlug: "acme"
ownerEmails: "admin@acme.com"
requireEmailVerification: "false"
public:
bootstrapAdminEmails: "admin@acme.com"
```
For releases that include initial-administrator bootstrap, inject the
release-documented one-time setup secret through the chart's Secret integration.
Do not put the setup code in Helm values, a ConfigMap, source control, logs, or a
PR. After the release is installed and the web host is reachable, open `/setup`,
enter an email configured in `config.tenancy.ownerEmails`, and verify it with the
one-time operator code. Den then creates the Better Auth account, creates or
claims the singleton organization identified by `singleOrgName` and
`singleOrgSlug`, grants owner and configured platform-admin access, and consumes
the setup claim. The code cannot be reused and public signup remains disabled.
Configuring `ownerEmails` or `bootstrapAdminEmails` does not create an account or
password. `ownerEmails` controls singleton-organization ownership;
`bootstrapAdminEmails` controls platform-admin allowlist access. Configure the
initial administrator in both lists when that person needs both roles.
Chart versions without the `/setup` route do not support this private bootstrap
flow. Upgrade to a release that includes initial-administrator bootstrap before
attempting first-user setup; entering the configured email on the normal sign-in
page cannot create the account and no default administrator password exists.
Later users are attached to the same singleton organization. They do not see an
organization creation step, and attempts to create another organization return a
single-org-mode error. If no eligible initial-administrator email is configured,
the private setup flow remains unavailable; it never falls back to allowing an
arbitrary first visitor to claim ownership.
For most production installs, use this first owner account as the break-glass
setup path, then configure SAML/OIDC SSO and SCIM from the organization
settings. Keep `bootstrapAdminEmails` aligned only if that same person should
also have platform/admin allowlist access; it is not a replacement for
`ownerEmails`.
After SAML/OIDC SSO is configured on the singleton organization, the auth
experience becomes SSO-only: root sign-in and sign-up show one "Continue with
SSO" action, other sign-in/sign-up entry points redirect there, and raw
email/password sign-in or sign-up requests are rejected by Den API.
## Internal Service URLs
By default, the chart wires internal services through Kubernetes DNS:
- `DEN_API_BASE=http://<release>-openwork-ee-den-api:8788`
- `DEN_AUTH_FALLBACK_BASE=http://<release>-openwork-ee-den-api:8788`
- `INFERENCE_PROXY_BASE_URL=http://<release>-openwork-ee-inference:8791` when `inference.enabled=true`
Override `config.internal.*` only when routing through a mesh, gateway, or external service.
## Den API Node Options
Set `config.denApiNodeOptions` to pass Node.js runtime flags to `den-api` through
`NODE_OPTIONS` when the container starts. The configured value is stored in the
chart ConfigMap as `DEN_API_NODE_OPTIONS` and defaults to an empty string.
Existing values files that set `denApi.env.NODE_OPTIONS` remain supported and
take precedence, so upgrading does not require changing that configuration.
```yaml
config:
denApiNodeOptions: "--max-old-space-size=4096"
```
`--use-openssl-ca` only changes how Node reads operating-system trust. It does
not create or mount a private CA bundle into the container; use `customCa` for
that.
## Service Exposure
Each service supports Kubernetes Service metadata and load balancer settings:
```yaml
denWeb:
service:
type: LoadBalancer
port: 443
loadBalancerClass: eks.amazonaws.com/nlb
loadBalancerSourceRanges:
- 203.0.113.0/24
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: arn:aws:acm:...
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
```
The same shape is available under `denApi.service` and `inference.service`.
`ingress.enabled=true` only emits Kubernetes `Ingress` resources; it does not
install an ingress controller.
## Config Rollouts
Den API, Den Web, and inference pods include checksums for the chart-managed
ConfigMap and Secret. Helm upgrades that change runtime config or secrets roll
the pods automatically so environment variables such as public origins, CORS
origins, and database URLs are refreshed.
## Isolated Networks
Use the published [Air-gapped deployment](../../../packages/docs/start-here/air-gapped-deployment.mdx)
and [Outbound network access](../../../packages/docs/start-here/outbound-network-access.mdx)
pages for customer-facing isolation and allowlist planning. The values below are
the chart-level controls.
The chart disables external password breach screening by default so isolated self-hosted installs do not depend on the Have I Been Pwned Pwned Passwords range API. If your deployment has approved outbound access and you want password creation and reset to reject known-compromised passwords through that service, enable it:
```yaml
config:
auth:
passwordBreachScreeningEnabled: "true"
```
Local sign-in lockout protections stay enabled either way.
Den API also protects External MCP connection URLs with an SSRF guard. On a
hosted multi-tenant deployment, someone who can add an MCP connection could
otherwise make Den fetch localhost, private-network services, or cloud metadata
addresses from Den's network position.
If a legitimate internal MCP server is blocked in an isolated private-network
deployment, the diagnostic code is `MCP_URL_BLOCKED` and the operator-facing
message is: "Use a public HTTPS MCP URL or change the deployment's
private-network policy through security review."
To allow private-address MCP servers for Den API, enable:
```yaml
config:
public:
allowPrivateMcpUrls: "1"
```
This disables SSRF protection for external MCP connection URLs. Enable it only
on a deployment where Den's network position and the set of people who can add
MCP connections are both trusted.
## Migrations
The migration Job runs as a Helm `pre-install,pre-upgrade` hook by default:
```yaml
migrations:
enabled: true
hook: true
hookDeletePolicy: before-hook-creation,hook-succeeded
command:
- node
args:
- /app/ee/packages/den-db/dist/scripts/bootstrap.js
```
The default hook executes the precompiled Den DB bootstrap runner already built into the Den API image. On a completely empty database it applies the build-time current-schema SQL snapshot, records the committed migrations as the baseline, then runs pending migrations with Drizzle ORM. On an existing schema without a Drizzle ledger, it records the baseline before migrating.
For retained-log troubleshooting, temporarily disable hook behavior and reduce
retries:
```yaml
migrations:
enabled: true
hook: false
backoffLimit: 0
```
The hook Job currently renders `DATABASE_URL` and `DEN_DB_ENCRYPTION_KEY` into
the Job environment when `secret.create=true`, because pre-install hooks run
before normal chart resources. Avoid sharing `kubectl describe job` output
without redacting secrets.
## Install links
The migration Job creates the `install_link` and `desktop_connect_grant` tables
automatically when `migrations.enabled=true`. Hosted deployments that enable
install-link gating must opt organizations in through `/admin`; self-hosted
deployments default to enabled. See the
[operator guide](../../../docs/org-install-links.md) and the published
[Installer delivery](../../../packages/docs/start-here/installer-delivery.mdx)
page.
Optional installer artifact values:
```yaml
config:
public:
installerReleaseTag: "v0.17.9"
installerReleaseRepo: "different-ai/openwork"
installerArtifacts:
enabled: true
existingClaim: openwork-desktop-artifacts
mountPath: /var/lib/openwork/installer-artifacts
```
Use either `installerArtifacts.existingClaim` or `installerArtifacts.hostPath`,
not both.
### Guided desktop setup
The organization download page hands the normal OpenWork app its Den
configuration in an explicit second step. The default is a short-lived,
single-use HTTPS exchange and needs no key configuration:
```yaml
config:
public:
connectLinkMode: exchange
```
Den validates the install token and then either:
- streams the standard installer already mounted at
`installerArtifacts.mountPath`; or
- redirects the browser directly to the exact configured GitHub release asset.
Den does not download, cache, wrap, or ZIP GitHub artifacts. The organization
setup stays in the **Open OpenWork** deep-link step after installation.
For an optional signed handoff, explicitly select signed mode and configure a
dedicated Ed25519 key whose public key is already trusted by the desktop build:
```yaml
config:
public:
connectLinkMode: signed
connectLinkKeyId: "owc-2026-07"
secret:
values:
connectLinkPrivateKey: |-
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
```
For an existing Secret, put the private key under the key named by
`secret.keys.connectLinkPrivateKey` (default `DEN_CONNECT_LINK_PRIVATE_KEY`).
`scripts/generate-connect-link-keypair.mjs` can generate a pair, but a standard
desktop build will reject it until the matching public key ships in that build.
For a semi-air-gapped deployment, mount these normal release filenames (where
`<version>` is `installerReleaseTag` without its leading `v`):
- `openwork-mac-arm64-<version>.dmg`
- `openwork-mac-x64-<version>.dmg`
- `openwork-win-x64-<version>.exe`
- `openwork-linux-x86_64-<version>.AppImage`
- `openwork-linux-arm64-<version>.AppImage`
Without mounted artifacts, client networks must permit the configured GitHub
release URL and GitHub's redirected release-asset host. With mounted artifacts,
the browser only talks to Den. Use a shared read-only PVC when
`denApi.replicaCount` is greater than one. Connection grants are stored and
consumed in MySQL, so the guided flow is safe when preview and acceptance land
on different API replicas.
## Health Probes
The chart uses the existing service health endpoints:
- `den-api`: `GET /health`
- `den-web`: `GET /api/health`
- `inference`: `GET /health`
Readiness probes use dependency-aware endpoints:
- `den-api`: `GET /ready`
- `den-web`: `GET /api/ready`
- `inference`: `GET /ready`
## Worker Provisioning Recovery
`den-api` periodically reconciles cloud workers that remain in `provisioning` beyond `config.provisioner.reconcileStaleMs`. This lets a replacement pod resume provisioning after a crash. Keep `denApi.replicaCount: 1` unless your worker provider operations are idempotent or you add external leader election.
@@ -0,0 +1,88 @@
# AWS EKS Auto Mode HTTP smoke-test values for a first OpenWork EE install when
# DNS and ACM TLS are not ready yet.
#
# Copy this file, replace every REPLACE_* value, and install with:
#
# helm upgrade --install openwork-ee oci://ghcr.io/different-ai/charts/openwork-ee \
# --version REPLACE_OPENWORK_VERSION \
# --namespace openwork-ee \
# --create-namespace \
# -f values.aws-http-smoke.yaml
#
# This creates two public AWS Network Load Balancers on clear HTTP ports. After
# the first successful install, replace the pending origins with the real NLB
# hostnames, run helm upgrade again, and then validate browser auth. Prefer the
# HTTPS/ACM example for production.
image:
tag: "REPLACE_OPENWORK_VERSION"
pullPolicy: IfNotPresent
config:
tenancy:
mode: single_org
singleOrgName: "REPLACE_COMPANY_NAME"
singleOrgSlug: "REPLACE_COMPANY_SLUG"
ownerEmails: "REPLACE_ADMIN_EMAIL"
allowPublicSignup: "false"
requireEmailVerification: "false"
auth:
passwordBreachScreeningEnabled: "false"
public:
webOrigin: "http://pending-web-nlb:3005"
apiOrigin: "http://pending-api-nlb:8788"
mcpResourceUrl: "http://pending-api-nlb:8788/mcp"
mcpClaimNamespace: "http://pending-web-nlb:3005"
desktopDenBaseUrl: "http://pending-web-nlb:3005"
corsOrigins: "http://pending-web-nlb:3005,http://pending-api-nlb:8788"
betterAuthTrustedOrigins: "http://pending-web-nlb:3005"
webAppHosts: "pending-web-nlb"
bootstrapAdminEmails: "REPLACE_ADMIN_EMAIL"
authCallbackUrl: "http://pending-web-nlb:3005"
provisioner:
mode: stub
secret:
create: true
values:
# RDS MySQL private-network smoke path. sslaccept=accept preserves TLS but
# does not require the RDS CA chain to be mounted into the image.
databaseUrl: "mysql://openwork:REPLACE_DB_PASSWORD@REPLACE_RDS_ENDPOINT:3306/openwork_den?sslaccept=accept"
# Generate with: openssl rand -base64 48
betterAuthSecret: "REPLACE_BETTER_AUTH_SECRET"
# Generate separately with: openssl rand -base64 48
denDbEncryptionKey: "REPLACE_DEN_DB_ENCRYPTION_KEY"
denApi:
replicaCount: 1
service:
type: LoadBalancer
port: 8788
loadBalancerClass: eks.amazonaws.com/nlb
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: HTTP
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /ready
denWeb:
replicaCount: 1
service:
type: LoadBalancer
port: 3005
loadBalancerClass: eks.amazonaws.com/nlb
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: HTTP
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /api/ready
inference:
enabled: false
migrations:
enabled: true
hook: true
ingress:
enabled: false
@@ -0,0 +1,94 @@
# AWS EKS Auto Mode starter values for a first OpenWork EE self-host install.
#
# Copy this file, replace every REPLACE_* value, and install with:
#
# helm upgrade --install openwork-ee oci://ghcr.io/different-ai/charts/openwork-ee \
# --version REPLACE_OPENWORK_VERSION \
# --namespace openwork-ee \
# --create-namespace \
# -f values.aws.yaml
#
# This example exposes Den Web and Den API through separate AWS Network Load
# Balancers created from Kubernetes Services. Use it for the first AWS smoke
# test and for installs where separate web/API hostnames are acceptable.
image:
tag: "REPLACE_OPENWORK_VERSION"
pullPolicy: IfNotPresent
config:
tenancy:
mode: single_org
singleOrgName: "REPLACE_COMPANY_NAME"
singleOrgSlug: "REPLACE_COMPANY_SLUG"
ownerEmails: "REPLACE_ADMIN_EMAIL"
allowPublicSignup: "false"
requireEmailVerification: "false"
auth:
passwordBreachScreeningEnabled: "false"
public:
webOrigin: "https://REPLACE_WEB_HOST"
# Split-origin compatibility override for this two-load-balancer example.
# Leave blank in single-origin deployments so Den derives it from webOrigin.
apiOrigin: "https://REPLACE_API_HOST"
mcpResourceUrl: "https://REPLACE_API_HOST/mcp"
mcpClaimNamespace: "https://REPLACE_WEB_HOST"
desktopDenBaseUrl: ""
corsOrigins: "https://REPLACE_WEB_HOST,https://REPLACE_API_HOST"
betterAuthTrustedOrigins: "https://REPLACE_WEB_HOST"
webAppHosts: "REPLACE_WEB_HOST"
bootstrapAdminEmails: "REPLACE_ADMIN_EMAIL"
authCallbackUrl: "https://REPLACE_WEB_HOST"
provisioner:
mode: stub
secret:
create: true
values:
# RDS MySQL private-network smoke path. sslaccept=accept preserves TLS but
# does not require the RDS CA chain to be mounted into the image.
databaseUrl: "mysql://openwork:REPLACE_DB_PASSWORD@REPLACE_RDS_ENDPOINT:3306/openwork_den?sslaccept=accept"
# Generate with: openssl rand -base64 48
betterAuthSecret: "REPLACE_BETTER_AUTH_SECRET"
# Generate separately with: openssl rand -base64 48
denDbEncryptionKey: "REPLACE_DEN_DB_ENCRYPTION_KEY"
# One-time /setup code. Generate a unique value and keep it out of source control.
initialAdminBootstrapCode: "REPLACE_BOOTSTRAP_CODE"
denApi:
replicaCount: 1
service:
type: LoadBalancer
port: 443
loadBalancerClass: eks.amazonaws.com/nlb
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: HTTP
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /ready
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: REPLACE_ACM_CERT_ARN
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
denWeb:
replicaCount: 1
service:
type: LoadBalancer
port: 443
loadBalancerClass: eks.amazonaws.com/nlb
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: HTTP
service.beta.kubernetes.io/aws-load-balancer-healthcheck-path: /api/ready
service.beta.kubernetes.io/aws-load-balancer-ssl-cert: REPLACE_ACM_CERT_ARN
service.beta.kubernetes.io/aws-load-balancer-ssl-ports: "443"
inference:
enabled: false
migrations:
enabled: true
hook: true
ingress:
enabled: false
@@ -0,0 +1,99 @@
# Azure AKS starter values for a first OpenWork EE self-host install.
#
# Follow docs/azure-aks-helm.md to create the VNet, AKS subnet, delegated
# MySQL subnet, AKS cluster, and Azure Database for MySQL Flexible Server.
# Then copy this file, replace every REPLACE_* value, and install with:
#
# helm upgrade --install openwork-ee oci://ghcr.io/different-ai/charts/openwork-ee \
# --version REPLACE_OPENWORK_VERSION \
# --namespace openwork-ee \
# --create-namespace \
# -f values.azure.yaml
#
# This example exposes Den Web and Den API through the AKS application routing
# add-on's managed NGINX Ingress controller. Use it for production-like Azure
# tests where browser auth and SSO need stable HTTPS hostnames.
image:
tag: "REPLACE_OPENWORK_VERSION"
pullPolicy: IfNotPresent
config:
tenancy:
mode: single_org
singleOrgName: "REPLACE_COMPANY_NAME"
singleOrgSlug: "REPLACE_COMPANY_SLUG"
ownerEmails: "REPLACE_ADMIN_EMAIL"
allowPublicSignup: "false"
requireEmailVerification: "false"
auth:
passwordBreachScreeningEnabled: "false"
public:
webOrigin: "https://REPLACE_WEB_HOST"
# Split-origin compatibility override for this two-host ingress example.
# Leave blank in single-origin deployments so Den derives it from webOrigin.
apiOrigin: "https://REPLACE_API_HOST"
mcpResourceUrl: "https://REPLACE_API_HOST/mcp"
mcpClaimNamespace: "https://REPLACE_WEB_HOST"
desktopDenBaseUrl: ""
corsOrigins: "https://REPLACE_WEB_HOST,https://REPLACE_API_HOST"
betterAuthTrustedOrigins: "https://REPLACE_WEB_HOST"
webAppHosts: "REPLACE_WEB_HOST"
bootstrapAdminEmails: "REPLACE_ADMIN_EMAIL"
authCallbackUrl: "https://REPLACE_WEB_HOST"
provisioner:
mode: stub
secret:
create: true
values:
# Azure Database for MySQL Flexible Server private-network smoke path.
# sslaccept=accept preserves TLS without requiring a mounted CA bundle.
databaseUrl: "mysql://openwork:REPLACE_DB_PASSWORD@REPLACE_MYSQL_SERVER_NAME.mysql.database.azure.com:3306/openwork_den?sslaccept=accept"
# Generate with: openssl rand -base64 48
betterAuthSecret: "REPLACE_BETTER_AUTH_SECRET"
# Generate separately with: openssl rand -base64 48
denDbEncryptionKey: "REPLACE_DEN_DB_ENCRYPTION_KEY"
# One-time /setup code. Generate a unique value and keep it out of source control.
initialAdminBootstrapCode: "REPLACE_BOOTSTRAP_CODE"
# Optional SMTP settings for invitations and other transactional email.
# SMTP delivery requires both emailFrom and smtpHost.
emailFrom: "OpenWork <no-reply@example.com>"
smtpHost: ""
smtpPort: "587"
smtpUser: ""
smtpPass: ""
smtpSecure: "false"
denApi:
replicaCount: 1
service:
type: ClusterIP
port: 8788
denWeb:
replicaCount: 1
service:
type: ClusterIP
port: 3005
inference:
enabled: false
migrations:
enabled: true
hook: true
ingress:
enabled: true
className: webapprouting.kubernetes.azure.com
web:
host: REPLACE_WEB_HOST
api:
enabled: true
host: REPLACE_API_HOST
tls:
- secretName: openwork-ee-tls
hosts:
- REPLACE_WEB_HOST
- REPLACE_API_HOST
@@ -0,0 +1,94 @@
# Google Cloud GKE starter values for a first OpenWork EE self-host install.
#
# Copy this file, replace every REPLACE_* value, and install with:
#
# helm upgrade --install openwork-ee oci://ghcr.io/different-ai/charts/openwork-ee \
# --version REPLACE_OPENWORK_VERSION \
# --namespace openwork-ee \
# --create-namespace \
# -f values.gcp.yaml
#
# This example exposes Den Web and Den API through GKE Ingress with a reserved
# global IP and Google-managed certificate. Apply the ManagedCertificate and
# BackendConfig manifests from docs/gcp-gke-helm.md before installing.
image:
tag: "REPLACE_OPENWORK_VERSION"
pullPolicy: IfNotPresent
config:
tenancy:
mode: single_org
singleOrgName: "REPLACE_COMPANY_NAME"
singleOrgSlug: "REPLACE_COMPANY_SLUG"
ownerEmails: "REPLACE_ADMIN_EMAIL"
allowPublicSignup: "false"
requireEmailVerification: "false"
auth:
passwordBreachScreeningEnabled: "false"
public:
webOrigin: "https://REPLACE_WEB_HOST"
# Split-origin compatibility override for this two-host ingress example.
# Leave blank in single-origin deployments so Den derives it from webOrigin.
apiOrigin: "https://REPLACE_API_HOST"
mcpResourceUrl: "https://REPLACE_API_HOST/mcp"
mcpClaimNamespace: "https://REPLACE_WEB_HOST"
desktopDenBaseUrl: ""
corsOrigins: "https://REPLACE_WEB_HOST,https://REPLACE_API_HOST"
betterAuthTrustedOrigins: "https://REPLACE_WEB_HOST"
webAppHosts: "REPLACE_WEB_HOST"
bootstrapAdminEmails: "REPLACE_ADMIN_EMAIL"
authCallbackUrl: "https://REPLACE_WEB_HOST"
provisioner:
mode: stub
secret:
create: true
values:
# Direct private IP Cloud SQL example. The current chart does not inject
# Cloud SQL Auth Proxy sidecars, so use a private IP reachable from GKE.
databaseUrl: "mysql://openwork:REPLACE_DB_PASSWORD@REPLACE_CLOUD_SQL_PRIVATE_IP:3306/openwork_den"
# Generate with: openssl rand -base64 48
betterAuthSecret: "REPLACE_BETTER_AUTH_SECRET"
# Generate separately with: openssl rand -base64 48
denDbEncryptionKey: "REPLACE_DEN_DB_ENCRYPTION_KEY"
# One-time /setup code. Generate a unique value and keep it out of source control.
initialAdminBootstrapCode: "REPLACE_BOOTSTRAP_CODE"
denApi:
replicaCount: 1
service:
type: ClusterIP
port: 8788
annotations:
cloud.google.com/backend-config: '{"default": "openwork-ee-den-api-backend"}'
cloud.google.com/neg: '{"ingress": true}'
denWeb:
replicaCount: 1
service:
type: ClusterIP
port: 3005
annotations:
cloud.google.com/backend-config: '{"default": "openwork-ee-den-web-backend"}'
cloud.google.com/neg: '{"ingress": true}'
inference:
enabled: false
migrations:
enabled: true
hook: true
ingress:
enabled: true
className: ""
annotations:
kubernetes.io/ingress.class: gce
kubernetes.io/ingress.global-static-ip-name: REPLACE_GLOBAL_STATIC_IP_NAME
networking.gke.io/managed-certificates: REPLACE_MANAGED_CERTIFICATE_NAME
web:
host: REPLACE_WEB_HOST
api:
enabled: true
host: REPLACE_API_HOST
@@ -0,0 +1,26 @@
OpenWork EE has been rendered with:
DEN_BASE_URL={{ .Values.config.public.webOrigin }}
DEN_BASE_URL is now the primary public Den URL. den-api derives Better Auth,
CORS/trusted origins, web-app hosts, API defaults, and MCP resource defaults
from this value unless compatibility overrides are set.
{{- $overrides := list -}}
{{- if .Values.config.public.apiOrigin }}{{- $overrides = append $overrides (printf "config.public.apiOrigin=%s" .Values.config.public.apiOrigin) -}}{{- end -}}
{{- if .Values.config.public.mcpResourceUrl }}{{- $overrides = append $overrides (printf "config.public.mcpResourceUrl=%s" .Values.config.public.mcpResourceUrl) -}}{{- end -}}
{{- if .Values.config.public.desktopDenBaseUrl }}{{- $overrides = append $overrides (printf "config.public.desktopDenBaseUrl=%s" .Values.config.public.desktopDenBaseUrl) -}}{{- end -}}
{{- if .Values.config.public.corsOrigins }}{{- $overrides = append $overrides (printf "config.public.corsOrigins=%s" .Values.config.public.corsOrigins) -}}{{- end -}}
{{- if .Values.config.public.betterAuthTrustedOrigins }}{{- $overrides = append $overrides (printf "config.public.betterAuthTrustedOrigins=%s" .Values.config.public.betterAuthTrustedOrigins) -}}{{- end -}}
{{- if .Values.config.public.webAppHosts }}{{- $overrides = append $overrides (printf "config.public.webAppHosts=%s" .Values.config.public.webAppHosts) -}}{{- end -}}
{{- if gt (len $overrides) 0 }}
Compatibility URL overrides are still set:
{{- range $overrides }}
- {{ . }}
{{- end }}
Keep these only for a deliberate split-origin/migration topology. For the
single-origin recommended deployment, clear them so Den derives the values from
config.public.webOrigin.
{{- end }}
@@ -0,0 +1,259 @@
{{- define "openwork-ee.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "openwork-ee.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "openwork-ee.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "openwork-ee.selectorLabels" -}}
app.kubernetes.io/name: {{ include "openwork-ee.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "openwork-ee.labels" -}}
helm.sh/chart: {{ include "openwork-ee.chart" . }}
{{ include "openwork-ee.selectorLabels" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "openwork-ee.componentSelectorLabels" -}}
{{ include "openwork-ee.selectorLabels" .root }}
app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{- define "openwork-ee.componentLabels" -}}
{{ include "openwork-ee.labels" .root }}
app.kubernetes.io/component: {{ .component }}
{{- end -}}
{{- define "openwork-ee.configName" -}}
{{ include "openwork-ee.fullname" . }}-config
{{- end -}}
{{- define "openwork-ee.allowPrivateMcpUrls" -}}
{{- $value := .Values.config.public.allowPrivateMcpUrls | default "" | toString | trim | lower -}}
{{- if eq $value "1" -}}
1
{{- else if or (eq $value "") (eq $value "0") (eq $value "false") -}}
{{- else -}}
{{- fail "config.public.allowPrivateMcpUrls must be blank, 0, false, or \"1\"" -}}
{{- end -}}
{{- end -}}
{{- define "openwork-ee.secretName" -}}
{{- if .Values.secret.existingSecret -}}
{{- .Values.secret.existingSecret -}}
{{- else -}}
{{- include "openwork-ee.fullname" . }}-secret
{{- end -}}
{{- end -}}
{{- define "openwork-ee.denApiServiceName" -}}
{{ include "openwork-ee.fullname" . }}-den-api
{{- end -}}
{{- define "openwork-ee.denWebServiceName" -}}
{{ include "openwork-ee.fullname" . }}-den-web
{{- end -}}
{{- define "openwork-ee.inferenceServiceName" -}}
{{ include "openwork-ee.fullname" . }}-inference
{{- end -}}
{{- define "openwork-ee.denApiInternalUrl" -}}
{{- default (printf "http://%s:%v" (include "openwork-ee.denApiServiceName" .) .Values.denApi.service.port) .Values.config.internal.apiBaseUrl -}}
{{- end -}}
{{- define "openwork-ee.authFallbackInternalUrl" -}}
{{- default (include "openwork-ee.denApiInternalUrl" .) .Values.config.internal.authFallbackBaseUrl -}}
{{- end -}}
{{- define "openwork-ee.inferenceInternalUrl" -}}
{{- default (printf "http://%s:%v" (include "openwork-ee.inferenceServiceName" .) .Values.inference.service.port) .Values.config.internal.inferenceProxyBaseUrl -}}
{{- end -}}
{{- define "openwork-ee.customCa.mountPath" -}}
/etc/openwork/custom-ca
{{- end -}}
{{- define "openwork-ee.customCa.filePath" -}}
{{ include "openwork-ee.customCa.mountPath" . }}/ca-bundle.pem
{{- end -}}
{{- define "openwork-ee.customCa.validate" -}}
{{- if .Values.customCa.enabled -}}
{{- if and .Values.customCa.existingSecret .Values.customCa.existingConfigMap -}}
{{- fail "customCa.existingSecret and customCa.existingConfigMap are mutually exclusive when customCa.enabled=true" -}}
{{- end -}}
{{- if not (or .Values.customCa.existingSecret .Values.customCa.existingConfigMap) -}}
{{- fail "customCa.existingSecret or customCa.existingConfigMap is required when customCa.enabled=true" -}}
{{- end -}}
{{- if not .Values.customCa.key -}}
{{- fail "customCa.key is required when customCa.enabled=true" -}}
{{- end -}}
{{- if hasKey .Values.denApi.env "NODE_EXTRA_CA_CERTS" -}}
{{- fail "denApi.env.NODE_EXTRA_CA_CERTS conflicts with customCa.enabled=true; remove it and use customCa instead" -}}
{{- end -}}
{{- if hasKey .Values.denWeb.env "NODE_EXTRA_CA_CERTS" -}}
{{- fail "denWeb.env.NODE_EXTRA_CA_CERTS conflicts with customCa.enabled=true; remove it and use customCa instead" -}}
{{- end -}}
{{- if hasKey .Values.inference.env "NODE_EXTRA_CA_CERTS" -}}
{{- fail "inference.env.NODE_EXTRA_CA_CERTS conflicts with customCa.enabled=true; remove it and use customCa instead" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "openwork-ee.customCa.volume" -}}
- name: custom-ca
{{- if .Values.customCa.existingSecret }}
secret:
secretName: {{ .Values.customCa.existingSecret | quote }}
items:
- key: {{ .Values.customCa.key | quote }}
path: ca-bundle.pem
{{- else }}
configMap:
name: {{ .Values.customCa.existingConfigMap | quote }}
items:
- key: {{ .Values.customCa.key | quote }}
path: ca-bundle.pem
{{- end }}
{{- end -}}
{{- define "openwork-ee.customCa.volumeMount" -}}
- name: custom-ca
mountPath: {{ include "openwork-ee.customCa.mountPath" . | quote }}
readOnly: true
{{- end -}}
{{- define "openwork-ee.customCa.env" -}}
- name: NODE_EXTRA_CA_CERTS
value: {{ include "openwork-ee.customCa.filePath" . | quote }}
{{- end -}}
{{- define "openwork-ee.observabilityBackend" -}}
{{- $backend := default "none" .Values.observability.backend -}}
{{- if not (has $backend (list "none" "otel" "sentry")) -}}
{{- fail "observability.backend must be one of none, otel, sentry" -}}
{{- end -}}
{{- $backend -}}
{{- end -}}
{{- define "openwork-ee.observabilityOtelExporter" -}}
{{- $exporter := default "otlp" .value -}}
{{- if not (has $exporter (list "otlp" "none")) -}}
{{- fail (printf "observability.otel.exporters.%s must be otlp or none" .signal) -}}
{{- end -}}
{{- $exporter -}}
{{- end -}}
{{- define "openwork-ee.observabilityOtelSampler" -}}
{{- $sampler := default "parentbased_always_on" . -}}
{{- if not (has $sampler (list "always_on" "always_off" "traceidratio" "parentbased_always_on" "parentbased_always_off" "parentbased_traceidratio")) -}}
{{- fail "observability.otel.tracesSampler must be a standard OpenTelemetry sampler" -}}
{{- end -}}
{{- $sampler -}}
{{- end -}}
{{- define "openwork-ee.observabilityEnv" -}}
{{- $root := .root -}}
{{- $serviceName := .serviceName -}}
{{- $backend := include "openwork-ee.observabilityBackend" $root -}}
{{- $otel := $root.Values.observability.otel -}}
{{- $sentry := $root.Values.observability.sentry -}}
- name: DEN_OBSERVABILITY_BACKEND
value: {{ $backend | quote }}
- name: OTEL_SERVICE_NAME
value: {{ $serviceName | quote }}
{{- if eq $backend "otel" }}
{{- $otelSampler := include "openwork-ee.observabilityOtelSampler" $otel.tracesSampler -}}
{{- $otelProtocol := default "http/protobuf" $otel.protocol -}}
{{- if ne $otelProtocol "http/protobuf" }}
{{- fail "observability.otel.protocol must be http/protobuf" -}}
{{- end }}
- name: OTEL_EXPORTER_OTLP_PROTOCOL
value: {{ $otelProtocol | quote }}
{{- with $otel.endpoint }}
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: {{ . | quote }}
{{- end }}
{{- with $otel.tracesEndpoint }}
- name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
value: {{ . | quote }}
{{- end }}
{{- with $otel.metricsEndpoint }}
- name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT
value: {{ . | quote }}
{{- end }}
{{- with $otel.logsEndpoint }}
- name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT
value: {{ . | quote }}
{{- end }}
- name: OTEL_TRACES_EXPORTER
value: {{ include "openwork-ee.observabilityOtelExporter" (dict "signal" "traces" "value" $otel.exporters.traces) | quote }}
- name: OTEL_METRICS_EXPORTER
value: {{ include "openwork-ee.observabilityOtelExporter" (dict "signal" "metrics" "value" $otel.exporters.metrics) | quote }}
- name: OTEL_LOGS_EXPORTER
value: {{ include "openwork-ee.observabilityOtelExporter" (dict "signal" "logs" "value" $otel.exporters.logs) | quote }}
- name: OTEL_TRACES_SAMPLER
value: {{ $otelSampler | quote }}
{{- if has $otelSampler (list "traceidratio" "parentbased_traceidratio") }}
- name: OTEL_TRACES_SAMPLER_ARG
value: {{ default "1" $otel.tracesSamplerArg | quote }}
{{- else if and $otel.tracesSamplerArg (ne (toString $otel.tracesSamplerArg) "1") }}
{{- fail "observability.otel.tracesSamplerArg is only supported for traceidratio samplers" -}}
{{- end }}
{{- with $otel.headers.existingSecret }}
- name: OTEL_EXPORTER_OTLP_HEADERS
valueFrom:
secretKeyRef:
name: {{ . | quote }}
key: {{ $otel.headers.key | quote }}
{{- end }}
{{- else if eq $backend "sentry" }}
{{- if and $sentry.dsn $sentry.dsnSecret.existingSecret }}
{{- fail "observability.sentry.dsn and observability.sentry.dsnSecret.existingSecret are mutually exclusive" -}}
{{- end }}
{{- if not (or $sentry.dsn $sentry.dsnSecret.existingSecret) }}
{{- fail "observability.sentry.dsn or observability.sentry.dsnSecret.existingSecret is required when observability.backend=sentry" -}}
{{- end }}
- name: SENTRY_DSN
{{- if $sentry.dsn }}
value: {{ $sentry.dsn | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ $sentry.dsnSecret.existingSecret | quote }}
key: {{ $sentry.dsnSecret.key | quote }}
{{- end }}
- name: SENTRY_TRACES_SAMPLE_RATE
value: {{ $sentry.tracesSampleRate | quote }}
{{- with $sentry.environment }}
- name: SENTRY_ENVIRONMENT
value: {{ . | quote }}
{{- end }}
{{- with $sentry.release }}
- name: SENTRY_RELEASE
value: {{ . | quote }}
{{- end }}
{{- with $sentry.dist }}
- name: SENTRY_DIST
value: {{ . | quote }}
{{- end }}
{{- end }}
{{- end -}}
@@ -0,0 +1,200 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "openwork-ee.configName" . }}
labels:
{{- include "openwork-ee.labels" . | nindent 4 }}
data:
CI: "true"
NODE_ENV: "production"
OPENWORK_DEV_MODE: {{ .Values.config.openworkDevMode | quote }}
DEN_API_NODE_OPTIONS: {{ .Values.config.denApiNodeOptions | quote }}
# SECURITY: Enables non-local redis:// DATABASE_REDIS_URL values only for
# trusted private networks. Keep false for public or cross-network Redis;
# prefer rediss:// whenever the provider supports TLS.
DATABASE_REDIS_ALLOW_INSECURE_INTERNAL: {{ ternary "1" "0" .Values.redis.allowInsecureInternal | quote }}
DB_MODE: {{ .Values.config.databaseMode | quote }}
DEN_ORG_MODE: {{ .Values.config.tenancy.mode | quote }}
DEN_SINGLE_ORG_NAME: {{ .Values.config.tenancy.singleOrgName | quote }}
DEN_SINGLE_ORG_SLUG: {{ .Values.config.tenancy.singleOrgSlug | quote }}
DEN_SINGLE_ORG_OWNER_EMAILS: {{ .Values.config.tenancy.ownerEmails | quote }}
DEN_SINGLE_ORG_ALLOW_PUBLIC_SIGNUP: {{ .Values.config.tenancy.allowPublicSignup | quote }}
DEN_REQUIRE_EMAIL_VERIFICATION: {{ .Values.config.tenancy.requireEmailVerification | quote }}
DEN_PASSWORD_BREACH_SCREENING_ENABLED: {{ .Values.config.auth.passwordBreachScreeningEnabled | quote }}
GITHUB_CONNECTOR_APP_ID: {{ .Values.config.githubConnector.appId | quote }}
GITHUB_CONNECTOR_APP_CLIENT_ID: {{ .Values.config.githubConnector.clientId | quote }}
DEN_BASE_URL: {{ .Values.config.public.webOrigin | quote }}
{{ if .Values.config.public.webOrigin }}
# Compatibility alias for older Den builds; current den-api derives it from DEN_BASE_URL.
BETTER_AUTH_URL: {{ .Values.config.public.webOrigin | quote }}
{{ end }}
{{ if .Values.config.public.mcpResourceUrl }}
DEN_MCP_RESOURCE_URL: {{ .Values.config.public.mcpResourceUrl | quote }}
{{ end }}
# Extra MCP resource URLs beyond the DEN_API_PUBLIC_URL API origin and web-app defaults.
{{ if .Values.config.public.mcpAdditionalResources }}
DEN_MCP_ADDITIONAL_RESOURCES: {{ .Values.config.public.mcpAdditionalResources | quote }}
{{ end }}
DEN_MCP_CLAIM_NAMESPACE: {{ .Values.config.public.mcpClaimNamespace | quote }}
{{ if .Values.config.public.betterAuthTrustedOrigins }}
DEN_BETTER_AUTH_TRUSTED_ORIGINS: {{ .Values.config.public.betterAuthTrustedOrigins | quote }}
{{ end }}
{{ if .Values.config.public.betterAuthCookieDomain }}
DEN_BETTER_AUTH_COOKIE_DOMAIN: {{ .Values.config.public.betterAuthCookieDomain | quote }}
{{ end }}
{{ if .Values.config.public.webAppHosts }}
DEN_WEB_APP_HOSTS: {{ .Values.config.public.webAppHosts | quote }}
{{ end }}
DEN_BOOTSTRAP_ADMIN_EMAILS: {{ .Values.config.public.bootstrapAdminEmails | quote }}
DEN_INSTALL_LINKS_GATING_ENABLED: {{ .Values.config.public.installLinksGatingEnabled | quote }}
DEN_AUTOMATIONS_ENABLED: {{ .Values.config.public.automationsEnabled | quote }}
DEN_DASHBOARDS_ENABLED: {{ .Values.config.public.dashboardsEnabled | quote }}
DEN_OPENWORK_WEB_ENABLED: {{ .Values.config.public.openworkWebEnabled | quote }}
DEN_AUTOMATIONS_RUNTIME_ENABLED: {{ .Values.config.public.automationsRuntimeEnabled | quote }}
DEN_GENERATED_ARTIFACT_VIEWS_ENABLED: {{ .Values.config.public.generatedArtifactViewsEnabled | quote }}
DEN_CONNECT_LINK_MODE: {{ .Values.config.public.connectLinkMode | quote }}
{{ if .Values.config.public.connectLinkKeyId }}
DEN_CONNECT_LINK_KEY_ID: {{ .Values.config.public.connectLinkKeyId | quote }}
{{ end }}
{{ if .Values.config.public.corsOrigins }}
CORS_ORIGINS: {{ .Values.config.public.corsOrigins | quote }}
DEN_CORS_ORIGINS: {{ .Values.config.public.corsOrigins | quote }}
{{ end }}
{{ if .Values.config.public.apiOrigin }}
DEN_API_PUBLIC_URL: {{ .Values.config.public.apiOrigin | quote }}
{{ end }}
{{ if .Values.config.public.desktopDenBaseUrl }}
DEN_DESKTOP_DEN_BASE_URL: {{ .Values.config.public.desktopDenBaseUrl | quote }}
{{ end }}
{{ if .Values.config.public.installerReleaseTag }}
OPENWORK_INSTALLER_RELEASE_TAG: {{ .Values.config.public.installerReleaseTag | quote }}
{{ end }}
{{ if .Values.config.public.installerReleaseRepo }}
OPENWORK_INSTALLER_RELEASE_REPO: {{ .Values.config.public.installerReleaseRepo | quote }}
{{ end }}
PROVISIONER_MODE: {{ .Values.config.provisioner.mode | quote }}
WORKER_PROVISIONING_RECONCILE_INTERVAL_MS: {{ .Values.config.provisioner.reconcileIntervalMs | quote }}
WORKER_PROVISIONING_RECONCILE_STALE_MS: {{ .Values.config.provisioner.reconcileStaleMs | quote }}
WORKER_PROVISIONING_RECONCILE_BATCH_SIZE: {{ .Values.config.provisioner.reconcileBatchSize | quote }}
WORKER_URL_TEMPLATE: {{ .Values.config.provisioner.workerUrlTemplate | quote }}
{{ if or .Values.inference.enabled .Values.config.internal.inferenceProxyBaseUrl }}
INFERENCE_PROXY_BASE_URL: {{ include "openwork-ee.inferenceInternalUrl" . | quote }}
{{ end }}
POLAR_FEATURE_GATE_ENABLED: {{ .Values.config.polar.featureGateEnabled | quote }}
DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS: {{ .Values.config.daytona.signedPreviewExpiresSeconds | quote }}
DEN_API_BASE: {{ include "openwork-ee.denApiInternalUrl" . | quote }}
DEN_AUTH_FALLBACK_BASE: {{ include "openwork-ee.authFallbackInternalUrl" . | quote }}
DEN_AUTH_ORIGIN: {{ .Values.config.public.webOrigin | quote }}
DEN_WEB_PUBLIC_ORIGIN: {{ .Values.config.public.webOrigin | quote }}
DEN_WEB_OPENWORK_AUTH_CALLBACK_URL: {{ .Values.config.public.authCallbackUrl | quote }}
{{ with .Values.config.polar.apiBase }}
POLAR_API_BASE: {{ . | quote }}
{{ end }}
{{ with .Values.config.polar.productId }}
POLAR_PRODUCT_ID: {{ . | quote }}
{{ end }}
{{ with .Values.config.polar.benefitId }}
POLAR_BENEFIT_ID: {{ . | quote }}
{{ end }}
{{ with .Values.config.polar.successUrl }}
POLAR_SUCCESS_URL: {{ . | quote }}
{{ end }}
{{ with .Values.config.polar.returnUrl }}
POLAR_RETURN_URL: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.apiUrl }}
DAYTONA_API_URL: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.target }}
DAYTONA_TARGET: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.snapshot }}
DAYTONA_SNAPSHOT: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxImage }}
DAYTONA_SANDBOX_IMAGE: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxCpu }}
DAYTONA_SANDBOX_CPU: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxMemory }}
DAYTONA_SANDBOX_MEMORY: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxDisk }}
DAYTONA_SANDBOX_DISK: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxPublic }}
DAYTONA_SANDBOX_PUBLIC: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxAutoStopInterval }}
DAYTONA_SANDBOX_AUTO_STOP_INTERVAL: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxAutoArchiveInterval }}
DAYTONA_SANDBOX_AUTO_ARCHIVE_INTERVAL: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxAutoDeleteInterval }}
DAYTONA_SANDBOX_AUTO_DELETE_INTERVAL: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sandboxNamePrefix }}
DAYTONA_SANDBOX_NAME_PREFIX: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sharedVolumeName }}
DAYTONA_SHARED_VOLUME_NAME: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.volumeNamePrefix }}
DAYTONA_VOLUME_NAME_PREFIX: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.workspaceMountPath }}
DAYTONA_WORKSPACE_MOUNT_PATH: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.dataMountPath }}
DAYTONA_DATA_MOUNT_PATH: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.runtimeWorkspacePath }}
DAYTONA_RUNTIME_WORKSPACE_PATH: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.runtimeDataPath }}
DAYTONA_RUNTIME_DATA_PATH: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.sidecarDir }}
DAYTONA_SIDECAR_DIR: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.openworkPort }}
DAYTONA_OPENWORK_PORT: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.opencodePort }}
DAYTONA_OPENCODE_PORT: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.createTimeoutSeconds }}
DAYTONA_CREATE_TIMEOUT_SECONDS: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.deleteTimeoutSeconds }}
DAYTONA_DELETE_TIMEOUT_SECONDS: {{ . | quote }}
{{ end }}
{{ with .Values.config.daytona.healthcheckTimeoutMs }}
DAYTONA_HEALTHCHECK_TIMEOUT_MS: {{ . | quote }}
{{ end }}
{{ with .Values.config.inference.openRouterWorkspaceId }}
OPENROUTER_WORKSPACE_ID: {{ . | quote }}
{{ end }}
{{ with .Values.config.inference.stripeInferencePriceId }}
STRIPE_INFERENCE_PRICE_ID: {{ . | quote }}
{{ end }}
{{ with .Values.config.inference.openRouterUpstreamUrl }}
OPENROUTER_UPSTREAM_URL: {{ . | quote }}
{{ end }}
{{ with .Values.config.inference.creditsPerDollar }}
INFERENCE_CREDITS_PER_DOLLAR: {{ . | quote }}
{{ end }}
{{ with .Values.config.public.openworkAppConnectUrl }}
DEN_WEB_OPENWORK_APP_CONNECT_URL: {{ . | quote }}
{{ end }}
{{ with .Values.config.public.openworkWebUrl }}
DEN_WEB_OPENWORK_WEB_URL: {{ . | quote }}
{{ end }}
{{ with .Values.config.web.posthogKey }}
DEN_WEB_POSTHOG_KEY: {{ . | quote }}
{{ end }}
{{ with .Values.config.web.posthogHost }}
DEN_WEB_POSTHOG_HOST: {{ . | quote }}
{{ end }}
@@ -0,0 +1,145 @@
{{- include "openwork-ee.customCa.validate" . }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "openwork-ee.denApiServiceName" . }}
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "den-api") | nindent 4 }}
{{- with .Values.denApi.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.denApi.service.type }}
{{- with .Values.denApi.service.loadBalancerClass }}
loadBalancerClass: {{ . | quote }}
{{- end }}
{{- with .Values.denApi.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- toYaml . | nindent 4 }}
{{- end }}
ports:
- name: http
port: {{ .Values.denApi.service.port }}
targetPort: http
selector:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "den-api") | nindent 4 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "openwork-ee.fullname" . }}-den-api
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "den-api") | nindent 4 }}
spec:
replicas: {{ .Values.denApi.replicaCount }}
selector:
matchLabels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "den-api") | nindent 6 }}
template:
metadata:
labels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "den-api") | nindent 8 }}
{{- with .Values.denApi.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.denApi.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.installerArtifacts.enabled }}
{{- if and .Values.installerArtifacts.existingClaim .Values.installerArtifacts.hostPath }}
{{- fail "installerArtifacts.existingClaim and installerArtifacts.hostPath are mutually exclusive" }}
{{- end }}
{{- if not (or .Values.installerArtifacts.existingClaim .Values.installerArtifacts.hostPath) }}
{{- fail "installerArtifacts.existingClaim or installerArtifacts.hostPath is required when installerArtifacts.enabled=true" }}
{{- end }}
{{- end }}
{{- if or .Values.installerArtifacts.enabled .Values.customCa.enabled }}
volumes:
{{- if .Values.installerArtifacts.enabled }}
- name: installer-artifacts
{{- if .Values.installerArtifacts.existingClaim }}
persistentVolumeClaim:
claimName: {{ .Values.installerArtifacts.existingClaim | quote }}
{{- else }}
hostPath:
path: {{ .Values.installerArtifacts.hostPath | quote }}
type: Directory
{{- end }}
{{- end }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.volume" . | nindent 8 }}
{{- end }}
{{- end }}
containers:
- name: den-api
image: "{{ .Values.denApi.image.repository }}:{{ default .Values.image.tag .Values.denApi.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.denApi.containerPort }}
{{- if or .Values.installerArtifacts.enabled .Values.customCa.enabled }}
volumeMounts:
{{- if .Values.installerArtifacts.enabled }}
- name: installer-artifacts
mountPath: {{ .Values.installerArtifacts.mountPath | quote }}
readOnly: true
{{- end }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.volumeMount" . | nindent 12 }}
{{- end }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "openwork-ee.configName" . }}
- secretRef:
name: {{ include "openwork-ee.secretName" . }}
env:
- name: PORT
value: {{ .Values.denApi.containerPort | quote }}
{{- if not (hasKey .Values.denApi.env "NODE_OPTIONS") }}
# Keep this as a direct value so upgrades from legacy denApi.env.NODE_OPTIONS
# do not leave both value and valueFrom in Kubernetes' strategic merge patch.
- name: NODE_OPTIONS
value: {{ .Values.config.denApiNodeOptions | quote }}
# Explicitly clear the valueFrom emitted by chart 0.17.24 during upgrades.
valueFrom: null
{{- end }}
{{- if .Values.installerArtifacts.enabled }}
- name: OPENWORK_INSTALLER_ARTIFACTS_DIR
value: {{ .Values.installerArtifacts.mountPath | quote }}
{{- end }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.env" . | nindent 12 }}
{{- end }}
{{- if eq (include "openwork-ee.allowPrivateMcpUrls" .) "1" }}
- name: DEN_ALLOW_PRIVATE_MCP_URLS
value: "1"
{{- end }}
{{- include "openwork-ee.observabilityEnv" (dict "root" . "serviceName" .Values.observability.serviceNames.denApi) | nindent 12 }}
{{- range $key, $value := .Values.denApi.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
livenessProbe:
httpGet:
path: /health
port: http
{{- toYaml .Values.denApi.probes.liveness | nindent 12 }}
readinessProbe:
httpGet:
path: /ready
port: http
{{- toYaml .Values.denApi.probes.readiness | nindent 12 }}
{{- with .Values.denApi.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
@@ -0,0 +1,101 @@
{{- include "openwork-ee.customCa.validate" . }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "openwork-ee.denWebServiceName" . }}
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "den-web") | nindent 4 }}
{{- with .Values.denWeb.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.denWeb.service.type }}
{{- with .Values.denWeb.service.loadBalancerClass }}
loadBalancerClass: {{ . | quote }}
{{- end }}
{{- with .Values.denWeb.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- toYaml . | nindent 4 }}
{{- end }}
ports:
- name: http
port: {{ .Values.denWeb.service.port }}
targetPort: http
selector:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "den-web") | nindent 4 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "openwork-ee.fullname" . }}-den-web
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "den-web") | nindent 4 }}
spec:
replicas: {{ .Values.denWeb.replicaCount }}
selector:
matchLabels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "den-web") | nindent 6 }}
template:
metadata:
labels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "den-web") | nindent 8 }}
{{- with .Values.denWeb.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.denWeb.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.customCa.enabled }}
volumes:
{{- include "openwork-ee.customCa.volume" . | nindent 8 }}
{{- end }}
containers:
- name: den-web
image: "{{ .Values.denWeb.image.repository }}:{{ default .Values.image.tag .Values.denWeb.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.denWeb.containerPort }}
{{- if .Values.customCa.enabled }}
volumeMounts:
{{- include "openwork-ee.customCa.volumeMount" . | nindent 12 }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "openwork-ee.configName" . }}
- secretRef:
name: {{ include "openwork-ee.secretName" . }}
env:
- name: PORT
value: {{ .Values.denWeb.containerPort | quote }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.env" . | nindent 12 }}
{{- end }}
{{- include "openwork-ee.observabilityEnv" (dict "root" . "serviceName" .Values.observability.serviceNames.denWeb) | nindent 12 }}
{{- range $key, $value := .Values.denWeb.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
livenessProbe:
httpGet:
path: /api/health
port: http
{{- toYaml .Values.denWeb.probes.liveness | nindent 12 }}
readinessProbe:
httpGet:
path: /api/ready
port: http
{{- toYaml .Values.denWeb.probes.readiness | nindent 12 }}
{{- with .Values.denWeb.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
@@ -0,0 +1,102 @@
{{- include "openwork-ee.customCa.validate" . }}
{{- if .Values.inference.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "openwork-ee.inferenceServiceName" . }}
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "inference") | nindent 4 }}
{{- with .Values.inference.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.inference.service.type }}
{{- with .Values.inference.service.loadBalancerClass }}
loadBalancerClass: {{ . | quote }}
{{- end }}
{{- with .Values.inference.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- toYaml . | nindent 4 }}
{{- end }}
ports:
- name: http
port: {{ .Values.inference.service.port }}
targetPort: http
selector:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "inference") | nindent 4 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "openwork-ee.fullname" . }}-inference
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "inference") | nindent 4 }}
spec:
replicas: {{ .Values.inference.replicaCount }}
selector:
matchLabels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "inference") | nindent 6 }}
template:
metadata:
labels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "inference") | nindent 8 }}
{{- with .Values.inference.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.inference.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.customCa.enabled }}
volumes:
{{- include "openwork-ee.customCa.volume" . | nindent 8 }}
{{- end }}
containers:
- name: inference
image: "{{ .Values.inference.image.repository }}:{{ default .Values.image.tag .Values.inference.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.inference.containerPort }}
{{- if .Values.customCa.enabled }}
volumeMounts:
{{- include "openwork-ee.customCa.volumeMount" . | nindent 12 }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "openwork-ee.configName" . }}
- secretRef:
name: {{ include "openwork-ee.secretName" . }}
env:
- name: PORT
value: {{ .Values.inference.containerPort | quote }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.env" . | nindent 12 }}
{{- end }}
{{- range $key, $value := .Values.inference.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
livenessProbe:
httpGet:
path: /health
port: http
{{- toYaml .Values.inference.probes.liveness | nindent 12 }}
readinessProbe:
httpGet:
path: /ready
port: http
{{- toYaml .Values.inference.probes.readiness | nindent 12 }}
{{- with .Values.inference.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
@@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "openwork-ee.fullname" . }}
labels:
{{- include "openwork-ee.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
- host: {{ .Values.ingress.web.host | quote }}
http:
paths:
- path: {{ .Values.ingress.web.path }}
pathType: {{ .Values.ingress.web.pathType }}
backend:
service:
name: {{ include "openwork-ee.denWebServiceName" . }}
port:
name: http
{{- if .Values.ingress.api.enabled }}
- host: {{ .Values.ingress.api.host | quote }}
http:
paths:
- path: {{ .Values.ingress.api.path }}
pathType: {{ .Values.ingress.api.pathType }}
backend:
service:
name: {{ include "openwork-ee.denApiServiceName" . }}
port:
name: http
{{- end }}
{{- end }}
@@ -0,0 +1,78 @@
{{- include "openwork-ee.customCa.validate" . }}
{{- if .Values.migrations.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "openwork-ee.fullname" . }}-migrate
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "migration") | nindent 4 }}
{{- if .Values.migrations.hook }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
{{- with .Values.migrations.hookDeletePolicy }}
"helm.sh/hook-delete-policy": {{ . | quote }}
{{- end }}
{{- end }}
spec:
backoffLimit: {{ .Values.migrations.backoffLimit }}
activeDeadlineSeconds: {{ .Values.migrations.activeDeadlineSeconds }}
template:
metadata:
labels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "migration") | nindent 8 }}
{{- with .Values.migrations.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
restartPolicy: Never
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.customCa.enabled }}
volumes:
{{- include "openwork-ee.customCa.volume" . | nindent 8 }}
{{- end }}
containers:
- name: migrate
image: "{{ .Values.denApi.image.repository }}:{{ default .Values.image.tag .Values.denApi.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.customCa.enabled }}
volumeMounts:
{{- include "openwork-ee.customCa.volumeMount" . | nindent 12 }}
{{- end }}
command:
{{- toYaml .Values.migrations.command | nindent 12 }}
args:
{{- toYaml .Values.migrations.args | nindent 12 }}
env:
- name: DB_MODE
value: {{ .Values.config.databaseMode | quote }}
- name: DATABASE_URL
{{- if .Values.secret.create }}
value: {{ .Values.secret.values.databaseUrl | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "openwork-ee.secretName" . }}
key: {{ .Values.secret.keys.databaseUrl }}
{{- end }}
- name: DEN_DB_ENCRYPTION_KEY
{{- if .Values.secret.create }}
value: {{ .Values.secret.values.denDbEncryptionKey | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "openwork-ee.secretName" . }}
key: {{ .Values.secret.keys.denDbEncryptionKey }}
{{- end }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.env" . | nindent 12 }}
{{- end }}
{{- with .Values.migrations.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
@@ -0,0 +1,30 @@
{{- if .Values.secret.create }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "openwork-ee.secretName" . }}
labels:
{{- include "openwork-ee.labels" . | nindent 4 }}
type: Opaque
stringData:
{{ .Values.secret.keys.databaseUrl }}: {{ .Values.secret.values.databaseUrl | quote }}
{{ .Values.secret.keys.databaseRedisUrl }}: {{ .Values.secret.values.databaseRedisUrl | quote }}
{{ .Values.secret.keys.betterAuthSecret }}: {{ .Values.secret.values.betterAuthSecret | quote }}
{{ .Values.secret.keys.denDbEncryptionKey }}: {{ .Values.secret.values.denDbEncryptionKey | quote }}
{{ .Values.secret.keys.daytonaApiKey }}: {{ .Values.secret.values.daytonaApiKey | quote }}
{{ .Values.secret.keys.polarAccessToken }}: {{ .Values.secret.values.polarAccessToken | quote }}
{{ .Values.secret.keys.openRouterManagementApiKey }}: {{ .Values.secret.values.openRouterManagementApiKey | quote }}
{{ .Values.secret.keys.inferenceAdminToken }}: {{ .Values.secret.values.inferenceAdminToken | quote }}
{{ .Values.secret.keys.inferenceWebhookSecret }}: {{ .Values.secret.values.inferenceWebhookSecret | quote }}
{{ .Values.secret.keys.emailFrom }}: {{ .Values.secret.values.emailFrom | quote }}
{{ .Values.secret.keys.smtpHost }}: {{ .Values.secret.values.smtpHost | quote }}
{{ .Values.secret.keys.smtpPort }}: {{ .Values.secret.values.smtpPort | quote }}
{{ .Values.secret.keys.smtpUser }}: {{ .Values.secret.values.smtpUser | quote }}
{{ .Values.secret.keys.smtpPass }}: {{ .Values.secret.values.smtpPass | quote }}
{{ .Values.secret.keys.smtpSecure }}: {{ .Values.secret.values.smtpSecure | quote }}
{{ .Values.secret.keys.githubConnectorAppClientSecret }}: {{ .Values.secret.values.githubConnectorAppClientSecret | quote }}
{{ .Values.secret.keys.githubConnectorAppPrivateKey }}: {{ .Values.secret.values.githubConnectorAppPrivateKey | quote }}
{{ .Values.secret.keys.githubConnectorAppWebhookSecret }}: {{ .Values.secret.values.githubConnectorAppWebhookSecret | quote }}
{{ .Values.secret.keys.connectLinkPrivateKey }}: {{ .Values.secret.values.connectLinkPrivateKey | quote }}
{{ .Values.secret.keys.initialAdminBootstrapCode }}: {{ .Values.secret.values.initialAdminBootstrapCode | quote }}
{{- end }}
@@ -0,0 +1,182 @@
{{- include "openwork-ee.customCa.validate" . }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "openwork-ee.fullname" . }}-env-probe
labels:
{{- include "openwork-ee.componentLabels" (dict "root" . "component" "env-probe") | nindent 4 }}
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": before-hook-creation
spec:
backoffLimit: 0
activeDeadlineSeconds: 120
template:
metadata:
labels:
{{- include "openwork-ee.componentSelectorLabels" (dict "root" . "component" "env-probe") | nindent 8 }}
spec:
restartPolicy: Never
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.customCa.enabled }}
volumes:
{{- include "openwork-ee.customCa.volume" . | nindent 8 }}
{{- end }}
containers:
- name: env-probe
image: "{{ .Values.denApi.image.repository }}:{{ default .Values.image.tag .Values.denApi.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.customCa.enabled }}
volumeMounts:
{{- include "openwork-ee.customCa.volumeMount" . | nindent 12 }}
{{- end }}
command:
- node
args:
- -e
- |
const fs = require("node:fs");
const tls = require("node:tls");
const expected = new Map([
["DB_MODE", process.env.OPENWORK_ENV_PROBE_EXPECTED_DB_MODE],
["DEN_ORG_MODE", process.env.OPENWORK_ENV_PROBE_EXPECTED_DEN_ORG_MODE],
["PROVISIONER_MODE", process.env.OPENWORK_ENV_PROBE_EXPECTED_PROVISIONER_MODE],
["OPENWORK_DEV_MODE", process.env.OPENWORK_ENV_PROBE_EXPECTED_OPENWORK_DEV_MODE],
["DEN_BASE_URL", process.env.OPENWORK_ENV_PROBE_EXPECTED_DEN_BASE_URL],
["DEN_AUTH_ORIGIN", process.env.OPENWORK_ENV_PROBE_EXPECTED_DEN_AUTH_ORIGIN],
["PORT", process.env.OPENWORK_ENV_PROBE_EXPECTED_PORT],
["NODE_OPTIONS", process.env.OPENWORK_ENV_PROBE_EXPECTED_NODE_OPTIONS],
["DEN_API_NODE_OPTIONS", process.env.OPENWORK_ENV_PROBE_EXPECTED_NODE_OPTIONS],
]);
const required = [
"DATABASE_URL",
"DEN_DB_ENCRYPTION_KEY",
"BETTER_AUTH_SECRET",
"DEN_BASE_URL",
"DB_MODE",
"DEN_ORG_MODE",
"PROVISIONER_MODE",
"DEN_API_BASE",
"DEN_AUTH_FALLBACK_BASE",
"DEN_AUTH_ORIGIN",
"OPENWORK_DEV_MODE",
"PORT",
"NODE_OPTIONS",
];
const secretPattern = /(SECRET|PASSWORD|PRIVATE|TOKEN|KEY|DATABASE_URL|BOOTSTRAP_CODE|SHA256)/;
const failures = [];
function compactPem(value) {
return value.replace(/\s+/g, "");
}
function display(name, value) {
if (value === undefined) return "<missing>";
if (secretPattern.test(name)) return value ? "<set>" : "<empty>";
return JSON.stringify(value);
}
for (const name of required) {
if (process.env[name] === undefined) {
failures.push(`${name}: expected present, actual <missing>`);
}
}
for (const [name, expectedValue] of expected) {
if (expectedValue === undefined) continue;
const actual = process.env[name];
if (actual !== expectedValue) {
failures.push(`${name}: expected ${display(name, expectedValue)}, actual ${display(name, actual)}`);
}
}
if (process.env.OPENWORK_ENV_PROBE_EXPECTED_CUSTOM_CA === "1") {
const expectedCaPath = process.env.OPENWORK_ENV_PROBE_EXPECTED_NODE_EXTRA_CA_CERTS;
if (process.env.NODE_EXTRA_CA_CERTS !== expectedCaPath) {
failures.push(`NODE_EXTRA_CA_CERTS: expected ${display("NODE_EXTRA_CA_CERTS", expectedCaPath)}, actual ${display("NODE_EXTRA_CA_CERTS", process.env.NODE_EXTRA_CA_CERTS)}`);
} else {
try {
const bundle = fs.readFileSync(expectedCaPath, "utf8");
const certs = bundle.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) || [];
if (certs.length === 0) {
failures.push(`NODE_EXTRA_CA_CERTS: ${expectedCaPath} did not contain a PEM certificate`);
} else if (typeof tls.getCACertificates !== "function") {
failures.push("NODE_EXTRA_CA_CERTS: node does not expose tls.getCACertificates; cannot prove runtime trust");
} else {
const extra = tls.getCACertificates("extra").map(compactPem);
const trusted = certs.some((cert) => extra.includes(compactPem(cert)));
if (!trusted) {
failures.push(`NODE_EXTRA_CA_CERTS: ${expectedCaPath} was mounted but did not appear in node's extra CA trust store`);
}
}
} catch (error) {
failures.push(`NODE_EXTRA_CA_CERTS: could not read ${expectedCaPath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
if (failures.length > 0) {
console.error("OpenWork Helm env probe failed:");
for (const failure of failures) console.error(`- ${failure}`);
console.error("Observed runtime env:");
for (const name of [...required, "DEN_API_NODE_OPTIONS", "NODE_EXTRA_CA_CERTS"].sort()) {
console.error(` ${name}: ${display(name, process.env[name])}`);
}
process.exit(1);
}
console.log("OpenWork Helm env probe passed.");
console.log(`DEN_ORG_MODE=${process.env.DEN_ORG_MODE}; PROVISIONER_MODE=${process.env.PROVISIONER_MODE}; NODE_OPTIONS=${JSON.stringify(process.env.NODE_OPTIONS || "")}`);
if (process.env.NODE_EXTRA_CA_CERTS) console.log(`NODE_EXTRA_CA_CERTS=${process.env.NODE_EXTRA_CA_CERTS} is loaded by node.`);
envFrom:
- configMapRef:
name: {{ include "openwork-ee.configName" . }}
- secretRef:
name: {{ include "openwork-ee.secretName" . }}
env:
- name: PORT
value: {{ .Values.denApi.containerPort | quote }}
{{- if not (hasKey .Values.denApi.env "NODE_OPTIONS") }}
- name: NODE_OPTIONS
value: {{ .Values.config.denApiNodeOptions | quote }}
{{- end }}
{{- if .Values.installerArtifacts.enabled }}
- name: OPENWORK_INSTALLER_ARTIFACTS_DIR
value: {{ .Values.installerArtifacts.mountPath | quote }}
{{- end }}
{{- if .Values.customCa.enabled }}
{{- include "openwork-ee.customCa.env" . | nindent 12 }}
{{- end }}
{{- if eq (include "openwork-ee.allowPrivateMcpUrls" .) "1" }}
- name: DEN_ALLOW_PRIVATE_MCP_URLS
value: "1"
{{- end }}
{{- range $key, $value := .Values.denApi.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
- name: OPENWORK_ENV_PROBE_EXPECTED_DB_MODE
value: {{ .Values.config.databaseMode | quote }}
- name: OPENWORK_ENV_PROBE_EXPECTED_DEN_ORG_MODE
value: {{ .Values.config.tenancy.mode | quote }}
- name: OPENWORK_ENV_PROBE_EXPECTED_PROVISIONER_MODE
value: {{ .Values.config.provisioner.mode | quote }}
- name: OPENWORK_ENV_PROBE_EXPECTED_OPENWORK_DEV_MODE
value: {{ .Values.config.openworkDevMode | quote }}
- name: OPENWORK_ENV_PROBE_EXPECTED_DEN_BASE_URL
value: {{ .Values.config.public.webOrigin | quote }}
- name: OPENWORK_ENV_PROBE_EXPECTED_DEN_AUTH_ORIGIN
value: {{ .Values.config.public.webOrigin | quote }}
- name: OPENWORK_ENV_PROBE_EXPECTED_PORT
value: {{ .Values.denApi.containerPort | quote }}
- name: OPENWORK_ENV_PROBE_EXPECTED_NODE_OPTIONS
value: {{ if hasKey .Values.denApi.env "NODE_OPTIONS" }}{{ index .Values.denApi.env "NODE_OPTIONS" | quote }}{{ else }}{{ .Values.config.denApiNodeOptions | quote }}{{ end }}
- name: OPENWORK_ENV_PROBE_EXPECTED_CUSTOM_CA
value: {{ ternary "1" "0" .Values.customCa.enabled | quote }}
{{- if .Values.customCa.enabled }}
- name: OPENWORK_ENV_PROBE_EXPECTED_NODE_EXTRA_CA_CERTS
value: {{ include "openwork-ee.customCa.filePath" . | quote }}
{{- end }}
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
assert_contains() {
local file="$1"
local needle="$2"
if ! grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart to contain %s\n' "$needle" >&2
return 1
fi
}
default_rendered="$tmp_dir/default.yaml"
helm template openwork-ee "$chart_dir" > "$default_rendered"
assert_contains "$default_rendered" 'DEN_AUTOMATIONS_ENABLED: "false"'
assert_contains "$default_rendered" 'DEN_AUTOMATIONS_RUNTIME_ENABLED: "true"'
enabled_values="$tmp_dir/enabled-values.yaml"
enabled_rendered="$tmp_dir/enabled.yaml"
printf '%s\n' 'config:' ' public:' ' automationsEnabled: "true"' > "$enabled_values"
helm template openwork-ee "$chart_dir" -f "$enabled_values" > "$enabled_rendered"
assert_contains "$enabled_rendered" 'DEN_AUTOMATIONS_ENABLED: "true"'
disabled_values="$tmp_dir/disabled-values.yaml"
disabled_rendered="$tmp_dir/disabled.yaml"
printf '%s\n' 'config:' ' public:' ' automationsRuntimeEnabled: "false"' > "$disabled_values"
helm template openwork-ee "$chart_dir" -f "$disabled_values" > "$disabled_rendered"
assert_contains "$disabled_rendered" 'DEN_AUTOMATIONS_RUNTIME_ENABLED: "false"'
printf 'automations-enabled chart checks passed\n'
@@ -0,0 +1,271 @@
import { spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const chartDir = join(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = join(chartDir, "..", "..", "..");
const denDbDir = join(repoRoot, "ee", "packages", "den-db");
const denDbPackage = join(denDbDir, "package.json");
const denDbRequire = createRequire(denDbPackage);
const mysql = denDbRequire("mysql2/promise");
const bootstrapPath = join(denDbDir, "dist", "scripts", "bootstrap.js");
const currentSchemaPath = join(denDbDir, "dist", "current-schema.sql");
const denDbIndexPath = join(denDbDir, "dist", "index.js");
const image = process.env.OPENWORK_EVAL_MYSQL_IMAGE?.trim() || "mysql:8.4";
const containerName = `openwork-custom-ca-mysql-${process.pid}-${Date.now()}`;
const tmp = mkdtempSync(join(tmpdir(), "openwork-custom-ca-mysql-"));
const certsDir = join(tmp, "certs");
const caKey = join(certsDir, "ca.key");
const caCert = join(certsDir, "ca.crt");
const serverKey = join(certsDir, "server.key");
const serverCsr = join(certsDir, "server.csr");
const serverCert = join(certsDir, "server.crt");
const opensslConfig = join(tmp, "openssl.cnf");
const mysqlConfig = join(tmp, "tls.cnf");
const databaseName = "openwork_den";
const databaseUser = "openwork";
const databasePassword = "openwork_tls_test_password";
let containerStarted = false;
function run(command, args, options = {}) {
return spawnSync(command, args, {
cwd: repoRoot,
encoding: "utf8",
maxBuffer: 20 * 1024 * 1024,
timeout: 180_000,
...options,
});
}
function ensureSuccess(result, description) {
if (result.status !== 0) {
throw new Error(`${description} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
}
}
function runOpenSsl(args) {
ensureSuccess(run("openssl", args), `openssl ${args.join(" ")}`);
}
function ensureBuildArtifacts() {
if (existsSync(bootstrapPath) && existsSync(currentSchemaPath) && existsSync(denDbIndexPath)) {
return;
}
const result = run("pnpm", ["--dir", denDbDir, "run", "build"], { timeout: 240_000 });
ensureSuccess(result, "pnpm --dir ee/packages/den-db run build");
console.log("Built @openwork-ee/den-db migration artifacts");
}
function generateCertificates() {
mkdirSync(certsDir, { recursive: true });
writeFileSync(
opensslConfig,
`[req]
distinguished_name = dn
prompt = no
req_extensions = v3_req
[dn]
CN = localhost
[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = mysql.private.test
IP.1 = 127.0.0.1
`,
);
runOpenSsl(["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", "-subj", "/CN=OpenWork Test MySQL CA", "-keyout", caKey, "-out", caCert]);
runOpenSsl(["req", "-newkey", "rsa:2048", "-nodes", "-keyout", serverKey, "-out", serverCsr, "-config", opensslConfig]);
runOpenSsl(["x509", "-req", "-in", serverCsr, "-CA", caCert, "-CAkey", caKey, "-CAcreateserial", "-out", serverCert, "-days", "1", "-extensions", "v3_req", "-extfile", opensslConfig]);
chmodSync(serverKey, 0o644);
chmodSync(serverCert, 0o644);
chmodSync(caCert, 0o644);
writeFileSync(
mysqlConfig,
`[mysqld]
ssl-ca=/etc/mysql/certs/ca.crt
ssl-cert=/etc/mysql/certs/server.crt
ssl-key=/etc/mysql/certs/server.key
require_secure_transport=ON
tls_version=TLSv1.2,TLSv1.3
`,
);
chmodSync(mysqlConfig, 0o644);
console.log("Generated private CA and MySQL server certificate with SAN DNS:localhost, DNS:mysql.private.test, IP:127.0.0.1");
}
function docker(args, options = {}) {
return run("docker", args, { timeout: 240_000, ...options });
}
function startMysqlContainer() {
const result = docker([
"run",
"--rm",
"--detach",
"--name",
containerName,
"--publish",
"127.0.0.1::3306",
"--env",
"MYSQL_ROOT_PASSWORD=openwork_root_password",
"--env",
"MYSQL_ROOT_HOST=%",
"--env",
`MYSQL_DATABASE=${databaseName}`,
"--env",
`MYSQL_USER=${databaseUser}`,
"--env",
`MYSQL_PASSWORD=${databasePassword}`,
"--volume",
`${certsDir}:/etc/mysql/certs:ro`,
"--volume",
`${mysqlConfig}:/etc/mysql/conf.d/tls.cnf:ro`,
image,
]);
ensureSuccess(result, `docker run ${image}`);
containerStarted = true;
console.log(`Started TLS-enabled MySQL container ${containerName} from ${image}`);
}
function mappedPort() {
const result = docker(["port", containerName, "3306/tcp"]);
ensureSuccess(result, "docker port");
const endpoint = result.stdout.trim().split("\n").find(Boolean);
const port = endpoint?.split(":").pop();
if (!port) {
throw new Error(`Unable to resolve mapped MySQL port from: ${result.stdout}`);
}
return Number(port);
}
async function waitForMysql(port) {
const started = Date.now();
let lastError = "";
while (Date.now() - started < 90_000) {
try {
const connection = await mysql.createConnection({
host: "127.0.0.1",
port,
user: databaseUser,
password: databasePassword,
database: databaseName,
ssl: { rejectUnauthorized: false },
});
await connection.query("select 1");
await connection.end();
return;
} catch (error) {
lastError = error instanceof Error ? error.message : String(error);
await new Promise((resolve) => setTimeout(resolve, 750));
}
}
const logs = docker(["logs", containerName], { timeout: 30_000 });
throw new Error(`MySQL container did not become ready: ${lastError}\n${logs.stdout}\n${logs.stderr}`);
}
function strictDatabaseUrl(port) {
return `mysql://${databaseUser}:${databasePassword}@127.0.0.1:${port}/${databaseName}?sslmode=verify-full`;
}
function redactedDatabaseUrl(port) {
return `mysql://${databaseUser}:REDACTED@127.0.0.1:${port}/${databaseName}?sslmode=verify-full`;
}
function childEnv(extra = {}) {
const env = { ...process.env, ...extra };
delete env.NODE_TLS_REJECT_UNAUTHORIZED;
return env;
}
function runOpenWorkMysqlQuery(databaseUrl, query, extraEnv = {}) {
const source = `
import { createRequire } from "node:module";
const mysql = createRequire(process.argv[1])("mysql2/promise");
const { parseMySqlConnectionConfig } = await import(process.argv[2]);
const connection = await mysql.createConnection(parseMySqlConnectionConfig(process.argv[3]));
try {
const [rows] = await connection.query(process.argv[4]);
console.log(JSON.stringify(rows));
} finally {
await connection.end();
}
`;
return run(process.execPath, ["--input-type=module", "-e", source, denDbPackage, pathToFileURL(denDbIndexPath).href, databaseUrl, query], {
env: childEnv(extraEnv),
timeout: 90_000,
});
}
function runBootstrap(databaseUrl) {
return run(process.execPath, [bootstrapPath], {
cwd: denDbDir,
env: childEnv({
DATABASE_URL: databaseUrl,
DEN_DB_ENCRYPTION_KEY: "openwork-custom-ca-mysql-tls-test-key-1234567890",
NODE_EXTRA_CA_CERTS: caCert,
OPENWORK_DEN_DB_ENV_PATH: join(tmp, "does-not-exist.env"),
}),
timeout: 180_000,
});
}
function cleanup() {
if (containerStarted) {
docker(["rm", "-f", containerName], { timeout: 30_000 });
}
rmSync(tmp, { recursive: true, force: true });
}
try {
ensureBuildArtifacts();
generateCertificates();
startMysqlContainer();
const port = mappedPort();
await waitForMysql(port);
const databaseUrl = strictDatabaseUrl(port);
console.log(`TLS MySQL endpoint ready at ${redactedDatabaseUrl(port)}`);
const withoutCa = runOpenWorkMysqlQuery(databaseUrl, "select 1 as ok");
if (withoutCa.status === 0) {
throw new Error("Strict OpenWork/mysql2 unexpectedly connected without NODE_EXTRA_CA_CERTS");
}
const withoutCaError = `${withoutCa.stdout}\n${withoutCa.stderr}`.trim();
if (!/certificate|verify|self-signed|unable/i.test(withoutCaError)) {
throw new Error(`Strict OpenWork/mysql2 failed for a non-certificate reason:\n${withoutCaError}`);
}
const certificateError = withoutCaError.split("\n").find((line) => /certificate|verify|self-signed|unable/i.test(line)) ?? withoutCaError.split("\n").find(Boolean);
console.log(`Strict OpenWork/mysql2 without custom CA: rejected (${certificateError})`);
const bootstrap = runBootstrap(databaseUrl);
ensureSuccess(bootstrap, "OpenWork migration bootstrap with NODE_EXTRA_CA_CERTS");
console.log("OpenWork migration bootstrap completed with NODE_EXTRA_CA_CERTS and strict DATABASE_URL");
console.log(bootstrap.stdout.trim());
const query = runOpenWorkMysqlQuery(
databaseUrl,
"select (select count(*) from information_schema.tables where table_schema = database()) as table_count, (select count(*) from `__drizzle_migrations`) as migration_count",
{ NODE_EXTRA_CA_CERTS: caCert },
);
ensureSuccess(query, "OpenWork/mysql2 strict post-migration query with NODE_EXTRA_CA_CERTS");
const rows = JSON.parse(query.stdout.trim());
const tableCount = Number(rows[0]?.table_count ?? 0);
const migrationCount = Number(rows[0]?.migration_count ?? 0);
if (tableCount <= 0 || migrationCount <= 0) {
throw new Error(`Expected migrated OpenWork tables and migration ledger rows, got ${query.stdout}`);
}
console.log(`OpenWork/mysql2 strict query succeeded after migration: tables=${tableCount}, migrations=${migrationCount}`);
} finally {
cleanup();
}
@@ -0,0 +1,147 @@
import { spawn, spawnSync } from "node:child_process";
import { createServer } from "node:https";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const tmp = mkdtempSync(join(tmpdir(), "openwork-custom-ca-"));
const caKey = join(tmp, "ca.key");
const caCert = join(tmp, "ca.crt");
const serverKey = join(tmp, "server.key");
const serverCsr = join(tmp, "server.csr");
const serverCert = join(tmp, "server.crt");
const opensslConfig = join(tmp, "openssl.cnf");
function runOpenSsl(args) {
const result = spawnSync("openssl", args, { encoding: "utf8" });
if (result.status !== 0) {
throw new Error(`openssl ${args.join(" ")} failed\n${result.stderr}`);
}
}
function clientEnvironment(caPath) {
const env = { ...process.env };
delete env.NODE_EXTRA_CA_CERTS;
delete env.NODE_TLS_REJECT_UNAUTHORIZED;
if (caPath) {
env.NODE_EXTRA_CA_CERTS = caPath;
}
return env;
}
function runClient(url, caPath) {
const clientSource = `
const https = require("node:https");
https.get(process.argv[1], (response) => {
let body = "";
response.setEncoding("utf8");
response.on("data", (chunk) => { body += chunk; });
response.on("end", () => {
if (response.statusCode === 200 && body === "openwork-custom-ca-ok") {
console.log(body);
process.exit(0);
}
console.error(\`unexpected response: \${response.statusCode} \${body}\`);
process.exit(1);
});
}).on("error", (error) => {
console.error(error.code || error.message);
process.exit(2);
});
`;
return new Promise((resolve) => {
const child = spawn(process.execPath, ["-e", clientSource, url], {
env: clientEnvironment(caPath),
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("close", (status) => {
resolve({ status, stdout, stderr });
});
});
}
function listen(server) {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.off("error", reject);
resolve();
});
});
}
function close(server) {
return new Promise((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
try {
writeFileSync(
opensslConfig,
`[req]
distinguished_name = dn
prompt = no
[dn]
CN = localhost
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
`,
);
runOpenSsl(["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", "-subj", "/CN=OpenWork Test CA", "-keyout", caKey, "-out", caCert]);
runOpenSsl(["req", "-newkey", "rsa:2048", "-nodes", "-keyout", serverKey, "-out", serverCsr, "-config", opensslConfig]);
runOpenSsl(["x509", "-req", "-in", serverCsr, "-CA", caCert, "-CAkey", caKey, "-CAcreateserial", "-out", serverCert, "-days", "1", "-extensions", "v3_req", "-extfile", opensslConfig]);
const server = createServer(
{
key: readFileSync(serverKey),
cert: readFileSync(serverCert),
},
(_request, response) => {
response.end("openwork-custom-ca-ok");
},
);
await listen(server);
const address = server.address();
const url = `https://127.0.0.1:${address.port}/`;
try {
const withoutCustomCa = await runClient(url);
if (withoutCustomCa.status === 0) {
throw new Error("Strict TLS unexpectedly trusted the private CA without NODE_EXTRA_CA_CERTS");
}
const withCustomCa = await runClient(url, caCert);
if (withCustomCa.status !== 0) {
throw new Error(`Strict TLS did not trust NODE_EXTRA_CA_CERTS=${caCert}\n${withCustomCa.stderr}`);
}
console.log(`Strict TLS without NODE_EXTRA_CA_CERTS: rejected (${withoutCustomCa.stderr.trim()})`);
console.log(`Strict TLS with NODE_EXTRA_CA_CERTS=${caCert}: accepted (${withCustomCa.stdout.trim()})`);
console.log("Focused runtime trust check passed; this verifies Node's strict TLS trust path without starting MySQL.");
} finally {
await close(server);
}
} finally {
rmSync(tmp, { recursive: true, force: true });
}
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
set -euo pipefail
chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
assert_count() {
local file="$1"
local needle="$2"
local expected="$3"
local count
count="$(grep -F -c -- "$needle" "$file" || true)"
if [[ "$count" != "$expected" ]]; then
printf 'Expected %s occurrences of %s, found %s\n' "$expected" "$needle" "$count" >&2
return 1
fi
}
assert_contains() {
local file="$1"
local needle="$2"
if ! grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart to contain %s\n' "$needle" >&2
return 1
fi
}
assert_not_contains() {
local file="$1"
local needle="$2"
if grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart not to contain %s\n' "$needle" >&2
return 1
fi
}
assert_failure() {
local values_file="$1"
local expected="$2"
local output_file="$tmp_dir/failure-output.yaml"
local error_file="$tmp_dir/failure-error.txt"
if helm template openwork-ee "$chart_dir" -f "$values_file" > "$output_file" 2> "$error_file"; then
printf 'Expected helm template to fail for %s\n' "$values_file" >&2
return 1
fi
assert_contains "$error_file" "$expected"
}
default_rendered="$tmp_dir/default.yaml"
disabled_values="$tmp_dir/disabled-values.yaml"
disabled_rendered="$tmp_dir/disabled.yaml"
helm template openwork-ee "$chart_dir" --set inference.enabled=true > "$default_rendered"
cat > "$disabled_values" <<'YAML'
inference:
enabled: true
customCa:
enabled: false
existingSecret: ignored-ca-secret
key: ignored.pem
YAML
helm template openwork-ee "$chart_dir" -f "$disabled_values" > "$disabled_rendered"
cmp -s "$default_rendered" "$disabled_rendered"
assert_not_contains "$default_rendered" 'name: NODE_EXTRA_CA_CERTS'
assert_not_contains "$default_rendered" '/etc/openwork/custom-ca'
assert_not_contains "$default_rendered" 'name: custom-ca'
secret_values="$tmp_dir/secret-values.yaml"
secret_rendered="$tmp_dir/secret.yaml"
cat > "$secret_values" <<'YAML'
customCa:
enabled: true
existingSecret: openwork-ca-secret
key: corp-root.pem
YAML
helm template openwork-ee "$chart_dir" -f "$secret_values" > "$secret_rendered"
assert_count "$secret_rendered" 'name: NODE_EXTRA_CA_CERTS' 4
assert_count "$secret_rendered" 'value: "/etc/openwork/custom-ca/ca-bundle.pem"' 5
assert_count "$secret_rendered" 'name: custom-ca' 8
assert_count "$secret_rendered" 'secretName: "openwork-ca-secret"' 4
assert_count "$secret_rendered" 'key: "corp-root.pem"' 4
assert_count "$secret_rendered" 'path: ca-bundle.pem' 4
assert_count "$secret_rendered" 'mountPath: "/etc/openwork/custom-ca"' 4
assert_not_contains "$secret_rendered" 'subPath:'
assert_not_contains "$secret_rendered" 'name: "openwork-ca-config"'
assert_not_contains "$secret_rendered" 'name: openwork-ee-inference'
configmap_values="$tmp_dir/configmap-values.yaml"
configmap_rendered="$tmp_dir/configmap.yaml"
cat > "$configmap_values" <<'YAML'
inference:
enabled: true
customCa:
enabled: true
existingConfigMap: openwork-ca-config
key: ca.crt
YAML
helm template openwork-ee "$chart_dir" -f "$configmap_values" > "$configmap_rendered"
assert_contains "$configmap_rendered" 'name: openwork-ee-den-api'
assert_contains "$configmap_rendered" 'name: openwork-ee-den-web'
assert_contains "$configmap_rendered" 'name: openwork-ee-inference'
assert_contains "$configmap_rendered" 'name: openwork-ee-migrate'
assert_count "$configmap_rendered" 'name: NODE_EXTRA_CA_CERTS' 5
assert_count "$configmap_rendered" 'value: "/etc/openwork/custom-ca/ca-bundle.pem"' 6
assert_count "$configmap_rendered" 'name: custom-ca' 10
assert_count "$configmap_rendered" 'name: "openwork-ca-config"' 5
assert_count "$configmap_rendered" 'key: "ca.crt"' 5
assert_count "$configmap_rendered" 'path: ca-bundle.pem' 5
assert_count "$configmap_rendered" 'mountPath: "/etc/openwork/custom-ca"' 5
assert_not_contains "$configmap_rendered" 'subPath:'
assert_not_contains "$configmap_rendered" 'secretName: "openwork-ca-secret"'
installer_values="$tmp_dir/installer-values.yaml"
installer_rendered="$tmp_dir/installer.yaml"
cat > "$installer_values" <<'YAML'
customCa:
enabled: true
existingSecret: openwork-ca-secret
installerArtifacts:
enabled: true
existingClaim: installer-artifacts-pvc
YAML
helm template openwork-ee "$chart_dir" -f "$installer_values" > "$installer_rendered"
assert_contains "$installer_rendered" 'name: installer-artifacts'
assert_contains "$installer_rendered" 'claimName: "installer-artifacts-pvc"'
assert_contains "$installer_rendered" 'name: OPENWORK_INSTALLER_ARTIFACTS_DIR'
assert_contains "$installer_rendered" 'name: custom-ca'
assert_contains "$installer_rendered" 'name: NODE_EXTRA_CA_CERTS'
missing_source_values="$tmp_dir/missing-source-values.yaml"
cat > "$missing_source_values" <<'YAML'
customCa:
enabled: true
YAML
assert_failure "$missing_source_values" 'customCa.existingSecret or customCa.existingConfigMap is required when customCa.enabled=true'
multiple_sources_values="$tmp_dir/multiple-sources-values.yaml"
cat > "$multiple_sources_values" <<'YAML'
customCa:
enabled: true
existingSecret: openwork-ca-secret
existingConfigMap: openwork-ca-config
YAML
assert_failure "$multiple_sources_values" 'customCa.existingSecret and customCa.existingConfigMap are mutually exclusive when customCa.enabled=true'
empty_key_values="$tmp_dir/empty-key-values.yaml"
cat > "$empty_key_values" <<'YAML'
customCa:
enabled: true
existingSecret: openwork-ca-secret
key: ""
YAML
assert_failure "$empty_key_values" 'customCa.key is required when customCa.enabled=true'
den_api_conflict_values="$tmp_dir/den-api-conflict-values.yaml"
cat > "$den_api_conflict_values" <<'YAML'
customCa:
enabled: true
existingSecret: openwork-ca-secret
denApi:
env:
NODE_EXTRA_CA_CERTS: /already/set.pem
YAML
assert_failure "$den_api_conflict_values" 'denApi.env.NODE_EXTRA_CA_CERTS conflicts with customCa.enabled=true; remove it and use customCa instead'
den_web_conflict_values="$tmp_dir/den-web-conflict-values.yaml"
cat > "$den_web_conflict_values" <<'YAML'
customCa:
enabled: true
existingSecret: openwork-ca-secret
denWeb:
env:
NODE_EXTRA_CA_CERTS: /already/set.pem
YAML
assert_failure "$den_web_conflict_values" 'denWeb.env.NODE_EXTRA_CA_CERTS conflicts with customCa.enabled=true; remove it and use customCa instead'
inference_conflict_values="$tmp_dir/inference-conflict-values.yaml"
cat > "$inference_conflict_values" <<'YAML'
customCa:
enabled: true
existingSecret: openwork-ca-secret
inference:
env:
NODE_EXTRA_CA_CERTS: /already/set.pem
YAML
assert_failure "$inference_conflict_values" 'inference.env.NODE_EXTRA_CA_CERTS conflicts with customCa.enabled=true; remove it and use customCa instead'
node "$chart_dir/tests/custom-ca-node-trust.mjs"
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
assert_contains() {
local file="$1"
local needle="$2"
if ! grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart to contain %s\n' "$needle" >&2
return 1
fi
}
default_rendered="$tmp_dir/default.yaml"
helm template openwork-ee "$chart_dir" > "$default_rendered"
assert_contains "$default_rendered" 'DEN_DASHBOARDS_ENABLED: "false"'
enabled_values="$tmp_dir/enabled-values.yaml"
enabled_rendered="$tmp_dir/enabled.yaml"
printf '%s\n' 'config:' ' public:' ' dashboardsEnabled: "true"' > "$enabled_values"
helm template openwork-ee "$chart_dir" -f "$enabled_values" > "$enabled_rendered"
assert_contains "$enabled_rendered" 'DEN_DASHBOARDS_ENABLED: "true"'
printf 'dashboards-enabled chart checks passed\n'
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
set -euo pipefail
chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
repo_root="$(cd "$chart_dir/../../.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
assert_count() {
local file="$1"
local needle="$2"
local expected="$3"
local count
count="$(grep -F -c -- "$needle" "$file" || true)"
if [[ "$count" != "$expected" ]]; then
printf 'Expected %s occurrences of %s, found %s\n' "$expected" "$needle" "$count" >&2
return 1
fi
}
assert_contains() {
local file="$1"
local needle="$2"
if ! grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart to contain %s\n' "$needle" >&2
return 1
fi
}
assert_not_contains() {
local file="$1"
local needle="$2"
if grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart not to contain %s\n' "$needle" >&2
return 1
fi
}
assert_file_contains() {
assert_contains "$1" "$2"
}
assert_file_not_contains() {
assert_not_contains "$1" "$2"
}
default_rendered="$tmp_dir/default.yaml"
helm template openwork-ee "$chart_dir" --set inference.enabled=true > "$default_rendered"
assert_contains "$default_rendered" 'DEN_API_NODE_OPTIONS: ""'
assert_count "$default_rendered" 'name: NODE_OPTIONS' 2
assert_count "$default_rendered" 'valueFrom: null' 1
assert_not_contains "$default_rendered" 'key: DEN_API_NODE_OPTIONS'
assert_count "$default_rendered" 'name: DEN_OBSERVABILITY_BACKEND' 2
assert_count "$default_rendered" 'value: "none"' 2
assert_not_contains "$default_rendered" 'name: OTEL_EXPORTER_OTLP_HEADERS'
assert_not_contains "$default_rendered" 'name: SENTRY_DSN'
otel_values="$tmp_dir/otel-values.yaml"
otel_rendered="$tmp_dir/otel.yaml"
cat > "$otel_values" <<'YAML'
inference:
enabled: true
config:
denApiNodeOptions: --max-old-space-size=4096
observability:
backend: otel
otel:
endpoint: https://otel.example.test/otlp
tracesEndpoint: https://otel.example.test/v1/traces
metricsEndpoint: https://otel.example.test/v1/metrics
logsEndpoint: https://otel.example.test/v1/logs
exporters:
metrics: none
tracesSampler: parentbased_always_on
headers:
existingSecret: otel-runtime
key: otlp-headers
denApi:
env:
CUSTOM_DEN_API_ENV: api
denWeb:
env:
CUSTOM_DEN_WEB_ENV: web
YAML
helm template openwork-ee "$chart_dir" -f "$otel_values" > "$otel_rendered"
assert_contains "$otel_rendered" 'DEN_API_NODE_OPTIONS: "--max-old-space-size=4096"'
assert_contains "$otel_rendered" 'value: "--max-old-space-size=4096"'
assert_count "$otel_rendered" 'name: DEN_OBSERVABILITY_BACKEND' 2
assert_count "$otel_rendered" 'value: "otel"' 2
assert_count "$otel_rendered" 'name: OTEL_EXPORTER_OTLP_HEADERS' 2
assert_count "$otel_rendered" 'name: OTEL_SERVICE_NAME' 2
assert_count "$otel_rendered" 'name: OTEL_METRICS_EXPORTER' 2
assert_count "$otel_rendered" 'value: "parentbased_always_on"' 2
assert_not_contains "$otel_rendered" 'name: OTEL_TRACES_SAMPLER_ARG'
assert_contains "$otel_rendered" 'value: "den-api"'
assert_contains "$otel_rendered" 'value: "den-web"'
assert_contains "$otel_rendered" 'name: "otel-runtime"'
assert_contains "$otel_rendered" 'key: "otlp-headers"'
assert_contains "$otel_rendered" 'name: CUSTOM_DEN_API_ENV'
assert_contains "$otel_rendered" 'name: CUSTOM_DEN_WEB_ENV'
assert_not_contains "$otel_rendered" 'name: SENTRY_AUTH_TOKEN'
legacy_values="$tmp_dir/legacy-node-options-values.yaml"
legacy_rendered="$tmp_dir/legacy-node-options.yaml"
cat > "$legacy_values" <<'YAML'
denApi:
env:
NODE_OPTIONS: --tls-max-v1.2
YAML
helm template openwork-ee "$chart_dir" -f "$legacy_values" > "$legacy_rendered"
assert_count "$legacy_rendered" 'name: NODE_OPTIONS' 2
assert_contains "$legacy_rendered" 'value: "--tls-max-v1.2"'
assert_not_contains "$legacy_rendered" 'valueFrom: null'
assert_not_contains "$legacy_rendered" 'key: DEN_API_NODE_OPTIONS'
sentry_values="$tmp_dir/sentry-values.yaml"
sentry_rendered="$tmp_dir/sentry.yaml"
cat > "$sentry_values" <<'YAML'
inference:
enabled: true
observability:
backend: sentry
sentry:
dsnSecret:
existingSecret: sentry-runtime
key: dsn
tracesSampleRate: "0.75"
environment: production
release: "2026.07.11"
dist: web
YAML
helm template openwork-ee "$chart_dir" -f "$sentry_values" > "$sentry_rendered"
assert_count "$sentry_rendered" 'name: DEN_OBSERVABILITY_BACKEND' 2
assert_count "$sentry_rendered" 'value: "sentry"' 2
assert_count "$sentry_rendered" 'name: SENTRY_DSN' 2
assert_contains "$sentry_rendered" 'name: "sentry-runtime"'
assert_not_contains "$sentry_rendered" 'SENTRY_AUTH_TOKEN'
assert_not_contains "$sentry_rendered" 'SENTRY_ORG'
assert_not_contains "$sentry_rendered" 'SENTRY_PROJECT'
assert_not_contains "$sentry_rendered" 'SENTRY_URL'
assert_not_contains "$sentry_rendered" 'name: OTEL_EXPORTER_OTLP_HEADERS'
assert_file_contains "$repo_root/packaging/docker/Dockerfile.den" '/app/ee/apps/den-api/dist/main.js'
assert_file_not_contains "$repo_root/packaging/docker/Dockerfile.den" '/app/ee/apps/den-api/dist/server.js'
assert_file_contains "$repo_root/packaging/docker/docker-compose.den-dev.yml" '/app/ee/apps/den-api/dist/main.js'
assert_file_not_contains "$repo_root/packaging/docker/docker-compose.den-dev.yml" '/app/ee/apps/den-api/dist/server.js'
assert_file_contains "$repo_root/.devcontainer/start-daytona-server.sh" 'pnpm --filter @openwork-ee/den-api exec tsx watch src/main.ts'
assert_file_not_contains "$repo_root/.devcontainer/start-daytona-server.sh" 'ee/apps/den-api/src/server.ts'
assert_file_contains "$repo_root/evals/packages/hosts/src/den-stack.ts" '"tsx", "src/main.ts"'
assert_file_not_contains "$repo_root/evals/packages/hosts/src/den-stack.ts" '"tsx", "src/server.ts"'
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
assert_contains() {
local file="$1"
local needle="$2"
if ! grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart to contain %s\n' "$needle" >&2
return 1
fi
}
default_rendered="$tmp_dir/default.yaml"
helm template openwork-ee "$chart_dir" > "$default_rendered"
assert_contains "$default_rendered" 'DEN_OPENWORK_WEB_ENABLED: "false"'
enabled_values="$tmp_dir/enabled-values.yaml"
enabled_rendered="$tmp_dir/enabled.yaml"
printf '%s\n' 'config:' ' public:' ' openworkWebEnabled: "true"' > "$enabled_values"
helm template openwork-ee "$chart_dir" -f "$enabled_values" > "$enabled_rendered"
assert_contains "$enabled_rendered" 'DEN_OPENWORK_WEB_ENABLED: "true"'
printf 'openwork-web-enabled chart checks passed\n'
@@ -0,0 +1,173 @@
#!/usr/bin/env bash
set -euo pipefail
chart_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
assert_count() {
local file="$1"
local needle="$2"
local expected="$3"
local count
count="$(grep -F -c -- "$needle" "$file" || true)"
if [[ "$count" != "$expected" ]]; then
printf 'Expected %s occurrences of %s, found %s\n' "$expected" "$needle" "$count" >&2
return 1
fi
}
assert_contains() {
local file="$1"
local needle="$2"
if ! grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart to contain %s\n' "$needle" >&2
return 1
fi
}
assert_not_contains() {
local file="$1"
local needle="$2"
if grep -F -q -- "$needle" "$file"; then
printf 'Expected rendered chart not to contain %s\n' "$needle" >&2
return 1
fi
}
assert_failure() {
local values_file="$1"
local expected="$2"
local output_file="$tmp_dir/failure-output.yaml"
local error_file="$tmp_dir/failure-error.txt"
if helm template openwork-ee "$chart_dir" -f "$values_file" > "$output_file" 2> "$error_file"; then
printf 'Expected helm template to fail for %s\n' "$values_file" >&2
return 1
fi
assert_contains "$error_file" "$expected"
}
assert_source_contains() {
local file="$1"
local source="$2"
local needle="$3"
local in_source=0
local found=0
local line
while IFS= read -r line; do
if [[ "$line" == '# Source: openwork-ee/templates/'* ]]; then
if [[ "$line" == "# Source: openwork-ee/templates/$source" ]]; then
in_source=1
else
in_source=0
fi
fi
if [[ "$in_source" == 1 && "$line" == *"$needle"* ]]; then
found=1
break
fi
done < "$file"
if [[ "$found" != 1 ]]; then
printf 'Expected %s to contain %s\n' "$source" "$needle" >&2
return 1
fi
}
assert_source_not_contains() {
local file="$1"
local source="$2"
local needle="$3"
local in_source=0
local line
while IFS= read -r line; do
if [[ "$line" == '# Source: openwork-ee/templates/'* ]]; then
if [[ "$line" == "# Source: openwork-ee/templates/$source" ]]; then
in_source=1
else
in_source=0
fi
fi
if [[ "$in_source" == 1 && "$line" == *"$needle"* ]]; then
printf 'Expected %s not to contain %s\n' "$source" "$needle" >&2
return 1
fi
done < "$file"
}
default_rendered="$tmp_dir/default.yaml"
helm template openwork-ee "$chart_dir" > "$default_rendered"
assert_count "$default_rendered" 'DEN_ALLOW_PRIVATE_MCP_URLS' 0
assert_not_contains "$default_rendered" 'openwork-ee-den-api-private-mcp-urls'
enabled_values="$tmp_dir/enabled-values.yaml"
enabled_rendered="$tmp_dir/enabled.yaml"
cat > "$enabled_values" <<'YAML'
inference:
enabled: true
config:
public:
allowPrivateMcpUrls: "1"
YAML
helm template openwork-ee "$chart_dir" -f "$enabled_values" > "$enabled_rendered"
assert_count "$enabled_rendered" 'DEN_ALLOW_PRIVATE_MCP_URLS' 2
assert_count "$enabled_rendered" 'name: DEN_ALLOW_PRIVATE_MCP_URLS' 2
assert_count "$enabled_rendered" 'value: "1"' 2
assert_not_contains "$enabled_rendered" 'key: DEN_ALLOW_PRIVATE_MCP_URLS'
assert_not_contains "$enabled_rendered" 'configMapKeyRef:'
assert_not_contains "$enabled_rendered" 'openwork-ee-den-api-private-mcp-urls'
assert_source_contains "$enabled_rendered" 'den-api.yaml' 'name: DEN_ALLOW_PRIVATE_MCP_URLS'
assert_source_contains "$enabled_rendered" 'den-api.yaml' 'value: "1"'
assert_source_not_contains "$enabled_rendered" 'den-web.yaml' 'DEN_ALLOW_PRIVATE_MCP_URLS'
assert_source_not_contains "$enabled_rendered" 'inference.yaml' 'DEN_ALLOW_PRIVATE_MCP_URLS'
disabled_values="$tmp_dir/disabled-values.yaml"
disabled_rendered="$tmp_dir/disabled.yaml"
cat > "$disabled_values" <<'YAML'
config:
public:
allowPrivateMcpUrls: "0"
YAML
helm template openwork-ee "$chart_dir" -f "$disabled_values" > "$disabled_rendered"
assert_count "$disabled_rendered" 'DEN_ALLOW_PRIVATE_MCP_URLS' 0
false_values="$tmp_dir/false-values.yaml"
false_rendered="$tmp_dir/false.yaml"
cat > "$false_values" <<'YAML'
config:
public:
allowPrivateMcpUrls: "false"
YAML
helm template openwork-ee "$chart_dir" -f "$false_values" > "$false_rendered"
assert_count "$false_rendered" 'DEN_ALLOW_PRIVATE_MCP_URLS' 0
blank_values="$tmp_dir/blank-values.yaml"
blank_rendered="$tmp_dir/blank.yaml"
cat > "$blank_values" <<'YAML'
config:
public:
allowPrivateMcpUrls: ""
YAML
helm template openwork-ee "$chart_dir" -f "$blank_values" > "$blank_rendered"
assert_count "$blank_rendered" 'DEN_ALLOW_PRIVATE_MCP_URLS' 0
invalid_true_values="$tmp_dir/invalid-true-values.yaml"
cat > "$invalid_true_values" <<'YAML'
config:
public:
allowPrivateMcpUrls: "true"
YAML
assert_failure "$invalid_true_values" 'config.public.allowPrivateMcpUrls must be blank, 0, false, or "1"'
invalid_yes_values="$tmp_dir/invalid-yes-values.yaml"
cat > "$invalid_yes_values" <<'YAML'
config:
public:
allowPrivateMcpUrls: "yes"
YAML
assert_failure "$invalid_yes_values" 'config.public.allowPrivateMcpUrls must be blank, 0, false, or "1"'
printf 'private-mcp-urls chart checks passed\n'
+322
View File
@@ -0,0 +1,322 @@
nameOverride: ""
fullnameOverride: ""
image:
tag: latest
pullPolicy: IfNotPresent
imagePullSecrets: []
config:
databaseMode: mysql
openworkDevMode: "0"
# Passed to the den-api Node.js process as NODE_OPTIONS at container startup.
denApiNodeOptions: ""
tenancy:
mode: single_org
singleOrgName: OpenWork
singleOrgSlug: default
ownerEmails: ""
allowPublicSignup: "false"
requireEmailVerification: "false"
auth:
passwordBreachScreeningEnabled: "false"
githubConnector:
appId: ""
clientId: ""
public:
# Single public Den web origin. The chart passes this as DEN_BASE_URL; den-api
# derives Better Auth, CORS/trusted origins, web-app hosts, API defaults, and
# MCP resource defaults from it unless the compatibility overrides below are set.
webOrigin: https://openwork.example.com
# Compatibility overrides for split-origin or migration deployments.
apiOrigin: ""
mcpResourceUrl: ""
mcpAdditionalResources: ""
# Leave blank to keep External MCP URL SSRF protection enabled.
# Set "1" only for trusted private-network MCP servers.
allowPrivateMcpUrls: ""
mcpClaimNamespace: ""
desktopDenBaseUrl: ""
corsOrigins: ""
betterAuthCookieDomain: ""
betterAuthTrustedOrigins: ""
webAppHosts: ""
bootstrapAdminEmails: ""
installLinksGatingEnabled: "false"
# Self-hosted Automations are opt-in. Hosted OpenWork Cloud enables them.
automationsEnabled: "false"
# Self-hosted Dashboards are opt-in. Hosted environments use the same Den flag.
dashboardsEnabled: "false"
# OpenWork Web is hosted-only by default. OpenWork Cloud enables it explicitly.
openworkWebEnabled: "false"
# Preserve the Den runtime for published legacy Desktops during upgrades.
# Set false only after every Desktop is v0.18.35+ to hard-disable Automations.
automationsRuntimeEnabled: "true"
# Enable only after compatible Desktop MCP Apps hosts are deployed.
generatedArtifactViewsEnabled: "false"
connectLinkMode: exchange
connectLinkKeyId: ""
openworkAppConnectUrl: ""
openworkWebUrl: ""
installerReleaseTag: ""
installerReleaseRepo: ""
authCallbackUrl: https://openwork.example.com
internal:
apiBaseUrl: ""
authFallbackBaseUrl: ""
inferenceProxyBaseUrl: ""
provisioner:
mode: stub
reconcileIntervalMs: "60000"
reconcileStaleMs: "1200000"
reconcileBatchSize: "10"
workerUrlTemplate: ""
daytona:
apiUrl: ""
target: ""
snapshot: ""
sandboxImage: ""
sandboxCpu: ""
sandboxMemory: ""
sandboxDisk: ""
sandboxPublic: ""
sandboxAutoStopInterval: ""
sandboxAutoArchiveInterval: ""
sandboxAutoDeleteInterval: ""
signedPreviewExpiresSeconds: "86400"
sandboxNamePrefix: ""
sharedVolumeName: ""
volumeNamePrefix: ""
workspaceMountPath: ""
dataMountPath: ""
runtimeWorkspacePath: ""
runtimeDataPath: ""
sidecarDir: ""
openworkPort: ""
opencodePort: ""
createTimeoutSeconds: ""
deleteTimeoutSeconds: ""
healthcheckTimeoutMs: ""
polar:
featureGateEnabled: "false"
apiBase: ""
productId: ""
benefitId: ""
successUrl: ""
returnUrl: ""
inference:
openRouterWorkspaceId: ""
stripeInferencePriceId: ""
openRouterUpstreamUrl: ""
creditsPerDollar: ""
web:
posthogKey: ""
posthogHost: ""
observability:
backend: none
serviceNames:
denApi: den-api
denWeb: den-web
otel:
endpoint: ""
tracesEndpoint: ""
metricsEndpoint: ""
logsEndpoint: ""
protocol: http/protobuf
exporters:
traces: otlp
metrics: otlp
logs: otlp
tracesSampler: parentbased_always_on
tracesSamplerArg: ""
headers:
existingSecret: ""
key: OTEL_EXPORTER_OTLP_HEADERS
sentry:
dsn: ""
dsnSecret:
existingSecret: ""
key: SENTRY_DSN
tracesSampleRate: "0.01"
environment: ""
release: ""
dist: ""
customCa:
enabled: false
existingSecret: ""
existingConfigMap: ""
key: ca.crt
redis:
# Prefer rediss:// DATABASE_REDIS_URL values. Set this to true only for
# trusted private, non-public Redis endpoints that do not support TLS, such as
# some managed internal Redis URLs exposed by hosting platforms.
allowInsecureInternal: false
secret:
create: true
existingSecret: ""
keys:
databaseUrl: DATABASE_URL
databaseRedisUrl: DATABASE_REDIS_URL
betterAuthSecret: BETTER_AUTH_SECRET
denDbEncryptionKey: DEN_DB_ENCRYPTION_KEY
daytonaApiKey: DAYTONA_API_KEY
polarAccessToken: POLAR_ACCESS_TOKEN
openRouterManagementApiKey: OPENROUTER_MANAGEMENT_API_KEY
inferenceAdminToken: INFERENCE_ADMIN_TOKEN
inferenceWebhookSecret: INFERENCE_WEBHOOK_SECRET
emailFrom: EMAIL_FROM
smtpHost: SMTP_HOST
smtpPort: SMTP_PORT
smtpUser: SMTP_USER
smtpPass: SMTP_PASS
smtpSecure: SMTP_SECURE
githubConnectorAppClientSecret: GITHUB_CONNECTOR_APP_CLIENT_SECRET
githubConnectorAppPrivateKey: GITHUB_CONNECTOR_APP_PRIVATE_KEY
githubConnectorAppWebhookSecret: GITHUB_CONNECTOR_APP_WEBHOOK_SECRET
connectLinkPrivateKey: DEN_CONNECT_LINK_PRIVATE_KEY
initialAdminBootstrapCode: DEN_INITIAL_ADMIN_BOOTSTRAP_CODE
values:
databaseUrl: mysql://openwork:change-me@mysql:3306/openwork_den
databaseRedisUrl: ""
betterAuthSecret: CHANGE_ME_32_CHARS_MINIMUM_BETTER_AUTH
denDbEncryptionKey: CHANGE_ME_32_CHARS_MINIMUM_DB_ENCRYPTION
daytonaApiKey: ""
polarAccessToken: ""
openRouterManagementApiKey: ""
inferenceAdminToken: ""
inferenceWebhookSecret: ""
emailFrom: ""
smtpHost: ""
smtpPort: "587"
smtpUser: ""
smtpPass: ""
smtpSecure: "false"
githubConnectorAppClientSecret: ""
githubConnectorAppPrivateKey: ""
githubConnectorAppWebhookSecret: ""
connectLinkPrivateKey: ""
initialAdminBootstrapCode: ""
installerArtifacts:
enabled: false
existingClaim: ""
hostPath: ""
mountPath: /var/lib/openwork/installer-artifacts
denApi:
replicaCount: 1
image:
repository: ghcr.io/different-ai/openwork-den-api
tag: ""
service:
type: ClusterIP
port: 8788
annotations: {}
loadBalancerClass: ""
loadBalancerSourceRanges: []
containerPort: 8788
env: {}
podAnnotations: {}
podLabels: {}
resources: {}
probes:
liveness:
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
readiness:
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 12
denWeb:
replicaCount: 1
image:
repository: ghcr.io/different-ai/openwork-den-web
tag: ""
service:
type: ClusterIP
port: 3005
annotations: {}
loadBalancerClass: ""
loadBalancerSourceRanges: []
containerPort: 3005
env: {}
podAnnotations: {}
podLabels: {}
resources: {}
probes:
liveness:
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 6
readiness:
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 10
failureThreshold: 18
inference:
enabled: false
replicaCount: 1
image:
repository: ghcr.io/different-ai/openwork-inference
tag: ""
service:
type: ClusterIP
port: 8791
annotations: {}
loadBalancerClass: ""
loadBalancerSourceRanges: []
containerPort: 8791
env: {}
podAnnotations: {}
podLabels: {}
resources: {}
probes:
liveness:
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
readiness:
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 12
migrations:
enabled: true
hook: true
hookDeletePolicy: before-hook-creation,hook-succeeded
backoffLimit: 2
activeDeadlineSeconds: 600
podAnnotations: {}
resources: {}
command:
- node
args:
- /app/ee/packages/den-db/dist/scripts/bootstrap.js
ingress:
enabled: false
className: ""
annotations: {}
web:
host: openwork.example.com
path: /
pathType: Prefix
api:
enabled: true
host: api.openwork.example.com
path: /
pathType: Prefix
tls: []