Skip to content

Feature/workspace scoped env - #95

Open
davi0015 wants to merge 8 commits into
mainfrom
feature/workspace-scoped-env
Open

Feature/workspace scoped env#95
davi0015 wants to merge 8 commits into
mainfrom
feature/workspace-scoped-env

Conversation

@davi0015

@davi0015 davi0015 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

No description provided.

davi0015 added 8 commits July 22, 2026 11:54
Per-workspace secret storage for env vars the agent references by name
(e.g. $OPENAI_API_KEY) so the literal value never enters model context.
Solves the guardrail problem: provider output filters block literal keys,
but if the model only writes $VAR_NAME and the shell expands it, there's
nothing to block.

Variant model: one var name can hold multiple labeled values (prod/staging/
test) with exactly one active. Switching active is a plaintext-metadata
flip — zero secret bytes touched.

Storage split:
- plaintext metadata (names, labels, active id) in IStorageService WORKSPACE
- encrypted values blob in ISecretStorageService APPLICATION, one key per
  workspace (void.envVar.<workspaceHash>). Reads (scrubber on every terminal
  result) vastly outnumber writes (interactive management), so one decrypt
  per read beats N decrypts perces designed: terminal injection (_createTerminal merges
active vars into IShellLaunchConfig.env), output scrubbing (replace known
values with [REDACTED:VAR_NAME] at every result point), LLM advertisement
(AVAILABLE_ENV_VARS block in volatile context, names only), management UI
(view-title button sibling to the allowlist shield).

Soft isolation: terminal still inherits base shell env so nvm/pyenv keep
working. strictEnv (hard isolation) is an additive later change on the same
storage layer.
Foundation for soft-isolation env var support. No consumers yet.

Two-tier storage:
- Plaintext metadata (var names, variant labels, active variant id) in
  IStorageService WORKSPACE scope (key void.workspaceEnvVarsI). Read every
  turn for LLM advertisement — no decrypt needed.
- Encrypted values blob in ISecretStorageService APPLICATION scope, one key
  per workspace (void.envVar.<workspaceHash>). One decrypt per read covers
  all variants; reads (scrubber on every terminal result) vastly outnumber
  writes (interactive management).

Variant model: one var name can hold multiple labeled values (prod/staging/
test) with exactly one active. Switching active is a plaintext-metadata flip
— zero secret bytes touched.

Workspace identity mirrorsspaceIdentity:
single-folder → folder URI, saved .code-workspace → configPath, untitled
multi-root → first folder URI (so adding/removing folders doesn't break
identity). Empty window → no-op.

Exports: addVar, addVariant, setActiveVariant, removeVar, removeVariant
(metadata + values CRUD), getActiveEnv (for terminal injection),
getAllEnvValues (for output scrubbing — all variants, not just active),
getActiveVarDescriptors (for LLM advertisement — names + labels only).
TerminalToolService._createTerminal now calls
IWorkspaceEnvVarService.getActiveEnv() and merges the result into
IShellLaunchConfig.env before creating the terminal.

Only terminals created via _createTerminal (hidden temporary ones from
run_command, plus persistent ones) get the injected vars. A terminal the
user opens manually via the + button doesn't go through this path, so
workspace secrets stay scoped to Void's agent terminals.

Failure to read (e.g. Keychain locked) logs and skips — terminal creation
must not fail because one secret is unreadable. The env is merged on top
of any caller-provided config.env so workspace vars take precedence.
Adds _scrubSecrets to TerminalToolService. At every point an output string
is produced (runCommand result, readTerminal, readTerminalByName — 4 return
points total), each known secret value is replaced with
[REDACTED:VAR_NAME].

Uses getAllEnvValues (all variants, active + inactive) so terminals created
under a now-inactive variant still get scrubbed — a persistent terminal keeps
its injected env even after the user switches the active variant.

Replacement uses split/join for literal string matching (no regex escaping).
Skips empty values to avoid infinite replacement. Best-effort: if the secret
read fails (Keychain locked), returns the text unscrubbed — the provider's
input guardrail is the backstop.

The commands array in readTerminalByName is NOT scrubbed — it contains the
command text the model typed (e.g. eiable
references, not secret values.
Instead of scrubbing all stored values (which redacts common config words
like 'development' from legitimate output), let the user choose per var
whether to scrub. Non-secret config like NODE_ENV=development gets
redact=false; secrets get redact=true (the default).

Changes:
- EnvVarEntry gains redact: boolean
- addVar accepts a redact param
- New setRedact(name, redact) for toggling in the management UI
- getVars() normalizes entries persisted before 'redact' existed to
  redact=true (safer default)
- getAllEnvValues renamed to getScrubableEnvValues, filtered to
  redact=true vars only — the scrubber only sees values the user marked
  for scrubbing
- TerminalToolService._scrubSecrets updated to call getScrubableEnvValues

No length threshold needeuristic.
generateChatVolatileContext now calls
IWorkspaceEnvVarService.getActiveVarDescriptors() and appends an
<available_env_vars> block to the volatile context. The block lists
var names + active variant labels (e.g. 'OPENAI_API_KEY (active: prod)'),
never values. Instructs the model to reference them by $VAR_NAME and
ask the user to add missing vars rather than searching.

The block is volatile (per-turn, not persisted into history), so
switching the active variant or adding/removing vars takes effect on the
next turn without touching the cacheable system message.
New view-title button (Codicon.key) sibling to the terminal-allowlist
shield. Opens a hierarchical QuickPick:

Top level: lists all vars as 'VAR_NAME (active: label)' + an
'Add new env var' entry.

Add var flow: name (validated against /^[A-Z_][A-Z0-9_]*$/) → variant
label → value (password-masked) → redact? (Yes/No pick).

Selecting a var drills into variant management:
- Switch active variant (pick from variants, (active) marker)
- Add variant (label + password-masked value)
- Toggle redact (flip between scrub/no-scrub)
- Remove var (drops metadata + encrypted values)

Values are never displayed — only labels. Value entry uses
password: true masking. ignoreFocusLost: true on all prompts so
switching focus mid-flow (e.g. to copy a key) doesn't cancel.
Replace the QuickPick env var UI with a proper React editor pane, mirroring
the global Settings pane pattern. The key icon opens a full form where all
env vars, their values, active state, and redact toggle are visible and
editable at once.

New files:
- workspaceSettingsPane.ts - EditorInput + EditorPane + open action (key icon)
- react/src/workspace-settings-tsx/WorkspaceSettings.tsx - React form
- react/src/workspace-settings-tsx/index.tsx - mount point

The form shows each env var as a card with:
- Name (fixed after creation)
- List of values (masked by default, eye toggle to reveal, radio to set
  active, trash to remove)
- Redact toggle (switch)
- Remove var button
Values added inline via Add value button. New vars created via Add env var
which opens an inline name+value creation row. Redact defaults to true.

Simplified env var model: dropped labels, ids, variants. A var is just a
list Index. getValues(name) added for the UI
to display masked previews. LLM advertisement simplified to just names.

tsup.config.js: added workspace-settings-tsx entry point.
void.contribution.ts: register workspaceSettingsPane.
services.tsx: expose IWorkspaceEnvVarService to React accessor.
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