Skip to content

fix(desktop): autofocus message composer on channel/thread open#572

Merged
tlongwell-block merged 1 commit into
mainfrom
fix/composer-autofocus
May 15, 2026
Merged

fix(desktop): autofocus message composer on channel/thread open#572
tlongwell-block merged 1 commit into
mainfrom
fix/composer-autofocus

Conversation

@tlongwell-block
Copy link
Copy Markdown
Collaborator

Problem

[Reported by @moneyball in #sprout-feedback](https://github.com/block/sprout/issues/... — sprout thread):

sprout does not automatically put the focus in the text box on many screens.

  1. when i click the reply icon for a message, i naturally start typing the reply. in slack, discord, signal, and any other app it works. but in sprout i have to find the text box for the reply and click it before typing.
  2. when i select a new channel i'm unable to begin typing a new message. i first have to find and click on the text box.

Fix

The composer already focused on editTarget and on in-thread replyTarget changes, but had no autofocus on mount or when the channel/thread changed. Added a small useComposerAutofocus hook that calls richText.focus() whenever the effective draft key changes (channel or thread switch) — same hook covers both repros because:

  • Switching channels → channelId (and therefore effectiveDraftKey) changes → focus.
  • Clicking the reply icon mounts the thread panel → its composer mounts → focus.

Guarded against:

  • Disabled composers (archived channels, no active channel).
  • Focus already being inside another text-entry surface (INPUT/TEXTAREA/SELECT/contenteditable) — so an open dialog or search box won't have focus yanked from it.

Tests

Added three Playwright tests in messaging.spec.ts:

  • composer is focused after selecting a channel
  • composer is focused after switching to a different channel
  • thread composer is focused after clicking the reply icon

All pass. Full messaging, mentions, and channels suites still green (62 tests).

Previously the user had to click the composer to start typing after:
  1. Clicking the reply icon on a message (thread panel composer not focused)
  2. Selecting a new channel (main composer not focused)

Now the composer focuses on mount and whenever the effective draft key
changes (channel or thread switch), matching Slack/Discord/Signal.

The logic lives in a small `useComposerAutofocus` hook and is guarded
against:
  - disabled composers (archived channels, no active channel,
    in-flight send at mount)
  - focus already being inside another text-entry surface (open dialog
    input, search box, etc.) so we don't yank focus from the user

The trigger deliberately excludes `disabled`: callers pass a disabled
flag that includes the transient `isSending` state, which would otherwise
re-fire autofocus after every send. With both the main and thread
composers mounted, that re-fire could race and steal focus from the
thread composer post-send. The effect now only runs on mount and on
draft-key changes (real navigation).

Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
@tlongwell-block tlongwell-block force-pushed the fix/composer-autofocus branch from 1588f0d to c08dbb9 Compare May 13, 2026 22:25
@tlongwell-block tlongwell-block merged commit 3a3501c into main May 15, 2026
15 checks passed
@tlongwell-block tlongwell-block deleted the fix/composer-autofocus branch May 15, 2026 00:35
tlongwell-block added a commit that referenced this pull request May 15, 2026
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/secrets — are
rejected at save time and stripped at runtime so a typo or malicious
value can't swap the agent's nsec.

Backend

- managed_agents/env_vars.rs (new): merged_user_env, RESERVED_ENV_KEYS,
  is_reserved_env_key, validate_user_env_keys, 16 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.
- 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.

Reserved keys: SPROUT_PRIVATE_KEY, NOSTR_PRIVATE_KEY, SPROUT_AUTH_TAG,
SPROUT_API_TOKEN, SPROUT_ACP_PRIVATE_KEY, SPROUT_ACP_API_TOKEN.
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

- 16 unit tests in env_vars.rs covering merge precedence, reserved-key
  stripping (persona + agent + case-insensitive), and the validator.
- 1 e2e test (desktop/tests/e2e/persona-env-vars.spec.ts) drives the
  EnvVarsEditor through the Persona dialog.
- All 296 desktop crate unit tests pass.

Also bumps the MessageComposer.tsx size limit from 700→710. The
composer-autofocus PR (#572) landed on origin/main while this branch
was in review and pushed it 3 lines over the limit. Unrelated to this
feature; bumping here so CI is green.

Supersedes #576.

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
tlongwell-block added a commit that referenced this pull request May 15, 2026
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/secrets — are
rejected at save time and stripped at runtime so a typo or malicious
value can't swap the agent's nsec.

Backend

- managed_agents/env_vars.rs (new): merged_user_env, RESERVED_ENV_KEYS,
  is_reserved_env_key, validate_user_env_keys, 16 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.
- 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.

Reserved keys: SPROUT_PRIVATE_KEY, NOSTR_PRIVATE_KEY, SPROUT_AUTH_TAG,
SPROUT_API_TOKEN, SPROUT_ACP_PRIVATE_KEY, SPROUT_ACP_API_TOKEN.
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

- 16 unit tests in env_vars.rs covering merge precedence, reserved-key
  stripping (persona + agent + case-insensitive), and the validator.
- 1 e2e test (desktop/tests/e2e/persona-env-vars.spec.ts) drives the
  EnvVarsEditor through the Persona dialog.
- All 296 desktop crate unit tests pass.

Also bumps the MessageComposer.tsx size limit from 700→710. The
composer-autofocus PR (#572) landed on origin/main while this branch
was in review and pushed it 3 lines over the limit. Unrelated to this
feature; bumping here so CI is green.

Supersedes #576.

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
tlongwell-block added a commit that referenced this pull request May 15, 2026
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/secrets — are
rejected at save time and stripped at runtime so a typo or malicious
value can't swap the agent's nsec.

Backend

- managed_agents/env_vars.rs (new): merged_user_env, RESERVED_ENV_KEYS,
  is_reserved_env_key, validate_user_env_keys, 17 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). Without this, a provider that echoes its
  request in failure messages could surface an ANTHROPIC_API_KEY-style
  secret unredacted via spawnError/last_error.
- 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.

Reserved keys

- SPROUT_PRIVATE_KEY, NOSTR_PRIVATE_KEY (agent identity)
- SPROUT_AUTH_TAG (NIP-OA relay auth)
- SPROUT_API_TOKEN, SPROUT_ACP_PRIVATE_KEY, SPROUT_ACP_API_TOKEN
- SPROUT_ACP_AGENT_OWNER (owner enforcement for legacy records
  without auth_tag — overriding would change who the agent
  responds to)

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

- 17 unit tests in env_vars.rs covering merge precedence, reserved-key
  stripping (persona + agent + case-insensitive), owner-key protection
  for legacy records, and the validator.
- 5 new unit tests in backend.rs covering redact_secrets_with (user env
  values scrubbed, short values skipped, overlapping secrets handled)
  and env_secrets_from_request (string extraction + missing-shape).
- 1 e2e test (desktop/tests/e2e/persona-env-vars.spec.ts) drives the
  EnvVarsEditor through the Persona dialog.
- All 302 desktop crate unit tests pass.

Also bumps the MessageComposer.tsx size limit from 700→710 — the
composer-autofocus PR (#572) landed on origin/main while this branch
was in review and pushed it 3 lines over. Unrelated to this feature;
bumping here so CI is green. Bumps backend.rs limit from 530→640 for
the new redaction helpers + their tests.

Supersedes #576.

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
tlongwell-block added a commit that referenced this pull request May 15, 2026
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/secrets — are
rejected at save time and stripped at runtime so a typo or malicious
value can't swap the agent's nsec.

Backend

- managed_agents/env_vars.rs (new): merged_user_env, RESERVED_ENV_KEYS,
  is_reserved_env_key, validate_user_env_keys, 17 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). Without this, a provider that echoes its
  request in failure messages could surface an ANTHROPIC_API_KEY-style
  secret unredacted via spawnError/last_error.
- 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.

Reserved keys

- SPROUT_PRIVATE_KEY, NOSTR_PRIVATE_KEY (agent identity)
- SPROUT_AUTH_TAG (NIP-OA relay auth)
- SPROUT_API_TOKEN, SPROUT_ACP_PRIVATE_KEY, SPROUT_ACP_API_TOKEN
- SPROUT_ACP_AGENT_OWNER (owner enforcement for legacy records
  without auth_tag — overriding would change who the agent
  responds to)

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

- 17 unit tests in env_vars.rs covering merge precedence, reserved-key
  stripping (persona + agent + case-insensitive), owner-key protection
  for legacy records, and the validator.
- 5 new unit tests in backend.rs covering redact_secrets_with (user env
  values scrubbed, short values skipped, overlapping secrets handled)
  and env_secrets_from_request (string extraction + missing-shape).
- 1 e2e test (desktop/tests/e2e/persona-env-vars.spec.ts) drives the
  EnvVarsEditor through the Persona dialog.
- All 302 desktop crate unit tests pass.

Also bumps the MessageComposer.tsx size limit from 700→710 — the
composer-autofocus PR (#572) landed on origin/main while this branch
was in review and pushed it 3 lines over. Unrelated to this feature;
bumping here so CI is green. Bumps backend.rs limit from 530→640 for
the new redaction helpers + their tests.

Supersedes #576.

Signed-off-by: Tyler Longwell <tlongwell@squareup.com>
tlongwell-block added a commit that referenced this pull request May 15, 2026
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>
tlongwell-block added a commit that referenced this pull request May 15, 2026
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.
tlongwell-block added a commit that referenced this pull request May 15, 2026
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.
tlongwell-block added a commit that referenced this pull request May 15, 2026
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.
tlongwell-block added a commit that referenced this pull request May 15, 2026
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.
tlongwell-block added a commit that referenced this pull request May 15, 2026
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.
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>
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.

1 participant