Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

### The Helm chart pins by digest, mounts a tenant package, and takes a Composio key

Three things a deployment on Kubernetes did by hand now have values of their own:

- `image.digest` deploys exactly the image a release's `container-images.json` names, as
`repository:tag@digest`. It refuses anything that is not `sha256:` and 64 hex characters.
- `config.extraVolumes` and `config.extraVolumeMounts` mount volumes on the server and on the routines
and computer-culler CronJobs, the pods that load the configuration. A tenant package from a
ConfigMap no longer needs a custom image.
- `secrets.composioApiKey`, or `composio-api-key` named in `externalSecrets.data`, reaches the server
as `COMPOSIO_API_KEY`. Only the server receives it, not the CronJobs.

With none of them set, every target renders exactly as before.

**Before upgrading.** Four things change for an existing deployment:
- A hosted computer's browser is now headed by default. It uses a little more memory than headless
did; set `COMPUTER_BROWSER_MODE=headless` (Helm: `computers.browserMode: headless`) to keep the old
Expand Down
39 changes: 39 additions & 0 deletions charts/openbot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,44 @@ helm upgrade --install openbot charts/openbot \
it in a file anybody commits. The chart marks the Secret it creates `helm.sh/resource-policy: keep`,
so an uninstall does not take the key that every stored credential was encrypted with.

### Pin the image by digest

A tag can be moved; a digest cannot. Each release's `container-images.json` names the digest CI
tested, and `image.digest` deploys exactly that:

```sh
gh release download v0.2.0 --pattern container-images.json
helm upgrade --install openbot charts/openbot ... \
--set image.tag=v0.2.0 \
--set-string image.digest="$(jq -r '.images.openbot.reference | split("@")[1]' container-images.json)"
```

The image becomes `repository:tag@digest`, so the tag stays readable and the digest decides what is
pulled. Anything other than `sha256:` and 64 hex characters is refused at install.

### Your own tenant package

The tenant package is a directory (`config.tenantPackageDir`, `/app/examples/fintech` by default).
To run your own without building an image, put its files in a ConfigMap and mount it with
`config.extraVolumes` and `config.extraVolumeMounts`. They reach the server and the routines and
computer-culler CronJobs, the pods that load the package, so the package is the same wherever it is
read:

```yaml
config:
tenantPackageDir: /tenant
extraVolumes:
- name: tenant
configMap: { name: my-tenant-package }
extraVolumeMounts:
- name: tenant
mountPath: /tenant
readOnly: true
```

A ConfigMap is flat, so a package that uses `agents/` needs `items` with `path: agents/<file>.yaml`,
or keeps its coworkers in `agents.yaml`.

## What the defaults assume

**A plain cluster with no cloud features.** The cluster's own default StorageClass, no RuntimeClass,
Expand Down Expand Up @@ -400,6 +438,7 @@ the key in the last column.
| SCIM provisioning | `config.scim.enabled` | `connectionId` | `scim-bearer-token` (required), `scim-bearer-token-next` (optional) |
| Inbound email triggers | `config.inboundEmail.domain` and `snsTopicArns` | both needed, or the route is not mounted | |
| OpenTelemetry export | `config.otel.endpoint` or `logsEndpoint` | `serviceName`, `paused` | `otel-logs-headers`, `otel-headers` (optional) |
| Composio app catalogue | `secrets.composioApiKey` (the key is the switch) | | `composio-api-key` (server only; see [Composio](../../docs/plugins/composio.md)) |

```yaml
config:
Expand Down
43 changes: 43 additions & 0 deletions charts/openbot/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,51 @@ actually exists. A named `image.tag` is used verbatim.
*/}}
{{- define "openbot.image" -}}
{{- $tag := .Values.image.tag | default (printf "v%s" .Chart.AppVersion) -}}
{{- with .Values.image.digest -}}
{{- if not (regexMatch "^sha256:[a-f0-9]{64}$" .) -}}
{{- fail (printf "image.digest must be sha256: followed by 64 hex characters, as in a release's container-images.json; got %q." .) -}}
{{- end -}}
{{- printf "%s:%s@%s" $.Values.image.repository $tag . -}}
{{- else -}}
{{- printf "%s:%s" .Values.image.repository $tag -}}
{{- end -}}
{{- end -}}

{{/*
Where a key of this chart's Secret comes from, for an optional credential.

`chart` and `store` mean the key is certainly there: this chart writes it from the literal, or
`externalSecrets.data` names it. `unknown` is `secrets.existingSecret`, which cannot be read at
template time, so the reference is made optional and a Secret without the key still starts. Empty
means nothing provides it and the variable is left out.

Called with (dict "root" . "key" "composio-api-key" "literal" .Values.secrets.composioApiKey).
*/}}
{{- define "openbot.secretKeySource" -}}
{{- $root := .root -}}
{{- if .literal -}}
chart
{{- else if $root.Values.externalSecrets.enabled -}}
{{- $named := list -}}
{{- range $root.Values.externalSecrets.data }}{{- $named = append $named .secretKey }}{{- end -}}
{{- if has .key $named }}store{{ end -}}
{{- else if $root.Values.secrets.existingSecret -}}
unknown
{{- end -}}
{{- end -}}

{{/* `config.extraVolumeMounts` and `config.extraVolumes`, for the pods that load the configuration. */}}
{{- define "openbot.extraVolumeMounts" -}}
{{- with .Values.config.extraVolumeMounts }}
{{ toYaml . }}
{{- end }}
{{- end -}}

{{- define "openbot.extraVolumes" -}}
{{- with .Values.config.extraVolumes }}
{{ toYaml . }}
{{- end }}
{{- end -}}

{{- define "openbot.secretName" -}}
{{- default (printf "%s-secrets" (include "openbot.fullname" .)) .Values.secrets.existingSecret -}}
Expand Down
6 changes: 6 additions & 0 deletions charts/openbot/templates/computer/culler-cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ spec:
- name: sandbox-template
mountPath: /etc/openbot
readOnly: true
{{- with (include "openbot.extraVolumeMounts" . | trim) }}
{{- . | nindent 16 }}
{{- end }}
resources:
requests:
cpu: 50m
Expand All @@ -100,4 +103,7 @@ spec:
- name: sandbox-template
configMap:
name: {{ include "openbot.componentName" (dict "root" . "component" "computer-template") }}
{{- with (include "openbot.extraVolumes" . | trim) }}
{{- . | nindent 12 }}
{{- end }}
{{- end }}
8 changes: 8 additions & 0 deletions charts/openbot/templates/routines/cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,18 @@ spec:
{{ include "openbot.commonEnv" . | indent 16 }}
- name: SERVER_INTERNAL_URL
value: http://{{ include "openbot.componentName" (dict "root" . "component" "server") }}:{{ .Values.server.service.port }}
{{- with .Values.config.extraVolumeMounts }}
volumeMounts:
{{ toYaml . | indent 16 }}
{{- end }}
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
{{- with .Values.config.extraVolumes }}
volumes:
{{ toYaml . | indent 12 }}
{{- end }}
{{- end }}
3 changes: 3 additions & 0 deletions charts/openbot/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ stringData:
{{- with .Values.secrets.licenseToken }}
license-token: {{ . | quote }}
{{- end }}
{{- with .Values.secrets.composioApiKey }}
composio-api-key: {{ . | quote }}
{{- end }}
{{- with .Values.secrets.betterAuthSecret }}
better-auth-secret: {{ . | quote }}
{{- end }}
Expand Down
29 changes: 26 additions & 3 deletions charts/openbot/templates/server/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ spec:
{{- with (include "openbot.learningServiceTokenEnv" . | trim) }}
{{- . | nindent 12 }}
{{- end }}
{{- /* Also the server's alone: only the API calls Composio, so the CronJobs are not handed the key. */}}
{{- $composio := include "openbot.secretKeySource" (dict "root" . "key" "composio-api-key" "literal" .Values.secrets.composioApiKey) }}
{{- if $composio }}
- name: COMPOSIO_API_KEY
valueFrom:
secretKeyRef:
name: {{ include "openbot.secretName" . }}
key: composio-api-key
{{- if eq $composio "unknown" }}
optional: true
{{- end }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "openbot.configMapName" . }}
Expand Down Expand Up @@ -140,20 +152,31 @@ spec:
periodSeconds: {{ .Values.server.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.server.livenessProbe.timeoutSeconds }}
failureThreshold: {{ .Values.server.livenessProbe.failureThreshold }}
{{- if eq .Values.computers.mode "sandbox" }}
{{- /* The shape of a Bot's computer, as a file rather than as a permission to read one. */}}
{{- $sandbox := eq .Values.computers.mode "sandbox" }}
{{- if or $sandbox .Values.config.extraVolumeMounts }}
volumeMounts:
{{- if $sandbox }}
{{- /* The shape of a Bot's computer, as a file rather than as a permission to read one. */}}
- name: sandbox-template
mountPath: /etc/openbot
readOnly: true
{{- end }}
{{- with (include "openbot.extraVolumeMounts" . | trim) }}
{{- . | nindent 12 }}
{{- end }}
{{- end }}
{{- with .Values.server.resources }}
resources:
{{ toYaml . | indent 12 }}
{{- end }}
{{- if eq .Values.computers.mode "sandbox" }}
{{- if or (eq .Values.computers.mode "sandbox") .Values.config.extraVolumes }}
volumes:
{{- if eq .Values.computers.mode "sandbox" }}
- name: sandbox-template
configMap:
name: {{ include "openbot.componentName" (dict "root" . "component" "computer-template") }}
{{- end }}
{{- with (include "openbot.extraVolumes" . | trim) }}
{{- . | nindent 8 }}
{{- end }}
{{- end }}
23 changes: 23 additions & 0 deletions charts/openbot/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ image:
# Empty means the chart's appVersion prefixed with `v` (the form published tags take), so an
# upgrade of the chart moves the image with it. A value set here is used verbatim.
tag: ""
# The image digest, `sha256:` and 64 hex characters, from the release's `container-images.json`.
# Set, the image becomes `repository:tag@digest` and Kubernetes pulls exactly those bytes, which is
# how docs/releasing.md says to deploy: a tag can be moved, a digest cannot. The tag stays in the
# reference for whoever reads it. Empty pulls by tag alone, as before.
digest: ""
pullPolicy: IfNotPresent
imagePullSecrets: []

Expand Down Expand Up @@ -304,6 +309,20 @@ config:
# Free-form additions, for anything this chart has no opinion about.
extraEnv: []
extraEnvFrom: []
# Volumes, and where to mount them, on every pod that loads this deployment's configuration: the
# server and the routines and computer-culler CronJobs, the same pods `extraEnv` reaches. The usual
# reason is a tenant package of your own, from a ConfigMap, with `tenantPackageDir` pointed at its
# mount; every one of those pods reads the package, so mounting it on the server alone is not enough.
#
# extraVolumes:
# - name: tenant
# configMap: { name: my-tenant-package }
# extraVolumeMounts:
# - name: tenant
# mountPath: /tenant
# readOnly: true
extraVolumes: []
extraVolumeMounts: []

# How a Bot gets a computer.
#
Expand Down Expand Up @@ -603,6 +622,10 @@ secrets:
# Optional. Managed Intelligence issues no licence token; set this only for a self-hosted
# Intelligence that has one, and it is passed to the server as COPILOTKIT_LICENSE_TOKEN.
licenseToken: ""
# Optional. One key for the whole deployment, for Composio, the broker holding people's accounts for
# a few hundred apps (docs/plugins/composio.md). Passed to the server alone as COMPOSIO_API_KEY.
# From a store, name `composio-api-key` in `externalSecrets.data` instead.
composioApiKey: ""
# Sent to `config.managedAgent.url` on every call. Required when that url is set.
managedAgentToken: ""
# Required when `routines.enabled`: what the CronJob presents to the API server to be recognised
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/composio.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Setting it up takes three hands, and none of them can do another's:

| Who | Does | Where |
| ---------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| Whoever deploys | Sets `COMPOSIO_API_KEY` | The deployment's environment. There is no screen for it |
| Whoever deploys | Sets `COMPOSIO_API_KEY` | The deployment's environment, or `secrets.composioApiKey` in the Helm chart. There is no screen for it |
| An administrator | Enables an app | `/admin/plugins/composio` |
| An administrator | Grants its actions to a Bot | `/admin/plugins/composio-<slug>`, then that page's per-Bot screen |
| Each person | Connects their own account to that app | `/settings/connected-accounts` |
Expand Down
3 changes: 3 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ wants the `build` sections overridden away rather than this variable alone.
A tag can be moved to point at a different image; a digest cannot. The same digest that CI smoke
tested is the one that runs, and rolling back is the same command with an earlier version.

On Kubernetes the chart takes the same digest as `image.digest`; see
[charts/openbot/README.md](../charts/openbot/README.md#pin-the-image-by-digest).

Before deploying, you can check an image is the one this repository built. Every published image
carries its own attestation:

Expand Down
Loading