Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ For another model provider, export its native credential variable and name it
with `--api-key-env`; for example, use
`--model anthropic/... --api-key-env ANTHROPIC_API_KEY`. The launcher does not
rename one provider's credential into another provider's variable. The selected
name is shown in Settings and its value is removed from CLI, cloud CLI,
Terraform, Ansible, and helper subprocesses.
name is shown in Settings. Registered OpenRouter, Anthropic, and OpenAI
credentials, plus any custom selected provider credential, are removed from
dependency, build, CLI, cloud CLI, Terraform, Ansible, and helper subprocesses.
Only the selected provider credential is restored to the LLM backend.

The state root and session/config directories are restricted to the console user
(`0700`), and the SQLite database and sidecars use `0600`. Startup repairs modes
Expand Down
8 changes: 6 additions & 2 deletions console/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,12 @@ The provider-native LLM API key selected by the launcher or entered in Settings
is kept in the backend process environment and is not returned by the API or
written to the console database. `OPENROUTER_API_KEY` remains the default for
the default OpenRouter model; other providers retain their own variable names.
Every registered LLM credential is removed from the environment passed to CLI,
cloud CLI, Terraform, Ansible, and helper subprocesses.
The launcher explicitly registers the supported OpenRouter, Anthropic, and
OpenAI credential variables plus any custom selected provider variable, then
removes their values before dependency, build, and frontend helpers run. Only
the selected credential is restored to the LLM backend. Every registered LLM
credential is also removed from the environment passed to CLI, cloud CLI,
Terraform, Ansible, and other backend subprocesses.

Cloud credentials are not collected or managed by the console. Provider tooling
uses the ambient credentials available to the launcher through environment
Expand Down
31 changes: 22 additions & 9 deletions console/backend/subprocess_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,43 @@

DEFAULT_LLM_SECRET_ENV = "OPENROUTER_API_KEY"
LLM_SECRET_ENV_SETTING = "DREADGOAD_CONSOLE_LLM_SECRET_ENV"
LLM_SECRET_ENVS_SETTING = "DREADGOAD_CONSOLE_LLM_SECRET_ENVS"

# These names pass the Settings route's credential-shaped-name validation, but
# they belong to infrastructure commands rather than the LLM. Treating one as
# an LLM key would silently remove a credential required by AWS descendants.
_INFRASTRUCTURE_CREDENTIAL_ENV_NAMES = frozenset(
{
"AWS_ACCESS_KEY_ID",
"AWS_CONTAINER_AUTHORIZATION_TOKEN",
"AWS_SECRET_ACCESS_KEY",
"AWS_SECURITY_TOKEN",
"AWS_SESSION_TOKEN",
"ARM_OIDC_TOKEN",
"LUDUS_API_KEY",
}
)

_configured_secret_env = (
os.environ.get(LLM_SECRET_ENV_SETTING, "").strip() or DEFAULT_LLM_SECRET_ENV
)
if _configured_secret_env in _INFRASTRUCTURE_CREDENTIAL_ENV_NAMES:
raise RuntimeError(
f"{LLM_SECRET_ENV_SETTING} cannot name infrastructure credential "
f"{_configured_secret_env}"
)
_configured_secret_env_names = {DEFAULT_LLM_SECRET_ENV, _configured_secret_env}
_configured_secret_env_names.update(
name.strip()
for name in os.environ.get(LLM_SECRET_ENVS_SETTING, "").split(",")
if name.strip()
)
for _secret_env_name in _configured_secret_env_names:
if _secret_env_name in _INFRASTRUCTURE_CREDENTIAL_ENV_NAMES:
raise RuntimeError(
f"LLM secret metadata cannot name infrastructure credential "
f"{_secret_env_name}"
)

# Start with only the configured provider credential. Rebinding an immutable
# snapshot lets child_env() iterate safely if a Settings request registers
# another key name at the same time as a command is starting.
_llm_secret_env_names = frozenset({_configured_secret_env})
# Start with every credential name registered by the launcher. Rebinding an
# immutable snapshot lets child_env() iterate safely if a Settings request
# registers another key name at the same time as a command is starting.
_llm_secret_env_names = frozenset(_configured_secret_env_names)
_active_llm_secret_env = _configured_secret_env


Expand All @@ -52,6 +63,7 @@ def register_llm_secret_env(name: str) -> None:
_llm_secret_env_names = _llm_secret_env_names | {name}
_active_llm_secret_env = name
os.environ[LLM_SECRET_ENV_SETTING] = name
os.environ[LLM_SECRET_ENVS_SETTING] = ",".join(sorted(_llm_secret_env_names))


def active_llm_secret_env() -> str:
Expand All @@ -70,4 +82,5 @@ def child_env() -> dict[str, str]:
for name in _llm_secret_env_names:
env.pop(name, None)
env.pop(LLM_SECRET_ENV_SETTING, None)
env.pop(LLM_SECRET_ENVS_SETTING, None)
return env
Loading
Loading