Skip to content

agent-sandbox: validate required secrets + existing-secret DSN ref#308

Open
JatinNanda wants to merge 1 commit into
r2-cleanupfrom
jatin/agentsandbox-secrets
Open

agent-sandbox: validate required secrets + existing-secret DSN ref#308
JatinNanda wants to merge 1 commit into
r2-cleanupfrom
jatin/agentsandbox-secrets

Conversation

@JatinNanda

@JatinNanda JatinNanda commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Part of the r2-cleanup integration branch (merges into r2-cleanup, then r2 via #303).

Problems

  1. Silent empty postgres URL. {{ $as.postgres.url | default "" | b64enc }} base64-encodes an empty value, so a deploy missing the DSN installs cleanly and the controller/proxy crash-loop at runtime.
  2. Unguarded required secrets. jwtPublicKey (controller + proxy process.exit(1) without it) and jwtPrivateKey (the backend signs sandbox tokens with it) had no install-time check.
  3. Rigid postgres config. Only a plaintext DSN was accepted — operators couldn't reuse an existing password-only secret (e.g. the backend's Postgres password) to construct the connection.

App constraint & approach

The agent-sandbox app passes AGENT_SANDBOX_POSTGRES_URL straight to Knex/pg as a connection string — no split host/user/password code path. To still reuse a password-only secret without embedding the password in a URL (and the escaping headaches that brings), the chart builds the DSN without the password and supplies it via the PGPASSWORD env var. node-postgres reads PGPASSWORD when the connection string omits the password — verified against the shipped stack: pg-connection-string 2.4.0 yields an empty password for user@host, and pg 8.11.3's val() uses || so it falls through to process.env.PGPASSWORD. The password is never parsed as part of a URL, so any characters are safe — no escaping required.

Postgres DSN sourcing (resolution order, validated at install)

  1. postgres.url — plaintext DSN.
  2. postgres.host (+ user + database) — chart assembles postgres://user@host:port/database; password supplied via PGPASSWORD from postgres.password (plaintext) or postgres.passwordSecretName/passwordSecretKey (existing secret). This is the reuse-an-existing-password-secret path; reserved characters need no escaping.
  3. postgres.urlSecretName/urlSecretKey — existing secret holding the full DSN.
  4. externalSecret.name — catch-all secret, postgres-url key.

Other changes

  • retool.agentSandbox.validateSecretsfail when an enabled workload lacks a Postgres source, lacks user/database on the assemble path, or lacks a JWT public/private key.
  • Controller/proxy URL block promoted to retool.agentSandbox.postgresUrlEnv (de-dupes two identical blocks; emits PGPASSWORD for the assemble path).
  • Chart-managed secret writes postgres-url only when a plaintext url is set.
  • Documented all shapes + the password-secret reuse path in values.yaml.

Audit of the other r2 workloads

Verification (helm template, agentSandbox.enabled=true)

Case Result
postgres.url + jwt pub/priv ✅ DSN inlined
assemble + passwordSecretName PGPASSWORD from secret; URL postgres://retool@pg.internal:5432/retool (no password)
assemble + plaintext password=p@ss/w:rd PGPASSWORD: "p@ss/w:rd" verbatim; URL has no password (special chars unescaped)
assemble missing user/database ❌ fails
urlSecretName/urlSecretKey secretKeyRef; chart secret omits empty postgres-url
externalSecret.name ✅ reads its postgres-url
no postgres source ❌ fails
no jwtPublicKey / no jwtPrivateKey ❌ fails (distinct messages)
agentSandbox.enabled=false ✅ no-op

Both values.yaml copies stay in sync; helm lint passes; no CI fixture enables agentSandbox.

🤖 Generated with Claude Code

@JatinNanda JatinNanda force-pushed the jatin/agentsandbox-secrets branch 3 times, most recently from 1c15011 to b5817c9 Compare June 8, 2026 17:53
The agent-sandbox secret story was under-validated and rigid:

- An empty postgres.url silently base64-encoded to nothing
  ({{ $as.postgres.url | default "" | b64enc }}), so a misconfigured deploy
  installed cleanly and the controller/proxy crash-looped at runtime.
- jwtPublicKey / jwtPrivateKey (required for the controller/proxy to boot and
  for the backend to sign sandbox tokens) had no guard when absent.
- Postgres could only be supplied as a plaintext DSN; operators could not reuse
  an existing password-only secret (e.g. the backend's Postgres password).

The agent-sandbox app consumes a single connection string (no split-field code
path), so the chart now offers four ways to supply it, validated at install:

  1. postgres.url            -- plaintext DSN.
  2. postgres.host (+ user + database) -- the chart assembles
     postgres://user@host:port/database and supplies the password out-of-band
     via the PGPASSWORD env var, from postgres.password or
     postgres.passwordSecretName. node-postgres reads PGPASSWORD when the DSN
     omits the password (verified: pg-connection-string yields an empty password
     for user@host, and pg's val() uses `||` so it falls through to PGPASSWORD),
     so the password is never parsed as a URL and needs no escaping -- any
     characters are safe. This is what lets a password-only secret be reused.
  3. postgres.urlSecretName  -- existing secret holding the full DSN.
  4. externalSecret.name     -- catch-all secret, postgres-url key.

Changes:
- Add retool.agentSandbox.validateSecrets: fail at install time when an enabled
  workload is missing a Postgres source, user/database for the assemble path, a
  JWT public key, or a JWT private key.
- Promote the controller/proxy URL block to retool.agentSandbox.postgresUrlEnv,
  which implements the resolution order above (emitting PGPASSWORD for the
  assemble path).
- Only write postgres-url into the chart-managed secret when a plaintext url is
  set, so empty keys are never emitted.
- Document the canonical shapes and the password-secret reuse path.

Audit: mcp already fails on its missing required secret; js_executor has no
secrets, so neither needs changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JatinNanda JatinNanda force-pushed the jatin/agentsandbox-secrets branch from b5817c9 to 14df4f9 Compare June 8, 2026 20:16
@JatinNanda JatinNanda marked this pull request as ready for review June 8, 2026 20:17
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown

Greptile Summary

This PR hardens the agent-sandbox Helm chart by adding install-time validation for required secrets and expanding Postgres DSN sourcing from a single plaintext URL to four options (plaintext, field-assembled, existing-secret ref, or catch-all external secret). The field-assembly path routes the password through PGPASSWORD to avoid URL-escaping issues.

  • validateSecrets helper (_helpers.tpl): fails helm install when agentSandbox.enabled is true but a Postgres source, jwtPublicKey, or jwtPrivateKey is absent — preventing the silent crash-loop that occurred before.
  • postgresUrlEnv helper (_helpers.tpl): de-duplicates the identical controller/proxy AGENT_SANDBOX_POSTGRES_URL blocks, adds PGPASSWORD injection for the host-assembly path, and handles all four DSN sourcing strategies in precedence order.
  • Chart-managed Secret (deployment_agent_sandbox.yaml): now omits the postgres-url key when no plaintext DSN is set, replacing the previous empty-base64 value that caused silent runtime failures.

Confidence Score: 3/5

The install-time validation and DSN de-duplication are sound, but the host-assembly path produces a malformed connection string for any username containing @ — a format required by Azure PostgreSQL — causing a runtime connection failure that the validation does not catch.

The chart's new host-assembly path embeds postgres.user verbatim into the connection string URL without percent-encoding. Cloud-managed Postgres services (Azure Database for PostgreSQL) mandate an @servername suffix in the username; that literal @ produces two @ characters in the assembled URL, which pg-connection-string misparses, breaking the connection at pod startup. The rest of the PR — the validateSecrets helper, the PGPASSWORD approach for the password, the de-duplicated env block, and the guarded chart-managed Secret — is well-structured and addresses the stated problems correctly.

The postgresUrlEnv helper in charts/retool/templates/_helpers.tpl (lines 670-696) needs attention, specifically the printf that assembles the connection string on the postgres.host path.

Important Files Changed

Filename Overview
charts/retool/templates/_helpers.tpl Adds validateSecrets and postgresUrlEnv helpers; the assembled-URL path does not percent-encode user/database, which breaks Azure-style @-containing usernames at runtime.
charts/retool/templates/deployment_agent_sandbox.yaml Calls validateSecrets at the top of the enabled block; replaces the two identical inline URL env blocks with the shared helper; guards postgres-url in the chart-managed Secret so empty DSNs are never written.
charts/retool/values.yaml Adds the four Postgres sourcing options (url, host-assembly, urlSecretName, externalSecret) and marks JWT keys as required; mirrors changes in root values.yaml; well-documented.
values.yaml Identical update to charts/retool/values.yaml kept in sync; no independent logic.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[helm install / upgrade
agentSandbox.enabled=true] --> B[validateSecrets]
    B -->|postgres.url OR host OR urlSecretName OR externalSecret| C{Postgres source?}
    B -->|none set| FAIL1[❌ fail: Postgres required]
    C -->|host set| D{user + database set?}
    D -->|no| FAIL2[❌ fail: user+database required]
    D -->|yes| E[postgresUrlEnv: assemble URL
PGPASSWORD from password/passwordSecretName]
    C -->|url set| F[postgresUrlEnv: value: url]
    C -->|urlSecretName set| G[postgresUrlEnv: secretKeyRef urlSecretName/urlSecretKey]
    C -->|externalSecret only| H[postgresUrlEnv: secretKeyRef externalSecret.name]
    B -->|jwtPublicKey OR externalSecret| I{JWT public key?}
    B -->|neither set| FAIL3[❌ fail: jwtPublicKey required]
    I -->|jwtPrivateKey OR externalSecret| J{JWT private key?}
    I -->|neither set| FAIL4[❌ fail: jwtPrivateKey required]
    J --> K[✅ Render controller + proxy Deployments]
    E --> K
    F --> K
    G --> K
    H --> K
Loading

Reviews (1): Last reviewed commit: "agent-sandbox: validate required secrets..." | Re-trigger Greptile

Comment on lines +670 to +683
{{- 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 }}

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.

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 }}

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.

@JatinNanda JatinNanda requested a review from ryanartecona June 8, 2026 22:08

@ryanartecona ryanartecona left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants