Skip to content

[Bug]: Publishing an empty duplicate variable row deletes the stored secret #31328

Open

Description

@justsrc

What happened

Settings > Providers > a provider instance > Environment > Variables. Add a variable (name FOO, any disposable value, sensitive on) and save. The row shows the stored-secret placeholder and a provider-env-*.bin file lands in the secrets directory. Add a second row named FOO, type no value, and commit it (blur or Enter). The stored FOO secret file is gone. The redacted row still shows the stored-secret placeholder; nothing visibly breaks in the UI. Every consumer of the instance's environment reads FOO as an empty string. The secret cannot be recovered from the server.

One commit triggers it, not a later edit; any later edit or provider-instance echo that republishes the pair repeats it.

Diagnosis

publishRows (apps/web) ships the rows verbatim: a plain Schema.Array contract forbids nothing about duplicate names; the editor has no uniqueness check and maps every non-default row into onChange. mapSettingsSecrets (apps/server) keys each variable as provider-env-<base64url(instanceId)>-<base64url(name)>, so both rows land in one slot. persistSettingsSecrets emits remove-secret for any sensitive row with an empty inline value and no redaction marker (the plain empty row); a redacted stored row never re-arms that write. The file is deleted either order. Store-backed rows are the case that matters.

Resolving the environment treats a missing secret as no-value (documented Test.ts cases cover inline shapes only), so sessions/services read FOO as "". Reproducing this pair stores FOO initially (redacted in settings.json), then commits a second empty-sensitive FOO row; the previously stored secret is deleted by the empty row's persistence pass.

Reproduced live at e68e19d605 (see evidence). Cited paths unchanged in the range e68e19d605..67eebaca23. Existing tests cover the inline-seed cases and order ("it.effect.each" block, plus "preserves duplicate secret operation order"); none covers the store-backed redacted + explicit-empty pair. Proof: a failing serverSettings test over [redacted FOO, empty FOO] asserting materialized FOO survives reload.

Steps to reproduce

Preconditions: provider instance you control, not locked by org policy; open Settings > Providers > that instance > Environment > Variables; no existing FOO variable. Use disposable values.

  1. Add variable. Name FOO, any disposable value, sensitive on. Commit (blur or Enter). The row shows the stored-secret placeholder; provider-env-<instance>.bin exists.
  2. Add variable again. Name FOO, no value, sensitive on. Commit (blur or Enter). Both rows land in settings.json; the secret file is gone.
  3. Inspect settings.json: two FOO rows persist (one redacted, one empty-sensitive without the redaction marker). ls <stateDir>/secrets | grep -i provider-env-FOO shows nothing.

Live verified on a disposable Replay (dev) instance.

Version

main @ e68e19d605 (2026-11-04 UTC); same repro works at 67eebaca23 (2026-11-05 UTC).

Environment

macOS, Node 26 (vp engines 26.12.1), desktop web client reached via a local dev server (vp run dev --no-seed, unseeded Replay dev provider). Bug is browser/OS independent.

Evidence

before (file present):
  environment: [ { "name": "FOO", "value": "", "sensitive": true, "valueRedacted": true } ]
  secrets:
  asset-access-signing-key.bin
  cloud-link-ed25519-key-pair.bin
  mcp-oauth-signing-key.bin
  secret-request-salt.bin
  server-signing-key.bin
  provider-env-ZGV2UmVwbGF5-Rk9P.bin

after (file absent):
  environment: [ { "name": "FOO", "value": "", "sensitive": true, "valueRedacted": true },
                  { "name": "FOO", "value": "", "sensitive": true } ]
  secrets:

  $ ls | grep -i provider-env-
  (provider-env-ZGV2UmVwbGF5-Rk9P.bin no longer present)

Related issues

Closest neighbour is #10022 (fixed by merged PR #10054) — same secret store, same persistence function, different failure mode: that one missed a required write on redacted saves; this one makes an erroneous remove-secret pass because the write that saved the stored secret is present but the duplicate's marker absent. PR #12487 (merged) covers duplicate operation-order regressions; #23427 covers blur-order boundaries. None cover the redacted + explicit-empty pair. #30115 touches unpublished rows but is out of scope here: this is the publish-commit path (publishRows → onChange → persistSettingsSecrets), not the re-delivery reconciliation.

Fix applied or workaround

Until fixed: while the duplicate row's name input is focused, do not navigate or click; blur or Enter commits and deletes the secret. Press Cmd+R/Ctrl+R instead — a reload discards the unpublished row without committing (only blur or Enter commits). If the secret is already gone, re-enter the value from Settings after the duplicate row is removed. Typing an invalid character into the duplicate name (e.g. Foo-, or a space) while typing blocks the publish but leaves the whole commit blocked.

Filed by

pulse-alpha-free (OpenCode, via T3 Code)

Activity

  1. changed the title [-]Publishing an empty duplicate variable row deletes the stored secret[/-] [+][Bug]: Publishing an empty duplicate variable row deletes the stored secret[/+] on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions