Skip to content
Open
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
73 changes: 73 additions & 0 deletions charts/retool/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,79 @@ app.kubernetes.io/component: proxy
telemetry.retool.com/service-name: agent-sandbox-proxy
{{- end -}}

{{/*
Validate that an enabled agent sandbox has its required secrets supplied. The
controller and proxy fail to boot without a Postgres connection and a JWT
public key, and the Retool backend needs the JWT private key to sign sandbox
tokens. Each may come from a plaintext value, the per-key existing-secret refs,
or the catch-all externalSecret.name. No-op when agentSandbox is disabled.
*/}}
{{- define "retool.agentSandbox.validateSecrets" -}}
{{- if .Values.agentSandbox.enabled -}}
{{- $as := .Values.agentSandbox -}}
{{- $ext := $as.externalSecret.name -}}
{{- if not (or $as.postgres.url $as.postgres.urlSecretName $as.postgres.host $ext) -}}
{{- fail "agentSandbox.enabled requires a Postgres connection. Set one of: agentSandbox.postgres.url (DSN), agentSandbox.postgres.host + user + database (the chart assembles the DSN, password from postgres.password or postgres.passwordSecretName), agentSandbox.postgres.urlSecretName (existing secret holding the DSN), or agentSandbox.externalSecret.name." -}}
{{- end -}}
{{- if and $as.postgres.host (not (and $as.postgres.user $as.postgres.database)) -}}
{{- fail "agentSandbox.postgres.host is set, so postgres.user and postgres.database are also required to assemble the DSN." -}}
{{- end -}}
{{- if not (or $as.jwtPublicKey $ext) -}}
{{- fail "agentSandbox.enabled requires a JWT public key. Set agentSandbox.jwtPublicKey or agentSandbox.externalSecret.name." -}}
{{- end -}}
{{- if not (or $as.jwtPrivateKey $ext) -}}
{{- fail "agentSandbox.enabled requires a JWT private key (the backend signs sandbox tokens with it). Set agentSandbox.jwtPrivateKey or agentSandbox.externalSecret.name." -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Render the AGENT_SANDBOX_POSTGRES_URL env entry for the controller/proxy (plus a
PGPASSWORD entry when assembling from fields). validateSecrets guarantees one of
these applies, in order: postgres.url -> postgres.host -> postgres.urlSecretName
-> externalSecret.name.

For the host path the password is passed via PGPASSWORD rather than embedded in
the URL: node-postgres reads PGPASSWORD when the connection string omits the
password, so it needs no URL escaping. PGPASSWORD is process-global but safe
here because the controller/proxy open exactly one Postgres connection.
Usage: {{- include "retool.agentSandbox.postgresUrlEnv" . | nindent 12 }}
*/}}
{{- define "retool.agentSandbox.postgresUrlEnv" -}}
{{- $pg := .Values.agentSandbox.postgres -}}
{{- $ext := .Values.agentSandbox.externalSecret.name -}}
{{- if $pg.url }}
- name: AGENT_SANDBOX_POSTGRES_URL
value: {{ $pg.url | quote }}
{{- else if $pg.host }}
{{- $port := $pg.port | default 5432 -}}
{{- if $pg.passwordSecretName }}
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: {{ $pg.passwordSecretName }}
key: {{ $pg.passwordSecretKey | default "password" }}
{{- else if $pg.password }}
- name: PGPASSWORD
value: {{ $pg.password | quote }}
{{- end }}
- name: AGENT_SANDBOX_POSTGRES_URL
value: {{ printf "postgres://%s@%s:%v/%s" $pg.user $pg.host $port $pg.database | quote }}
Comment on lines +670 to +683

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No install-time guard when postgres.host is set with no password source

When postgres.host is set and neither postgres.password nor postgres.passwordSecretName is provided, the template emits no PGPASSWORD entry and the assembled URL carries no credentials. For most production Postgres deployments this results in a rejected connection, but the failure surfaces at pod startup rather than at helm install time.

validateSecrets already enforces user and database on the host path; extending it to check (not (or $pg.password $pg.passwordSecretName)) and {{- fail }} when true (or document it as intentional for trust-auth) would make the behaviour explicit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unencoded @ in assembled URL breaks cloud-managed Postgres usernames

The printf call embeds $pg.user verbatim in the RFC 3986 userinfo segment. Azure Database for PostgreSQL and similar managed services require the username to include an @hostname suffix. A username containing a literal @ produces two @ characters in the connection string; pg-connection-string treats the second one as the userinfo-host delimiter, so both the username and the host are parsed incorrectly and the connection fails at runtime.

The PR correctly routes the password through PGPASSWORD to avoid escaping, but user and database are still URL-embedded and need percent-encoding for such values. Consider applying Helm's urlquery function to $pg.user and $pg.database, or add a {{- fail }} guard that rejects @, :, and / in those fields on the host-assembly path.

{{- else if $pg.urlSecretName }}
- name: AGENT_SANDBOX_POSTGRES_URL
valueFrom:
secretKeyRef:
name: {{ $pg.urlSecretName }}
key: {{ $pg.urlSecretKey | default "postgres-url" }}
{{- else if $ext }}
- name: AGENT_SANDBOX_POSTGRES_URL
valueFrom:
secretKeyRef:
name: {{ $ext }}
key: postgres-url
{{- end }}
{{- end -}}

{{/*
Agent sandbox env vars for the Retool backend, workflow backend, and workers.
Outputs env entries that tell the backend how to reach the agent sandbox services.
Expand Down
25 changes: 6 additions & 19 deletions charts/retool/templates/deployment_agent_sandbox.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{{- if .Values.agentSandbox.enabled }}
{{- include "retool.agentSandbox.validateSecrets" . }}
{{- $as := .Values.agentSandbox -}}
{{- $defaultSecretName := $as.externalSecret.name | default (include "retool.agentSandbox.name" .) -}}
{{- $nodeSelector := $as.nodeSelector | default .Values.nodeSelector -}}
Expand All @@ -22,7 +23,9 @@ data:
jwt-private-key: {{ $as.jwtPrivateKey | default "" | b64enc | quote }}
encryption-key: {{ $as.encryptionKey | default "" | b64enc | quote }}
api-secret: {{ $as.apiSecret | default "" | b64enc | quote }}
postgres-url: {{ $as.postgres.url | default "" | b64enc | quote }}
{{- if $as.postgres.url }}
postgres-url: {{ $as.postgres.url | b64enc | quote }}
{{- end }}
---
{{- end }}
{{- /*
Expand Down Expand Up @@ -300,15 +303,7 @@ spec:
value: {{ $as.controller.port | quote }}
- name: STATE_BACKEND
value: "postgres"
- name: AGENT_SANDBOX_POSTGRES_URL
{{- if $as.postgres.url }}
value: {{ $as.postgres.url | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ $defaultSecretName }}
key: postgres-url
{{- end }}
{{- include "retool.agentSandbox.postgresUrlEnv" . | nindent 12 }}
- name: AGENT_SANDBOX_POSTGRES_SCHEMA
value: {{ $as.postgres.schema | quote }}
- name: AGENT_SANDBOX_POSTGRES_POOL_MAX
Expand Down Expand Up @@ -504,15 +499,7 @@ spec:
value: {{ $as.proxy.port | quote }}
- name: STATE_BACKEND
value: "postgres"
- name: AGENT_SANDBOX_POSTGRES_URL
{{- if $as.postgres.url }}
value: {{ $as.postgres.url | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ $defaultSecretName }}
key: postgres-url
{{- end }}
{{- include "retool.agentSandbox.postgresUrlEnv" . | nindent 12 }}
- name: AGENT_SANDBOX_POSTGRES_SCHEMA
value: {{ $as.postgres.schema | quote }}
- name: AGENT_SANDBOX_POSTGRES_POOL_MAX
Expand Down
60 changes: 36 additions & 24 deletions charts/retool/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -928,33 +928,45 @@ agentSandbox:
# Labels for agent sandbox pods
labels: {}

# Pre-existing K8s Secret containing keys: jwt-public-key, jwt-private-key,
# encryption-key, api-secret, postgres-url. When set, the chart references
# this secret by default for all secret-backed env vars.
#
# Individual keys can still be overridden by setting the corresponding
# plaintext values below (e.g. jwtPublicKey, postgres.url). When a plaintext
# value is provided alongside externalSecret.name, the plaintext value takes
# precedence for that key and the external secret is used for the rest.
# === Secrets ============================================================
# Provide each secret as a plaintext value below, OR set externalSecret.name
# to a pre-existing Secret with keys jwt-public-key, jwt-private-key,
# encryption-key, api-secret, postgres-url. A plaintext value always wins over
# the external secret for that key.
externalSecret:
name: ''

# Secrets — used directly when externalSecret.name is not set, or as
# per-key overrides when externalSecret.name IS set.
# JWT key pair (ES256) for sandbox token authentication.
jwtPublicKey: ''
jwtPrivateKey: ''
# Hex-encoded 256-bit key for encrypting credentials stored in state backend.
# Must match the backend's AGENT_SANDBOX_ENCRYPTION_KEY.
encryptionKey: ''
# API secret for admin/test endpoints.
apiSecret: ''

# Postgres state backend (shared by controller and proxy for state coordination).
# Connection string for the agent sandbox's state database. When set, takes
# precedence over the postgres-url key in externalSecret.
name: '' # optional: existing Secret holding all keys below

jwtPublicKey: '' # REQUIRED (ES256) unless provided via externalSecret
jwtPrivateKey: '' # REQUIRED (ES256) unless provided via externalSecret
encryptionKey: '' # optional: hex 256-bit; must match backend AGENT_SANDBOX_ENCRYPTION_KEY
apiSecret: '' # optional: admin/test endpoints

# === Postgres state backend =============================================
# REQUIRED. Choose exactly ONE sourcing option; leave the others blank.
postgres:
# -- Option 1: plaintext DSN --
url: ''

# -- Option 2: assemble from fields --
# The password is passed via PGPASSWORD (never embedded in the URL), so any
# characters are safe and a password-only secret can be reused as-is.
# Set either password or passwordSecretName.
host: ''
port: 5432
database: ''
user: ''
password: ''
passwordSecretName: ''
passwordSecretKey: 'password'

# -- Option 3: existing Secret holding the full DSN --
urlSecretName: ''
urlSecretKey: 'postgres-url'

# -- Option 4: reuse externalSecret.name (its postgres-url key) --
# Nothing to set here; just leave options 1-3 blank.

# -- Optional tuning (defaults shown) --
schema: 'agent_executor'
poolMax: 10
sweeperIntervalMs: 60000
Expand Down
60 changes: 36 additions & 24 deletions values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -928,33 +928,45 @@ agentSandbox:
# Labels for agent sandbox pods
labels: {}

# Pre-existing K8s Secret containing keys: jwt-public-key, jwt-private-key,
# encryption-key, api-secret, postgres-url. When set, the chart references
# this secret by default for all secret-backed env vars.
#
# Individual keys can still be overridden by setting the corresponding
# plaintext values below (e.g. jwtPublicKey, postgres.url). When a plaintext
# value is provided alongside externalSecret.name, the plaintext value takes
# precedence for that key and the external secret is used for the rest.
# === Secrets ============================================================
# Provide each secret as a plaintext value below, OR set externalSecret.name
# to a pre-existing Secret with keys jwt-public-key, jwt-private-key,
# encryption-key, api-secret, postgres-url. A plaintext value always wins over
# the external secret for that key.
externalSecret:
name: ''

# Secrets — used directly when externalSecret.name is not set, or as
# per-key overrides when externalSecret.name IS set.
# JWT key pair (ES256) for sandbox token authentication.
jwtPublicKey: ''
jwtPrivateKey: ''
# Hex-encoded 256-bit key for encrypting credentials stored in state backend.
# Must match the backend's AGENT_SANDBOX_ENCRYPTION_KEY.
encryptionKey: ''
# API secret for admin/test endpoints.
apiSecret: ''

# Postgres state backend (shared by controller and proxy for state coordination).
# Connection string for the agent sandbox's state database. When set, takes
# precedence over the postgres-url key in externalSecret.
name: '' # optional: existing Secret holding all keys below

jwtPublicKey: '' # REQUIRED (ES256) unless provided via externalSecret
jwtPrivateKey: '' # REQUIRED (ES256) unless provided via externalSecret
encryptionKey: '' # optional: hex 256-bit; must match backend AGENT_SANDBOX_ENCRYPTION_KEY
apiSecret: '' # optional: admin/test endpoints

# === Postgres state backend =============================================
# REQUIRED. Choose exactly ONE sourcing option; leave the others blank.
postgres:
# -- Option 1: plaintext DSN --
url: ''

# -- Option 2: assemble from fields --
# The password is passed via PGPASSWORD (never embedded in the URL), so any
# characters are safe and a password-only secret can be reused as-is.
# Set either password or passwordSecretName.
host: ''
port: 5432
database: ''
user: ''
password: ''
passwordSecretName: ''
passwordSecretKey: 'password'

# -- Option 3: existing Secret holding the full DSN --
urlSecretName: ''
urlSecretKey: 'postgres-url'

# -- Option 4: reuse externalSecret.name (its postgres-url key) --
# Nothing to set here; just leave options 1-3 blank.

# -- Optional tuning (defaults shown) --
schema: 'agent_executor'
poolMax: 10
sweeperIntervalMs: 60000
Expand Down
Loading