Single Helm chart repository for all AgentWorkforce services. Each service lives under charts/<service-name>/.
| Chart | Description |
|---|---|
| agent-relay | Self-hosted Agent Relay suite umbrella — relaycast, relayauth, sessions-pipeline, relayhistory, control-plane API, dashboard (one GKE Ingress, no HA claim) |
| relay-cloud-api | Self-host control-plane API — organizations, workspaces, device auth; existing-Secret file mounts only, separate migration Job |
| relay-dashboard | Static self-host dashboard — nginx SPA calling same-origin /cloud/api |
| relayhistory | Relay History data plane — session delivery, search, replay on its own Postgres database (digest-pinned proof image) |
| relaycast | Portable self-hosted Relaycast core — realtime agent communication with single-writer SQLite persistence |
| relayfile | Relayfile server — single Go binary, HTTP on :8080, Postgres-backed in production |
| relayflows | Durable workflows running locally in Kubernetes, with optional Cloud assignment mode |
| sessions-pipeline-worker | Sessions Pipeline workers: HoopDb migrations, Locker bucket init, and the indexer/referee/bob-costas stage workers |
| sessions-pipeline | The whole Sessions Pipeline in one release: sessions-pipeline-worker + alleyoop, with optional HoopDb (CloudNativePG), Locker (MinIO) and an OTel collector |
| alleyoop | Sessions Pipeline ingest: receives session manifests and chunks and lands them in the Locker (S3) |
| relayauth | Self-hosted RelayAuth issuer: immutable image, externally managed keys, single-writer SQLite, and optional JWKS ingress |
self-hosted/requirements.yaml is the single
source of truth for what a self-hosted install needs: every requirement
(Secrets with exact names and keys, database roles and extensions, DNS/TLS,
workload identity, cluster capabilities) and every option (HoopDb backend,
databaseMode, function boundary, search backend, ingress scheme, add-ons),
per install profile, with the chart values, env vars and Secret keys each maps
to, who provides it, how to verify it, and file:line sources. It also holds the
compatibility matrix: chart versions, services image digests, the
sessions-pipeline commit and migration generation, and the supported
databaseMode + boundary combinations. The readable form,
docs/self-hosted-requirements.md, is
generated from it.
To add or change an entry, edit the YAML (the field reference is at the top of the file), then:
node ci/self-hosted-requirements/registry.mjs render # regenerate the doc
node ci/self-hosted-requirements/registry.mjs check # what CI runsCI (lint.yml, static job) fails when the registry and the charts disagree:
a template env var, Secret key or Secret values path that is not registered; a
shipped entry whose values path, env var or Secret name/key the charts do not
have; a template that references a planned entry; a matrix digest, commit,
generation, chart version or release tag that differs from the pins; or a stale
generated doc. A new chart version needs a current row in
compatibility.releases (status: unreleased until its tag exists).
helm repo add agentworkforce https://AgentWorkforce.github.io/helm-charts
helm repo updateRelaycast is the minimal self-hosted core. Relayflows and the Sessions Pipeline are optional add-ons and are not installed with it:
kubectl create namespace relaycast
kubectl -n relaycast create secret generic relaycast-secrets \
--from-literal=internal-secret="$(openssl rand -hex 32)" \
--from-literal=workspace-bootstrap-secret="$(openssl rand -hex 32)"
helm install relaycast agentworkforce/relaycast -n relaycast \
--set baseUrl=https://relay.example.com \
--set existingSecret=relaycast-secretsAnonymous workspace creation is off; see
charts/relaycast/README.md for the first-run
workspace bootstrap, ingress, backups and restore, and upgrading from 0.1.0. The staged AWS-first/GCP-second adapter
plan and its proof gates are in
docs/self-hosted-reference-architecture.md.
That document also records the transcript search decision: PostgreSQL 17 with
pg_trgm is the default backend (postgres). ClickHouse OSS is an optional
scale profile and is not charted yet.
What each install profile must provide and can choose is in
docs/self-hosted-requirements.md.
helm install relayfile agentworkforce/relayfile \
--set secrets.internalHmacSecret=<strong-secret> \
--set secrets.productionDsn='postgres://user:pass@host:5432/dbname?sslmode=require' \
--set auth.jwksUrl=https://auth.relay.example.com/.well-known/jwks.jsonSee charts/relayfile/README.md for the full parameter reference.
Bundle a flow, the Relayflows runtime CLI, and its harness CLIs into an image, then run it as a Kubernetes Job. This default mode does not use Agent Relay Cloud:
helm install relayflows agentworkforce/relayflows \
--namespace relayflows --create-namespace \
--set image.repository=registry.example.com/acme/my-flow \
--set image.tag=sha-0123456789abcdef \
--set runtimeInstaller.enabled=false \
--set standalone.flow.path=/app/flows/customer.flow.tsSee charts/relayflows/README.md for ConfigMap flows, model credentials, resuming runs, and optional Cloud worker mode.
One release runs everything. Prerequisite: the CloudNativePG operator (see
deps/PINS.md). Then create one Secret and install:
kubectl create namespace sessions-pipeline
read -rsp 'SUMMARY_MASTER_KEYS (<key-id>:<64-hex>): ' SUMMARY_MASTER_KEYS; printf '\n'
printf %s "$SUMMARY_MASTER_KEYS" | kubectl -n sessions-pipeline create secret generic sessions-pipeline-locker \
--from-literal=rootUser=<user> \
--from-literal=rootPassword=<password> \
--from-file=SUMMARY_MASTER_KEYS=/dev/stdin
unset SUMMARY_MASTER_KEYS
helm install sessions-pipeline agentworkforce/sessions-pipeline -n sessions-pipeline \
--set global.tenant.orgId=<org> \
--set alleyoop.auth.jwksUrl=https://<relayauth>/.well-known/jwks.jsonThe umbrella was renamed from 96bulls (last released as 0.5.4) to
sessions-pipeline (0.6.0). An existing release upgrades in place:
helm upgrade <release> agentworkforce/sessions-pipeline -f <your values>.
Resource names and selectors are unchanged, because the worker subchart alias
is still worker. Only the helm.sh/chart and app.kubernetes.io/part-of
labels change. Two renamed defaults need a value if you relied on them. Set
worker.existingSecret, alleyoop.existingSecret and
locker.existingSecret to your current Secret name (the old default was
96bulls-locker). Set
worker.migrations.runtimeRole to the role your database already has. A
standalone 96bulls-worker release keeps its selectors only with
nameOverride: 96bulls-worker; otherwise, install it fresh. Under the
agent-relay umbrella (0.3.0), move your 96bulls: overrides to
sessions-pipeline:.
The charts default to ghcr.io/agentworkforce/96bulls-services, pinned by
digest. That hosted-service package is private. For direct installs, create a
docker-registry Secret and add --set worker.imagePullSecrets[0].name=<secret> --set alleyoop.imagePullSecrets[0].name=<secret>. When overriding either app's
image repository or tag, also clear its shipped digest with
--set worker.image.digest= --set alleyoop.image.digest= (or supply matching
digests). Hosted EKS installs instead set both worker.image.repository and
alleyoop.image.repository to the fully qualified ECR repository plus its
matching digest; EKS nodes authenticate through their node role, so that path
does not need an image pull Secret. The OpenTelemetry collector is off by default; set
global.otel.endpoint to your backend, or set both otel.enabled=true and
global.otel.endpoint=http://otel-collector:4318 to use the bundled collector.
Its safe default exporter drops telemetry; replace otel.config.exporters and
the pipeline exporters with your backend configuration outside local testing.
Bob Costas reads SUMMARY_MASTER_KEYS from a Secret and stores only encrypted
summary objects in the Locker; no key value belongs in Helm values. The worker
chart projects that Secret key as a read-only file into Bob Costas only. Set
worker.summaryEncryption.existingSecret when the key is not in the shared
Secret (required for pod-identity installs with no worker.existingSecret).
HoopDb credentials come from the Secret CloudNativePG generates
(hoopdb-app). Bring your own Postgres or S3 by disabling hoopdb or
locker and pointing worker.* / alleyoop.* at your infrastructure. On EKS,
set both app charts' locker.credentials.mode=podIdentity to omit
LOCKER_ACCESS_KEY_ID and LOCKER_SECRET_ACCESS_KEY; the AWS SDK then uses its
workload identity credential chain. Leave locker.endpoint empty to use the
native AWS S3 endpoint for locker.region. Unless the cluster uses EKS Auto
Mode, first install the
EKS Pod Identity Agent.
Create the IAM associations for the rendered worker and alleyoop
ServiceAccounts before deploying the workloads, because EKS injects the Pod
Identity configuration when each Pod starts. If associations are added later,
restart the app Pods and recreate any failed locker-init Job. If the S3 bucket
is already created and versioned, also set worker.lockerInit.enabled=false;
otherwise the worker role needs permission to create the bucket and enable
versioning.
ci/sessions-pipeline-umbrella/smoke.sh runs the umbrella on kind.
worker.migrations carries the migrator's database profile: sessions-pipeline
src/hoopdb/migrate.ts migrationProfile() at main
7d7229bb31c121ecb98cd4e35c248fc2cf5669ad (#124 session list RPC, #125 the
Supabase event-trigger function boundary, #139 its manifest sync, #137 the exact sessions function ACL check in every
non-local boundary mode). Each value is rendered at most
once, as an env var in the migrate container only. The render refuses these
names in worker.extraEnv and component env, along with
HOOPDB_ROLE_BOOTSTRAP and the old HOOPDB_FUNCTION_BOUNDARY_APPROVED name.
Where the migrator has no default, neither does the chart:
| value | env | rule |
|---|---|---|
databaseMode |
HOOPDB_DB_MODE |
Required: local, selfhosted, cloudsql (Google Cloud SQL) or supabase. |
supabaseShim |
HOOPDB_SUPABASE_SHIM |
Required in local, selfhosted and cloudsql. In supabase mode it must be unset or false, and it is rendered false. |
roleBootstrap |
HOOPDB_ROLE_BOOTSTRAP |
Optional (the migrator defaults it to the shim choice). Refused as true in supabase. Cloud SQL sets false. |
runtimeRole |
HOOPDB_RUNTIME_ROLE |
Required. Outside local mode it must be a dedicated role, never the migration owner or the search Worker. |
runtimeRoleLogin |
HOOPDB_RUNTIME_ROLE_LOGIN |
Required outside local mode; rendered only when set. |
runtimeWorkloadRole |
HOOPDB_RUNTIME_WORKLOAD_ROLE |
Optional. |
functionBoundaryMode |
HOOPDB_FUNCTION_BOUNDARY_MODE |
global-default-revoke, or event-trigger in supabase mode only (refused in local, selfhosted and cloudsql, as the migrator refuses it). Required in selfhosted and cloudsql. Optional in supabase; unset, migrate fails closed. |
charts/sessions-pipeline/profiles/ ships four render shapes:
-
selfhost-postgres.yaml: a new install on the in-cluster CNPG HoopDb, in selfhosted mode. initdb creates the search Worker role. Before migrate, two privileged Jobs run aspostgreson every deploy:runtimeRole.bootstrapJobrunshoopdb_runtime_role.sql. It creates or verifies the runtime role, revokes TEMPORARY from PUBLIC on the database (a database-wide change) and grants the role CONNECT.functionDefaultRevoke.bootstrapJobrunshoopdb_function_default_revoke.sql. It sets the global function default of the owner and every superuser to{<self>:EXECUTE}and refuses a poisoned default (55000).
migrate's
wait-for-rolewaits for both roles and for the owner's exact function default. -
migration.yaml: an existing in-cluster CNPG HoopDb from before the search Worker role existed. It isselfhost-postgres.yamlplussearchWorkerRole.bootstrapJob. -
external-selfhosted.yaml: an external operator-controlled plain-Postgres HoopDb (RDS, your own Postgres) in selfhosted mode. There are no in-cluster SQL Jobs, and migrate waits for the roles. The operator runs the three bootstrap files as a superuser. Google Cloud SQL usescloudsqlmode instead: seecharts/sessions-pipeline/ci/gcp-cloudsql-values.yaml. There the shim is off,roleBootstrapis false, the migration-owner URL is separate, and the operator runs the Cloud SQL bootstrap files. -
external-supabase.yaml: an external Supabase project in supabase mode. It has no shim, no wait-for-role and no in-cluster SQL Jobs, and the chart refuses each of those in supabase mode. Never runhoopdb_function_default_revoke.sqlon Supabase. The runtime role issessions_pipeline_runtimewith LOGIN and no workload role (a database bootstrapped before the rename keeps its role: setworker.migrations.runtimeRoleto that name, for examplebulls96_runtime). Do not use the drifted legacyrelayhistory_runtime. The Worker role issession_directory_search_worker.The profile pins the 7d7229b GHCR image and selects
functionBoundaryMode: event-trigger(sessions-pipeline #125). It is still fail-closed:supabaseActivation.holdstops it from rendering, and even rendered, migrate refuses (function_boundary_unproven) until the operator has installedhoopdb_sessions_function_boundary_supabase.sqland--activation-checkreportsevent_trigger.verified: true. Lift the hold only once that image is confirmed as the production-Supabase pin and the live Supabase steps are approved. The operator prerequisites are listed in the profile, including the Supabase-only bootstrap wrappers.
Every profile, every CI values file, and the worker and alleyoop chart
defaults pin the services image built from sessions-pipeline main 7d7229b, GHCR
ghcr.io/agentworkforce/96bulls-services@sha256:6f22ea17dff336e1431664542759fd27958649388b90a0b2f0881883caa7c6b2
(publish-image run 41375745173). Its migration generation is
eff8b3be46565b57ad8fa5e4a066dbb6d1fe608313d9c1680bfbcd484add259b. The AWS
proof values (charts/sessions-pipeline/ci/aws-cnpg-values.yaml) pin the immutable ECR
digest from the same source,
500268966557.dkr.ecr.us-west-2.amazonaws.com/96bulls-services@sha256:5bb6075240afa02a1b1797a81c07bffa029954eac7803d702ecba20db35d4db9
(publish-image-ecr run 41375747474). Every profile
also needs global.tenant.orgId and alleyoop.auth.jwksUrl.
ci/sessions-pipeline-profiles/matrix.sh renders every profile and every
charts/sessions-pipeline/ci/*-values.yaml. Those include the AWS CNPG proof, AWS
external Supabase (pod-identity) and GCP Cloud SQL. For each one it asserts:
- the workloads and migrate, with phase 2 verify;
- the exact profile env, in migrate only;
- HoopDb and Locker;
- which privileged Jobs render, with their psql variables and files;
- what migrate waits for;
- the image pins: every row renders only the final images, and the ECR image only on the AWS path.
It also checks every mode combination the chart must refuse. CI runs it
offline (static) and as a server-side dry run on kind (dryrun).
ci/sessions-pipeline-search-worker-role/kind.sh builds the services image from
services-commit (7d7229b) and fails unless the image's generation equals
services-generation.
HoopDb's session directory migration only verifies the role
session_directory_search_worker (NOLOGIN NOINHERIT NOBYPASSRLS NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION, exactly statement_timeout=2s, no
memberships). Creating it needs a superuser on PostgreSQL 16+, so the chart
provisions it (sessions-pipeline runbooks/session-directory-search-worker-bootstrap.md):
- New clusters: two
hoopdb.cluster.initdb.postInitApplicationSQLstatements, run once by CNPG aspostgres. Remove them to opt out. - Existing clusters:
searchWorkerRole.bootstrapJob.enabled=truewithsqlImageset to the services image by digest. A separate Job (its own ServiceAccount; a Role limited topodsget/list andpods/exec) runs the image'ssupabase/bootstrap/session_directory_search_worker.sqlaspostgresbykubectl execinto the primary.enableSuperuserAccessstays false. It is idempotent and never alters a role that differs from the spec (it fails with55000). The sql init container, which runs the services image, gets no API token: the pod hasautomountServiceAccountToken: falseand only the kubectl container mounts a projected token. Pull auth isworker.imagePullSecrets(the umbrella's sharedimagePullSecrets), the same Secret as the workloads. - Order (no Helm hooks): the migrate Job's
wait-for-roleinit container (worker.migrations.waitForRole, on by default in the umbrella, on the pinned HoopDb image) polls HoopDb until the role exists, so bootstrap and migrate are applied together. migrate then verifies the role (phase 1), migrates, and checks its grants (phase 2) and records PASS for the image's migration generation insessions.__96bulls_role_validation. Workloads are ready only while that PASS is current, so a phase 2 failure after the ledger is complete leaves everything unready. migrate always setsHOOPDB_ROLE_VERIFY=true, andworker.extraEnvmay not override it: with verify off, readiness would need a separate mark_pass step, which this chart does not run. A drifted role fails both Jobs with55000and nothing becomes ready. - Disabling the Job removes the Job and its RBAC; the role is never dropped (see the runbook's "Rollback and removal").
ci/sessions-pipeline-search-worker-role/render.sh asserts the rendered objects;
ci/sessions-pipeline-search-worker-role/kind.sh (required in CI) runs on the real
services image, which CI builds from the sessions-pipeline commit in
ci/sessions-pipeline-search-worker-role/services-commit (the published image is
private; locally, pass SERVICES_IMAGE). It proves:
- a fresh and an existing CNPG cluster converge to the same role;
- a drifted role is refused with
55000(exit 3); - in the whole release, migrate waits while the bootstrap is held back and no workload is ready; after the bootstrap, migrate passes both role checks and the workloads become ready;
- a phase-2-only failure after the ledger is complete (a direct grant) sets the role validation marker to pending, and running and restarted workloads go unready;
- with a drifted role, both Jobs fail with
55000and nothing becomes ready.
Shared transcript search has two profiles. The default is postgres:
PostgreSQL 17 and pg_trgm on the HoopDb this chart already installs. The
search Worker role above is the privilege boundary for that path.
Self-managed ClickHouse OSS is an optional derived index for high-volume
installs. No chart reads a search.* value, and core availability does not
depend on ClickHouse. neon is a hosted-provider name, not the self-hosted
search backend. HOOPDB_DB_MODE=selfhosted is unchanged. The decision and
the planned Secret boundary are in
Transcript search profiles. The registry tracks it as the planned option
opt.sessions-pipeline.search-backend.
Both charts are hook-free, so helm install and Alchemy's
helm template --no-hooks + server-side apply behave the same. Create a
Secret with HOOPDB_URL, LOCKER_ACCESS_KEY_ID,
LOCKER_SECRET_ACCESS_KEY and SUMMARY_MASTER_KEYS first (or enable
externalSecrets). SUMMARY_MASTER_KEYS is referenced only by Bob Costas.
For workload identity, set locker.credentials.mode=podIdentity and provide
hoopdb.existingSecret; existingSecret may then be empty. This suppresses
both static Locker credential environment variables in every workload and in
the worker's locker-init Job. locker.endpoint may also be empty to select the
native AWS S3 endpoint.
The default image
ghcr.io/agentworkforce/96bulls-services(pinned by digest) is private: add--set imagePullSecrets[0].name=<secret>to both commands. If you overrideimage.repositoryorimage.tag, also setimage.digest=or supply the matching digest.
helm install sessions-pipeline-worker agentworkforce/sessions-pipeline-worker -n sessions-pipeline --create-namespace \
--set existingSecret=sessions-pipeline \
--set locker.endpoint=http://locker:9000 \
--set tenant.orgId=<org>
helm install alleyoop agentworkforce/alleyoop -n sessions-pipeline \
--set existingSecret=sessions-pipeline \
--set locker.endpoint=http://locker:9000 \
--set tenant.orgId=<org> \
--set auth.jwksUrl=https://<relayauth>/.well-known/jwks.jsonci/sessions-pipeline/smoke.sh runs the whole pipeline on kind, with CloudNativePG
(Postgres 17), MinIO and an OpenTelemetry collector. Third-party charts and
images are pinned once, in deps/.
Charts are packaged and published to GitHub Pages via helm/chart-releaser-action on every merge to main, after the same lint checks that run on pull requests pass. chart-releaser detects changed chart versions automatically — each service chart is released independently.
Each released chart is also pushed to GHCR as an OCI artifact:
helm install <release> oci://ghcr.io/agentworkforce/charts/<chart> --version <version>Add new service charts under charts/<service-name>/. Open a PR against main. Merges are gated by Khaliq.
Every chart must install correctly through helm template --no-hooks plus server-side apply, which is how Alchemy's Kubernetes.HelmChart installs it. Pull requests run .github/workflows/lint.yml:
helm lint --strict, once percharts/<chart>/ci/*-values.yaml(or with defaults when there are none).- No Helm hooks. Hooks never run under
--no-hooks; use a plain Job instead (seesessions-pipeline-worker). - Deterministic renders. No
randAlphaNum,lookupornow, or every render looks like drift. kubeconform, plus a server-side dry-run apply on kind.- A version bump for any chart change outside
ci/.
Run the same checks locally with scripts/ci/charts.sh static and scripts/ci/charts.sh dryrun (the dry run needs a cluster in the current kube context).