Problem
Today we treat LLMConfiguration.apiKey as ambiguous: it can be a literal secret (e.g. sk-...) or an env-var-shaped reference (heuristic /^[A-Z0-9_]+$/).
This is a security footgun:
- encourages heuristic guessing instead of explicit meaning
- increases risk of accidentally persisting/logging secrets
- makes audits brittle because
apiKey doesn’t have a single meaning
Proposed direction
Change the model so LLM config never contains the raw secret:
- Replace
LLMConfiguration.apiKey with LLMConfiguration.apiKeyRef (explicit reference only).
- Resolve the reference to the actual secret only at the moment we need to send it over the network (provider client / request construction), and keep the secret value as ephemeral as possible.
This also implies: for security reasons, TS must not ever propagate a literal apiKey in LLM configuration again (beyond ephemeral runtime resolution).
Must be documented (non-negotiable)
This decision must be clearly documented in-code and in docs:
-
Comment on apiKeyRef
- at the type/property definition (what formats are allowed; what it means; what it must not contain).
-
Comments at every point where we determine/set the value
- either right where
apiKeyRef is set, or immediately before (so future contributors don’t reintroduce literal secrets/heuristics).
-
LLM Profiles webview
- add a comment at the right place in the webview code explaining the boundary + how
apiKeyRef is handled.
- consider whether we can store secrets directly to VS Code SecretStorage from the host side and keep the webview input strictly ephemeral.
-
Docstrings in the LLMConfiguration type
- clarify the invariants: no raw secret values in config, only references; how resolution works.
-
Update packages/agent-sdk-ts/docs/python-parity.md
- document that TypeScript uses
apiKeyRef and must not carry apiKey in LLM config (security reason).
-
Update settings_prd.md
- reflect the intended design and any user-facing implications.
Implementation sketch (high level)
- Introduce
apiKeyRef in LLMConfiguration (TS SDK + extension).
- Define explicit reference format(s) (e.g.
secret:OPENAI_API_KEY, etc.) and reject everything else.
- Remove
/^[A-Z0-9_]+$/ heuristics from:
packages/agent-sdk-ts/src/sdk/llm/factory.ts
packages/agent-sdk-ts/src/sdk/llm/profiles.ts
src/webview/host/llmProfilesStore.ts
- Ensure profile persistence never writes secrets by default (and ideally never at all).
- Add tests covering:
- allowed/denied
apiKeyRef formats
- no secret persistence
- no secret echoing to webview
Acceptance criteria
- No heuristic guessing of env-var-like strings.
- No raw provider secrets stored in profile JSON when
includeSecrets=false (ideally gate includeSecrets very hard and comment on it so we remember not to mess with it without human approval).
- LLM requests still work via resolved secrets from SecretStorage/SecretRegistry/env, but secrets are only materialized at request time and are not logged.
Problem
Today we treat
LLMConfiguration.apiKeyas ambiguous: it can be a literal secret (e.g.sk-...) or an env-var-shaped reference (heuristic/^[A-Z0-9_]+$/).This is a security footgun:
apiKeydoesn’t have a single meaningProposed direction
Change the model so LLM config never contains the raw secret:
LLMConfiguration.apiKeywithLLMConfiguration.apiKeyRef(explicit reference only).This also implies: for security reasons, TS must not ever propagate a literal
apiKeyin LLM configuration again (beyond ephemeral runtime resolution).Must be documented (non-negotiable)
This decision must be clearly documented in-code and in docs:
Comment on
apiKeyRefComments at every point where we determine/set the value
apiKeyRefis set, or immediately before (so future contributors don’t reintroduce literal secrets/heuristics).LLM Profiles webview
apiKeyRefis handled.Docstrings in the
LLMConfigurationtypeUpdate
packages/agent-sdk-ts/docs/python-parity.mdapiKeyRefand must not carryapiKeyin LLM config (security reason).Update
settings_prd.mdImplementation sketch (high level)
apiKeyRefinLLMConfiguration(TS SDK + extension).secret:OPENAI_API_KEY, etc.) and reject everything else./^[A-Z0-9_]+$/heuristics from:packages/agent-sdk-ts/src/sdk/llm/factory.tspackages/agent-sdk-ts/src/sdk/llm/profiles.tssrc/webview/host/llmProfilesStore.tsapiKeyRefformatsAcceptance criteria
includeSecrets=false(ideally gateincludeSecretsvery hard and comment on it so we remember not to mess with it without human approval).