From f9e969327310c723e019113e2534687c24242508 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sat, 15 Aug 2026 11:25:24 +1200 Subject: [PATCH] Add secrets to the Run Configuration, via SecretStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Secrets a local run needs — a feed token, a signing password — alongside the plain parameters, but handled differently at every step. Values go to VS Code's SecretStorage, which is OS keychain-backed, rather than workspaceState; only the *names* are kept in workspace state so the view can list them. The webview is never sent a value: it renders a fixed mask, so a secret cannot leak through a webview devtools inspection or a state snapshot. They resolve to environment variables rather than CLI arguments. An argument would appear in shell history and in the process list, where any other process on the machine can read it; the environment of a spawned process is not exposed that way. buildEnv() resolves them at run time rather than caching, so a secret rotated in the keychain takes effect on the next run without reloading the window. Nothing consumes the environment yet; wiring it into the run flow is #14. Closes #13 Co-Authored-By: Claude Opus 5 --- src/runConfig.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/runConfig.ts b/src/runConfig.ts index eb2f7e6..df1a27c 100644 --- a/src/runConfig.ts +++ b/src/runConfig.ts @@ -7,11 +7,15 @@ export interface Parameter { } const PARAMS_KEY = 'fallout.parameters'; +const SECRET_NAMES_KEY = 'fallout.secretNames'; +const SECRET_PREFIX = 'fallout.secret.'; /** - * Persists the local run configuration. Parameters live in workspace state — they are - * per-workspace by nature (a configuration for *this* build), and plain enough to sit - * in plugin storage. They are rendered as CLI args (`--name value`) for a local run. + * Persists the local run configuration: parameters live in workspace state (plain, + * per-workspace); secret *values* live in VS Code's SecretStorage (OS keychain-backed) + * and are never rendered into the webview — only their names are. Parameters become + * CLI args (`--name value`); secrets become environment variables so they don't leak + * into shell history or the process list. */ export class RunConfigStore { private readonly onDidChangeEmitter = new vscode.EventEmitter(); @@ -36,19 +40,49 @@ export class RunConfigStore { this.onDidChangeEmitter.fire(); } + getSecretNames(): string[] { + return this.context.workspaceState.get(SECRET_NAMES_KEY, []); + } + + async setSecret(name: string, value: string): Promise { + await this.context.secrets.store(SECRET_PREFIX + name, value); + if (!this.getSecretNames().includes(name)) { + await this.context.workspaceState.update(SECRET_NAMES_KEY, [...this.getSecretNames(), name].sort()); + } + this.onDidChangeEmitter.fire(); + } + + async removeSecret(name: string): Promise { + await this.context.secrets.delete(SECRET_PREFIX + name); + await this.context.workspaceState.update(SECRET_NAMES_KEY, this.getSecretNames().filter(n => n !== name)); + this.onDidChangeEmitter.fire(); + } + /** Parameters rendered as a CLI argument string, e.g. `--configuration Release`. */ buildArgs(): string { return this.getParameters() .map(p => `--${p.name} ${quoteArg(p.value)}`) .join(' '); } + + /** Resolves all stored secrets into an environment map for a local run. */ + async buildEnv(): Promise> { + const env: Record = {}; + for (const name of this.getSecretNames()) { + const value = await this.context.secrets.get(SECRET_PREFIX + name); + if (value !== undefined) { + env[name] = value; + } + } + return env; + } } function quoteArg(value: string): string { return /\s/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; } -/** The "Run Configuration" webview view — a form for build parameters. */ +/** The "Run Configuration" webview view — a form for parameters and secret names. */ export class RunConfigViewProvider implements vscode.WebviewViewProvider { private view: vscode.WebviewView | undefined; @@ -75,6 +109,12 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { case 'removeParameter': await this.store.removeParameter(String(message.name)); break; + case 'setSecret': + if (message.name && message.value) { await this.store.setSecret(String(message.name), String(message.value)); } + break; + case 'removeSecret': + await this.store.removeSecret(String(message.name)); + break; } }); } @@ -83,6 +123,7 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { void this.view?.webview.postMessage({ type: 'state', parameters: this.store.getParameters(), + secretNames: this.store.getSecretNames(), }); } @@ -106,6 +147,7 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { button.icon { background: transparent; color: var(--vscode-foreground); padding: 3px 6px; } .name { flex: 0 0 40%; font-family: var(--vscode-editor-font-family); } .hint { opacity: 0.65; font-size: 11px; margin: 2px 0 8px; } + .secret-val { font-family: var(--vscode-editor-font-family); opacity: 0.6; } @@ -118,6 +160,15 @@ export class RunConfigViewProvider implements vscode.WebviewViewProvider { +

Secrets

+
Stored in the OS keychain (SecretStorage); passed as environment variables. Values are never shown.
+
+
+ + + +
+