feat(desktop): per-persona and per-agent env var overrides#594
Merged
Conversation
a552fa8 to
43e6185
Compare
Adds env_vars: BTreeMap<String, String> on both PersonaRecord and ManagedAgentRecord. Precedence at spawn: parent env < persona < agent (last write wins). Reserved keys — Sprout's identity, code-execution surface, and security gates — are rejected at save time and stripped at runtime so a typo or malicious value can't swap the agent's nsec, code, relay, or respond-to gate. Backend - managed_agents/env_vars.rs (new): merged_user_env, RESERVED_ENV_KEYS, is_reserved_env_key, validate_user_env_keys, 20 unit tests. - managed_agents/runtime.rs: merges persona env → agent env at spawn via env_vars::merged_user_env. - managed_agents/types.rs: env_vars field on PersonaRecord and ManagedAgentRecord; included in ManagedAgentSummary so the frontend list/edit reload sees saved state. - managed_agents/backend.rs: extends provider stderr/error redaction to scrub user-supplied env values (redact_secrets_with + env_secrets_from_request + redact_env_values_in). Uses single-pass str::replace to avoid a non-terminating loop when a user env value is a substring of '[REDACTED]'. - commands/personas.rs, commands/agents.rs, commands/agent_models.rs: validate_user_env_keys at create/update; merged_user_env also used by sprout-acp models discovery and provider deploy so credentials in persona env flow into both. Model-discovery stderr is now redacted through redact_env_values_in before being formatted into the frontend-visible error. Reserved keys (three categories) - Identity / secrets: SPROUT_PRIVATE_KEY, NOSTR_PRIVATE_KEY, SPROUT_AUTH_TAG, SPROUT_API_TOKEN, SPROUT_ACP_PRIVATE_KEY, SPROUT_ACP_API_TOKEN. - Code-execution surface: SPROUT_ACP_AGENT_COMMAND, SPROUT_ACP_AGENT_ARGS, SPROUT_ACP_MCP_COMMAND, SPROUT_RELAY_URL. - Security gates: SPROUT_ACP_RESPOND_TO, SPROUT_ACP_RESPOND_TO_ALLOWLIST, SPROUT_ACP_AGENT_OWNER (legacy-record owner fallback). Case-insensitive match — lowercase variants of these specific keys are almost certainly typos, not legitimate use. Behavior knobs (GOOSE_MODE, SPROUT_TOOLSETS, SPROUT_ACP_MODEL, SPROUT_ACP_SYSTEM_PROMPT) remain freely overridable. Two-layer enforcement: 1. Save-time — validate_user_env_keys rejects reserved keys with a clear error listing the offending keys, surfaced in the dialog. 2. Runtime — merged_user_env strips reserved keys with a warning log. Defense in depth for older on-disk records that predate validation. Frontend - features/agents/ui/EnvVarsEditor.tsx (new): reusable key/value editor with add/remove rows and validation hints. - PersonaDialog, CreateAgentDialog, EditAgentDialog: embed the editor. - shared/api/types.ts, tauri.ts, tauriPersonas.ts: envVars field on create/update payloads. envVars: undefined on update = 'don't touch' so editing unrelated fields can't wipe saved credentials. Tests - 20 unit tests in env_vars.rs covering merge precedence, reserved-key stripping for each category (identity, code-execution, security gates, legacy owner, relay URL, case-insensitive), and the validator. - 6 new unit tests in backend.rs covering redact_secrets_with (user env values scrubbed, short values skipped, overlapping secrets handled, termination when value is substring of marker), env_secrets_from_request (string extraction + missing-shape), and redact_env_values_in. - 1 e2e test (desktop/tests/e2e/persona-env-vars.spec.ts) drives the EnvVarsEditor through the Persona dialog. - All 307 desktop crate unit tests pass. Also bumps the MessageComposer.tsx size limit from 700→710 (the composer-autofocus PR #572 landed on origin/main mid-review and pushed it 3 lines over). Bumps backend.rs limit from 530→700 for the new redaction helpers + their tests. Supersedes #576. Signed-off-by: Tyler Longwell <tlongwell@squareup.com> Security: reject malformed env var keys Tightens validate_user_env_keys to require POSIX-shaped keys ([A-Za-z_][A-Za-z0-9_]*). Closes a denylist bypass: Rust's Command::env(k, v) accepts a key containing '=' and writes it verbatim into the child's environ block. A key like SPROUT_AUTH_TAG=x with value forged lands as SPROUT_AUTH_TAG=x=forged in the child env, so getenv("SPROUT_AUTH_TAG") returns "x=forged" — bypassing the reserved- key string compare. Confirmed exploitable for SPROUT_AUTH_TAG (legacy agents) and any other reserved key Sprout doesn't always set with the canonical name first. Two-layer fix: 1. validate_user_env_keys rejects malformed keys at save time, listing each invalid key with a clear regex hint. 2. merged_user_env strips malformed keys at spawn time as defense in depth for on-disk records that predate the validator. +6 unit tests pinning the bypass and adjacent edge cases (empty, whitespace, NUL, leading digit, non-ASCII, =-in-key). 313 unit tests pass.
43e6185 to
a51b689
Compare
tlongwell-block
added a commit
that referenced
this pull request
May 15, 2026
* origin/main: (33 commits) dev-mcp: add view_image tool (#602) fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips (#601) fix(desktop): derive unread state from NIP-RS + relay catch-up only (#599) docs(testing): rewrite TESTING.md for current API and CLI-first workflow (#597) fix(agent): fix OpenAI-compat request body serialization and max_tokens (#595) feat(desktop): per-persona and per-agent env var overrides (#594) fix(desktop): stop pinning agents to deprecated SPROUT_ACP_TURN_TIMEOUT (#592) fix(desktop): populate member_count in get_channels so channel browser shows real counts (#548) fix(desktop): autofocus message composer on channel/thread open (#572) refactor(cli): restructure flat commands into 12 subcommand groups (#585) feat(sdk): add builder functions for workflows, DMs, and presence (#589) feat(desktop): add message more-actions dropdown menu (#590) fix(mobile): preserve channel list across background/resume reconnection (#588) Redesign Home as an inbox (#582) fix(desktop): drive unread badges from live subscription, not refetched lastMessageAt (#581) fix(desktop): refine header scaling and shadow (#573) fix(desktop): keep day dividers below header (#574) Move agent activity below composer (#579) docs(nips): NIP-AE — Agent Engrams (#575) refactor: extract shared @mention resolver into sprout-sdk (#580) ... Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
tlongwell-block
added a commit
that referenced
this pull request
May 15, 2026
Signed-off-by: Tyler Longwell <tlongwell@squareup.com> * origin/main: dev-mcp: add view_image tool (#602) fix(relay,desktop): only advertise NIP-43 when enforced; probe pairing by supported_nips (#601) fix(desktop): derive unread state from NIP-RS + relay catch-up only (#599) docs(testing): rewrite TESTING.md for current API and CLI-first workflow (#597) fix(agent): fix OpenAI-compat request body serialization and max_tokens (#595) feat(desktop): per-persona and per-agent env var overrides (#594)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds per-persona and per-agent env var overrides to the desktop GUI. Personas and managed agents each carry a
BTreeMap<String, String>of env vars that get layered into the spawned agent's environment.Precedence (last wins): desktop parent env < persona env < agent env.
A small set of reserved keys — Sprout's identity and secrets — are rejected at save time and stripped at runtime so a typo or malicious value can't swap the agent's nsec.
Why
Users want to set provider credentials (
ANTHROPIC_API_KEY,OPENAI_API_KEY), provider selection (GOOSE_PROVIDER), local-LLM endpoints (OPENAI_BASE_URL), etc. without editing shell rc files. Per-persona keys cover the common case; per-agent overrides handle the "one-off agent on a different model" case.Surface area
Backend (Rust):
managed_agents/env_vars.rs— new module withmerged_user_env,RESERVED_ENV_KEYS,is_reserved_env_key,validate_user_env_keys+ 16 unit tests.managed_agents/runtime.rs— mergespersona env → agent env, calls intoenv_vars::merged_user_envat spawn.managed_agents/types.rs—env_varsfield onPersonaRecordandManagedAgentRecord.commands/personas.rs,commands/agents.rs,commands/agent_models.rs—validate_user_env_keysat save time;merged_user_envalso used bysprout-acp modelsdiscovery and provider deploy so credentials in persona env flow into both.Frontend (TS):
features/agents/ui/EnvVarsEditor.tsx— new shared component (key/value rows, add/remove, validation hint).features/agents/ui/PersonaDialog.tsx,CreateAgentDialog.tsx,EditAgentDialog.tsx— embed the editor; persona env shown read-only when editing an agent, with per-agent overrides on top.shared/api/types.ts,tauri.ts,tauriPersonas.ts—envVarsfield on create/update payloads, with the convention thatenvVars: undefinedon update = "don't touch" (so editing unrelated fields can't wipe saved credentials).Reserved keys
SPROUT_PRIVATE_KEYNOSTR_PRIVATE_KEYgit-credential-nostrSPROUT_AUTH_TAGSPROUT_API_TOKENSPROUT_ACP_PRIVATE_KEYSPROUT_ACP_API_TOKENBehavior knobs (
GOOSE_MODE,SPROUT_TOOLSETS,SPROUT_ACP_MODEL,SPROUT_ACP_SYSTEM_PROMPT, etc.) remain freely overridable — those have dedicated UI fields, but power users may want to bypass them.Two-layer enforcement:
validate_user_env_keysrejects reserved keys with a clear error likethe following env vars are reserved by Sprout and cannot be overridden: SPROUT_PRIVATE_KEY, surfaced in the dialog.merged_user_envstrips reserved keys with a warning log. Defense in depth for older on-disk records that predate validation.Case-insensitive match so
sprout_private_keyis also rejected — Unix env vars are case-sensitive at the syscall level, but lowercase variants of these specific keys are almost certainly typos.Tests
env_vars.rscover the merge order, the reserved-key filter, and the validator.desktop/tests/e2e/persona-env-vars.spec.ts) drives the EnvVarsEditor through the Persona dialog.cargo fmt,biome check,pnpm typecheck,cargo clippyall clean.Review history
Codex reviewed five times across the branch lifecycle:
hasOwnProperty→in), fixed.The final fix in this PR (reserved-key denylist) closes a footgun I called out in self-review but the earlier passes didn't flag: user env was written LAST during spawn, so a GUI typo of
SPROUT_PRIVATE_KEYwould have swapped the agent's identity at runtime. The "no allow-list, by design" comment was wrong on its own terms — fixed.Supersedes
#576 (already closed).