feat(code): add LangSmith tracing config to /auth (#4193)

LangSmith tracing can now be configured directly from `/auth` (and
`dcode auth set langsmith`), including enabling tracing, a temporary
opt-out via `DEEPAGENTS_CODE_LANGSMITH_TRACING=false`, and a custom
project name.

---

Wires LangSmith tracing into the `/auth` surface as a configurable
service, mirroring how Tavily is already handled. Storing a LangSmith
key via `/auth` persists it to the credential store, bridges it onto
`LANGSMITH_API_KEY`, and opts the user into tracing — so configuring
tracing no longer requires exporting environment variables before
launch.

Key behaviors:
- A stored key turns tracing on by default. An explicit falsy tracing
flag (most simply `DEEPAGENTS_CODE_LANGSMITH_TRACING=false`) is honored
as a non-destructive, session-scoped opt-out — pause tracing without
deleting the key. The TUI prompt surfaces this path.
- An optional project name (advanced field in the TUI prompt,
`--project` on `dcode auth set`) routes traces to a custom
`LANGSMITH_PROJECT`; the default stays `deepagents-code`.
- Services are now surfaced in `dcode auth list`/`status` (this also
fixes a pre-existing gap where Tavily was invisible to the CLI).
- Bootstrap reordered so a `/auth`-stored key is applied before
orphaned-tracing detection, keeping tracing alive instead of being
disabled.

Made by [Open
SWE](https://openswe.vercel.app/agents/2ba1485c-7c70-59b6-82bf-18302c214c6c)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-06-24 02:24:50 -04:00
committed by GitHub
parent 23f52054e1
commit 8e62957910
16 changed files with 1104 additions and 152 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ aliases, descriptions, visibility, or hidden-command metadata.
| Command | Aliases | Description |
| --- | --- | --- |
| `/agents` | | Browse and switch between available agents |
| `/auth` | `/connect` | Connect and manage model provider credentials |
| `/auth` | `/connect` | Connect and manage provider and service credentials |
| `/auto-update` | | Turn automatic updates on or off |
| `/changelog` | | Open the changelog in a browser |
| `/clear` | | Clear the chat and start a new thread |
+2
View File
@@ -63,6 +63,7 @@ from deepagents_code.config import (
get_default_coding_instructions,
get_glyphs,
get_langsmith_project_name,
restore_user_tracing_env,
settings,
)
from deepagents_code.configurable_model import ConfigurableModelMiddleware
@@ -1523,6 +1524,7 @@ def create_cli_agent(
shell_env["LANGSMITH_PROJECT"] = settings.user_langchain_project
else:
shell_env.pop("LANGSMITH_PROJECT", None)
restore_user_tracing_env(shell_env)
# Re-apply a launch-time PYTHONPATH that was stripped from the server
# interpreter but relayed for approval-gated `execute` commands.
_apply_inherited_pythonpath(shell_env)
+52 -7
View File
@@ -101,6 +101,13 @@ def setup_auth_parser(
default=None,
help="Copy the key from this environment variable instead of stdin",
)
set_parser.add_argument(
"--project",
dest="project",
metavar="NAME",
default=None,
help="With `set langsmith`, set a custom LangSmith project name",
)
set_parser.add_argument(
"-h",
"--help",
@@ -160,7 +167,11 @@ def run_auth_command(args: argparse.Namespace) -> int:
if command in {"list", "ls"}:
return _run_list()
if command == "set":
return _run_set(args.provider, from_env=args.from_env)
return _run_set(
args.provider,
from_env=args.from_env,
project=getattr(args, "project", None),
)
if command in {"remove", "rm", "delete"}:
return _run_remove(args.provider)
if command == "status":
@@ -233,6 +244,7 @@ def _known_providers() -> tuple[list[str], str | None]:
from deepagents_code.model_config import (
CODEX_PROVIDER,
PROVIDER_API_KEY_ENV,
SERVICE_API_KEY_ENV,
ModelConfig,
get_available_models,
)
@@ -253,8 +265,12 @@ def _known_providers() -> tuple[list[str], str | None]:
# no API-key env var entry. Mirror the TUI auth manager by showing it when
# the OpenAI integration was discovered.
codex_installed = {CODEX_PROVIDER} if "openai" in installed else set()
# Non-model services (Tavily web search, LangSmith tracing) are always
# shown — they are configurable here regardless of any backing package —
# so the CLI listing matches the TUI `/auth` manager.
services = set(SERVICE_API_KEY_ENV)
providers = sorted(
well_known_installed | codex_installed | stored | config_providers
well_known_installed | codex_installed | stored | config_providers | services
)
return providers, warning
@@ -288,13 +304,22 @@ def _warn_if_store_unreadable() -> None:
def _print_rows(providers: list[str]) -> None:
"""Print one `<provider> <status>` row per provider, column-aligned."""
from deepagents_code.model_config import get_provider_auth_status
from deepagents_code.model_config import (
get_provider_auth_status,
get_service_auth_status,
is_service,
)
if not providers:
return
width = max(len(name) for name in providers)
for provider in providers:
label = _resolution_label(get_provider_auth_status(provider))
status = (
get_service_auth_status(provider)
if is_service(provider)
else get_provider_auth_status(provider)
)
label = _resolution_label(status)
# Plain stdout so rows stay greppable/pipeable, not Rich-styled.
print(f"{provider.ljust(width)} {label}") # noqa: T201
@@ -333,13 +358,17 @@ def _run_status(provider: str | None) -> int:
return 0
def _run_set(provider: str, *, from_env: str | None) -> int:
def _run_set(provider: str, *, from_env: str | None, project: str | None) -> int:
"""Store an API key for `provider`, reading it from env or stdin.
Returns:
Process exit code (`0` on success, `1` on a recoverable input error).
"""
from deepagents_code.model_config import CODEX_PROVIDER
from deepagents_code.model_config import (
CODEX_PROVIDER,
LANGSMITH_SERVICE,
is_langsmith,
)
if provider == CODEX_PROVIDER:
print( # noqa: T201
@@ -349,6 +378,13 @@ def _run_set(provider: str, *, from_env: str | None) -> int:
)
return 1
if project is not None and not is_langsmith(provider):
print( # noqa: T201
f"Error: --project is only valid for {LANGSMITH_SERVICE}.",
file=sys.stderr,
)
return 1
import os
from deepagents_code import auth_store
@@ -383,8 +419,17 @@ def _run_set(provider: str, *, from_env: str | None) -> int:
return 1
try:
# Preserve a previously stored project unless `--project` overrides it
# (an explicit empty value clears it). Resolved inside the try so a
# corrupt store surfaces as a clean error, not a traceback.
stored_project = (
project if project is not None else auth_store.get_stored_project(provider)
)
outcome = auth_store.set_stored_key(
provider, key, base_url=auth_store.get_stored_base_url(provider)
provider,
key,
base_url=auth_store.get_stored_base_url(provider),
project=stored_project,
)
except (ValueError, RuntimeError) as exc:
# `auth_store` messages never include the secret value. `ValueError`
+65 -2
View File
@@ -67,6 +67,15 @@ class ApiKeyCredential(TypedDict):
the URL.
"""
project: NotRequired[str]
"""Optional LangSmith project name paired with this credential.
Set only for the `langsmith` tracing service when the user supplies a
custom project in `/auth`; absent means traces fall back to the default
(`deepagents-code`). Not a secret — it is shown in the `/auth` advanced
panel and applied to `LANGSMITH_PROJECT` at startup.
"""
class OAuthCredential(TypedDict):
"""A persisted OAuth subscription credential.
@@ -312,6 +321,15 @@ def _coerce_credential(raw: Any) -> StoredCredential | None: # noqa: ANN401
logger.warning(
"Ignoring malformed base_url for a stored credential: %r", base_url
)
project = raw.get("project")
if isinstance(project, str) and project:
credential["project"] = project
elif project is not None:
# Same rationale as `base_url`: a hand-edited non-string value is
# dropped with a trace. `project` is non-secret, so logging is safe.
logger.warning(
"Ignoring malformed project for a stored credential: %r", project
)
return credential
# OAuth is reserved for a future PR — silently skip until the producer
# path lands. `cred_type in {"oauth"}` falls through to None here.
@@ -352,8 +370,28 @@ def get_stored_base_url(provider: str) -> str | None:
return entry.get("base_url") or None
def get_stored_project(provider: str) -> str | None:
"""Return the LangSmith project paired with `provider`'s stored key, or `None`.
Returns `None` when no key is stored and when a key is stored without a
custom project (the user left the field blank, meaning "use the default").
Raises:
RuntimeError: If the credential file is corrupt.
""" # noqa: DOC502 - re-raised from `_read_raw` via `load_credentials`
creds = load_credentials()
entry = creds.get(provider)
if entry is None or entry["type"] != "api_key":
return None
return entry.get("project") or None
def set_stored_key(
provider: str, key: str, *, base_url: str | None = None
provider: str,
key: str,
*,
base_url: str | None = None,
project: str | None = None,
) -> WriteOutcome:
"""Persist an API key for `provider`.
@@ -362,19 +400,31 @@ def set_stored_key(
`apply_stored_credentials` in `model_config` — a stored empty would
unconditionally overwrite the env var).
This rewrites the whole credential record: `base_url` and `project` are
*not* merged with any previously stored values. Passing blank/`None` for
either clears it, so a caller rotating a key while wanting to keep the
existing endpoint/project must read it back (e.g. via `get_stored_base_url`
/ `get_stored_project`) and pass it in again.
Args:
provider: Provider identifier (e.g., `"anthropic"`).
key: The API key value. Whitespace is stripped before storage.
base_url: Optional provider endpoint to pair with the key. Whitespace
is stripped; blank/`None` stores no endpoint, meaning the key uses
the provider default rather than any inherited (e.g. gateway) URL.
project: Optional LangSmith project name to pair with the key. Valid
only for the `langsmith` tracing service. Whitespace is stripped;
blank/`None` stores no project, meaning traces use the default
project.
Returns:
A `WriteOutcome` whose `warnings` tuple lists chmod failures the
caller should surface to the user. Empty on a clean save.
Raises:
ValueError: If `provider` or the stripped `key` is empty.
ValueError: If `provider` or the stripped `key` is empty, or a non-empty
`project` is paired with a provider other than the `langsmith`
service.
RuntimeError: If the credential file is corrupt and cannot be read, or
the new file cannot be written (e.g. no disk space or an
unwritable state directory).
@@ -398,6 +448,19 @@ def set_stored_key(
cleaned_base_url = base_url.strip() if base_url else ""
if cleaned_base_url:
entry["base_url"] = cleaned_base_url
cleaned_project = project.strip() if project else ""
if cleaned_project:
# A project name is meaningful only for the LangSmith tracing service;
# enforce the invariant at the write boundary so a stray project can
# never be persisted onto an unrelated provider, regardless of caller.
# Lazy import avoids a circular dependency (model_config imports this
# module), matching the pattern in `auth_path`.
from deepagents_code.model_config import is_langsmith
if not is_langsmith(provider):
msg = f"project is only valid for the langsmith service, not {provider!r}"
raise ValueError(msg)
entry["project"] = cleaned_project
creds[provider] = entry
data["version"] = _STORAGE_VERSION
data["credentials"] = creds
@@ -80,9 +80,11 @@ COMMANDS: tuple[SlashCommand, ...] = (
),
SlashCommand(
name="/auth",
description="Connect and manage model provider credentials",
description="Connect and manage provider and service credentials",
bypass_tier=BypassTier.IMMEDIATE_UI,
hidden_keywords="key keys credential credentials login token api",
hidden_keywords=(
"key keys credential credentials login token api tracing langsmith"
),
aliases=("/connect",),
),
SlashCommand(
+167 -45
View File
@@ -12,7 +12,7 @@ import shlex
import shutil
import sys
import threading
from dataclasses import dataclass
from dataclasses import dataclass, field as dataclass_field
from enum import StrEnum
from importlib.metadata import PackageNotFoundError, distribution, version
from pathlib import Path
@@ -41,8 +41,26 @@ logger = logging.getLogger(__name__)
# callers that never touch `settings` (e.g. `deepagents --help`).
# ---------------------------------------------------------------------------
_bootstrap_done = False
"""Whether `_ensure_bootstrap()` has executed."""
@dataclass
class _BootstrapState:
"""Mutable state captured by `_ensure_bootstrap()`."""
done: bool = False
"""Whether `_ensure_bootstrap()` has executed."""
start_path: Path | None = None
"""Working directory captured at bootstrap time for dotenv and discovery."""
original_langsmith_project: str | None = None
"""Caller's `LANGSMITH_PROJECT` before the app overrides it for traces."""
original_tracing_env: dict[str, str | None] = dataclass_field(default_factory=dict)
"""Caller's tracing-enable env before Deep Agents Code mutates flags."""
_bootstrap_state = _BootstrapState()
"""State captured and mutated by lazy bootstrap."""
_bootstrap_lock = threading.Lock()
"""Guards `_ensure_bootstrap()` against concurrent access from the main thread
@@ -51,16 +69,6 @@ and the prewarm worker thread."""
_singleton_lock = threading.Lock()
"""Guards lazy singleton construction in `_get_console` / `_get_settings`."""
_bootstrap_start_path: Path | None = None
"""Working directory captured at bootstrap time for dotenv and project discovery."""
_original_langsmith_project: str | None = None
"""Caller's `LANGSMITH_PROJECT` value before the app overrides it for agent traces.
Captured inside `_ensure_bootstrap()` after dotenv loading but before the
`LANGSMITH_PROJECT` override, so `.env`-only values are visible.
"""
_dotenv_loaded_values: dict[str, str] = {}
"""Environment values injected by our dotenv loader and safe to refresh later."""
@@ -438,6 +446,31 @@ def _tracing_enabled() -> bool:
)
def _disable_set_tracing_flags() -> list[str]:
"""Set every configured tracing-enable flag to `false`.
Returns:
Env var names that were disabled.
"""
disabled = [var for var in _TRACING_ENABLE_ENV_VARS if var in os.environ]
for var in disabled:
os.environ[var] = "false"
return disabled
def restore_user_tracing_env(env: dict[str, str]) -> None:
"""Restore caller tracing flags in an environment passed to user code.
Args:
env: Environment mapping prepared for a child/user subprocess.
"""
for var, value in _bootstrap_state.original_tracing_env.items():
if value is None:
env.pop(var, None)
else:
env[var] = value
def _disable_orphaned_tracing() -> None:
"""Disable LangSmith tracing when enabled without a usable API key.
@@ -471,9 +504,7 @@ def _disable_orphaned_tracing() -> None:
if has_key or _has_langsmith_profile_credentials():
return
disabled = [var for var in _TRACING_ENABLE_ENV_VARS if var in os.environ]
for var in disabled:
os.environ[var] = "false"
disabled = _disable_set_tracing_flags()
_orphaned_tracing_disabled_notice = _build_orphaned_tracing_disabled_notice()
logger.warning(
"LangSmith tracing is enabled (%s) but no API key is set; disabling "
@@ -503,6 +534,101 @@ def _apply_default_langsmith_project() -> None:
os.environ["LANGSMITH_PROJECT"] = LANGSMITH_PROJECT_DEFAULT
def apply_stored_langsmith_auth(*, replace_project: bool = False) -> None:
"""Apply a `/auth`-stored LangSmith key and tracing settings now.
Args:
replace_project: Whether the stored LangSmith project should replace
the current process `LANGSMITH_PROJECT`. Startup leaves this false
so an explicit environment value remains authoritative; the `/auth`
save path sets it true because the saved project is the newest user
choice for the already-running session.
"""
from deepagents_code.model_config import apply_stored_service_credentials
apply_stored_service_credentials()
_apply_stored_langsmith_tracing(replace_project=replace_project)
_disable_orphaned_tracing()
_apply_default_langsmith_project()
def _apply_stored_langsmith_tracing(*, replace_project: bool = False) -> None:
"""Enable tracing (and apply a custom project) for a `/auth`-stored key.
Storing a LangSmith key via `/auth` is a deliberate opt-in to tracing, but
a key alone never starts tracing — the SDK only traces when a tracing-enable
flag is truthy. So when a key is stored, turn tracing on by default.
The opt-out is intentionally non-destructive and session-scoped: an explicit
falsy tracing flag (most simply `DEEPAGENTS_CODE_LANGSMITH_TRACING=false`,
which bootstrap bridges to `LANGSMITH_TRACING`) is honored and tracing stays
off, so the stored key can be paused without deleting it. A custom stored
project is applied to `LANGSMITH_PROJECT` when the user has not set one,
unless `replace_project` is set for the immediate `/auth` save path.
No-op when no LangSmith key is stored, so a key supplied only through the
environment keeps the prior behavior (tracing stays off unless a flag is
set).
A stored key is trusted by *presence*, not validity: this never pings
LangSmith (a network round-trip at startup would fight the package's
startup-perf budget). So a stored-but-invalid key (typo'd, revoked, or for
the wrong workspace) still force-enables tracing, and its traces are then
silently dropped at ingest with only SDK-internal 401s — which
`_quiet_sdk_tracing_logging` routes away from the TUI. `_disable_orphaned_tracing`
and `consume_orphaned_tracing_disabled_notice` guard only the *absent*-key
case, not the invalid-key case. If traces never appear, the key is the first
thing to re-check via `/auth`.
The store is read exactly once: a single corrupt-file `RuntimeError` is
logged and treated as "no stored key" rather than being raised (bootstrap
must never crash the app) or partially applied.
"""
from deepagents_code import auth_store
from deepagents_code._env_vars import classify_env_bool
from deepagents_code.model_config import LANGSMITH_SERVICE
try:
creds = auth_store.load_credentials()
except RuntimeError:
logger.warning(
"Could not read the stored LangSmith credential; the credential file "
"may be corrupt. Re-add the key via /auth."
)
return
entry = creds.get(LANGSMITH_SERVICE)
# No-op unless a LangSmith API key was stored via `/auth`. A key supplied
# only through the environment never lands here, keeping its prior behavior
# (tracing stays off unless a flag is set).
if entry is None or entry["type"] != "api_key" or not entry["key"]:
return
# The key was bridged onto LANGSMITH_API_KEY by
# `apply_stored_service_credentials`. Decide whether to enable tracing.
flags = [
classify_env_bool(os.environ[var])
for var in _TRACING_ENABLE_ENV_VARS
if var in os.environ
]
if any(flag is False for flag in flags):
# Explicit, deliberate opt-out — keep the key but make the opt-out
# authoritative over sibling SDK tracing flags.
_disable_set_tracing_flags()
return
if not any(flag is True for flag in flags):
os.environ["LANGSMITH_TRACING"] = "true"
project = entry.get("project") or None
if replace_project:
if project:
os.environ["LANGSMITH_PROJECT"] = project
else:
os.environ.pop("LANGSMITH_PROJECT", None)
return
if project and not os.environ.get("LANGSMITH_PROJECT"):
os.environ["LANGSMITH_PROJECT"] = project
def _ensure_bootstrap() -> None:
"""Run one-time bootstrap: dotenv loading and `LANGSMITH_PROJECT` override.
@@ -514,13 +640,11 @@ def _ensure_bootstrap() -> None:
loops. Exceptions are caught and logged at ERROR level; the app proceeds
with the environment as-is.
"""
global _bootstrap_done, _bootstrap_start_path, _original_langsmith_project # noqa: PLW0603
if _bootstrap_done:
if _bootstrap_state.done:
return
with _bootstrap_lock:
if _bootstrap_done: # double-check after acquiring lock
if _bootstrap_state.done: # double-check after acquiring lock
return
try:
@@ -529,8 +653,8 @@ def _ensure_bootstrap() -> None:
)
ctx = _get_server_project_context()
_bootstrap_start_path = ctx.user_cwd if ctx else None
_load_dotenv(start_path=_bootstrap_start_path)
_bootstrap_state.start_path = ctx.user_cwd if ctx else None
_load_dotenv(start_path=_bootstrap_state.start_path)
# `configure_debug_logging` already ran at import, before the `.env`
# above was loaded. Re-run it so a `DEEPAGENTS_CODE_DEBUG` set only in
@@ -546,7 +670,12 @@ def _ensure_bootstrap() -> None:
# Capture AFTER dotenv loading so .env-only values are visible,
# but BEFORE the override below replaces it.
_original_langsmith_project = os.environ.get("LANGSMITH_PROJECT")
_bootstrap_state.original_langsmith_project = os.environ.get(
"LANGSMITH_PROJECT"
)
_bootstrap_state.original_tracing_env = {
var: os.environ.get(var) for var in _TRACING_ENABLE_ENV_VARS
}
# CRITICAL: Override LANGSMITH_PROJECT to route agent traces to a
# separate project. LangSmith reads LANGSMITH_PROJECT at invocation
@@ -589,29 +718,18 @@ def _ensure_bootstrap() -> None:
canonical,
)
# Tracing enabled without a key floods the TUI with 401 ingest
# errors; disable it before any traced run starts.
_disable_orphaned_tracing()
# If tracing is still active but no project is configured, default
# to `deepagents-code` so ingestion matches the name we display and
# look up. Runs after `_disable_orphaned_tracing` so a keyless setup
# (tracing already turned off) is left untouched.
_apply_default_langsmith_project()
# Bridge stored service keys (e.g. Tavily web search, entered via
# `/auth`) onto their canonical env vars before settings detection,
# so a stored key activates the feature without exporting the var.
from deepagents_code.model_config import apply_stored_service_credentials
apply_stored_service_credentials()
# Bridge stored service keys, apply stored LangSmith tracing defaults,
# disable orphaned tracing, and route active tracing to the displayed
# project. Keeping this in one helper lets `/auth` save apply the same
# state immediately inside an already-running TUI session.
apply_stored_langsmith_auth()
except Exception:
logger.exception(
"Bootstrap failed; .env values and LANGSMITH_PROJECT override "
"may be missing. The app will proceed with environment as-is.",
)
finally:
_bootstrap_done = True
_bootstrap_state.done = True
if TYPE_CHECKING:
@@ -1783,7 +1901,9 @@ class Settings:
)
deepagents_langchain_project = resolve_env_var(LANGSMITH_PROJECT)
user_langchain_project = _original_langsmith_project # Use saved original!
# Use the saved original, not the current `LANGSMITH_PROJECT` that
# bootstrap may have overridden for agent traces.
user_langchain_project = _bootstrap_state.original_langsmith_project
# Detect project
from deepagents_code.project_utils import find_project_root
@@ -1987,8 +2107,10 @@ class Settings:
# re-apply the default so ingestion keeps matching the name
# `get_langsmith_project_name` displays (the default is a no-op when
# tracing is off, so a disabled setup is left unset).
if _original_langsmith_project:
os.environ["LANGSMITH_PROJECT"] = _original_langsmith_project
if _bootstrap_state.original_langsmith_project:
os.environ["LANGSMITH_PROJECT"] = (
_bootstrap_state.original_langsmith_project
)
else:
os.environ.pop("LANGSMITH_PROJECT", None)
_apply_default_langsmith_project()
@@ -3788,11 +3910,11 @@ def _get_settings() -> Settings:
return cached
_ensure_bootstrap()
try:
inst = Settings.from_environment(start_path=_bootstrap_start_path)
inst = Settings.from_environment(start_path=_bootstrap_state.start_path)
except Exception:
logger.exception(
"Failed to initialize settings from environment (start_path=%s)",
_bootstrap_start_path,
_bootstrap_state.start_path,
)
raise
globals()["settings"] = inst
+34 -8
View File
@@ -547,15 +547,25 @@ Providers not listed here fall through to the config-file check or the langchain
registry fallback.
"""
LANGSMITH_SERVICE = "langsmith"
"""Service name for LangSmith tracing in `SERVICE_API_KEY_ENV`.
Storing a key for this service via `/auth` also enables tracing at startup
(see `config._apply_stored_langsmith_tracing`) and can carry a custom project
name, so it gets special handling beyond a plain key copy.
"""
SERVICE_API_KEY_ENV: dict[str, str] = {
LANGSMITH_SERVICE: "LANGSMITH_API_KEY",
"tavily": "TAVILY_API_KEY",
}
"""Non-model services configurable via `/auth`, mapped to their API-key env var.
These are not LLM providers — they back features such as web search — but
their credentials follow the same store-on-disk model as model providers, so
they appear in the `/auth` manager and can be entered directly in the TUI
instead of being exported as environment variables before launch.
These are not LLM providers — they back features such as web search (Tavily) or
agent tracing (LangSmith) — but their credentials follow the same store-on-disk
model as model providers, so they appear in the `/auth` manager and can be
entered directly in the TUI instead of being exported as environment variables
before launch.
"""
CODEX_PROVIDER = "openai_codex"
@@ -2012,6 +2022,16 @@ def is_service(name: str) -> bool:
return name in SERVICE_API_KEY_ENV
def is_langsmith(name: str) -> bool:
"""Return whether `name` is the LangSmith tracing service.
Centralizes the identity check so the LangSmith-specific branches (project
field instead of a base URL, tracing auto-enable) share one definition
rather than scattering `== LANGSMITH_SERVICE` comparisons.
"""
return name == LANGSMITH_SERVICE
def get_service_auth_status(service: str) -> ProviderAuthStatus:
"""Return credential readiness for a non-model service (e.g. `"tavily"`).
@@ -2042,9 +2062,10 @@ def apply_stored_service_credentials() -> None:
Services (e.g. web search via Tavily) have no base URL to reconcile, so
this is a plain key copy onto the canonical env var name the underlying
SDK reads. A stored key takes precedence over an existing env var, matching
`apply_stored_credentials`. Only the unprefixed canonical name is written,
so a `DEEPAGENTS_CODE_`-prefixed override still wins via `resolve_env_var`.
SDK reads. A stored key takes precedence over an existing plain env var,
matching `apply_stored_credentials`; a `DEEPAGENTS_CODE_`-prefixed override
is left authoritative because the app already treats it as the top-priority
per-session credential.
"""
for service, env_var in SERVICE_API_KEY_ENV.items():
try:
@@ -2056,7 +2077,12 @@ def apply_stored_service_credentials() -> None:
service,
)
continue
if stored and os.environ.get(env_var) != stored:
if not stored:
continue
prefixed = f"{_ENV_PREFIX}{env_var}"
if prefixed in os.environ:
continue
if os.environ.get(env_var) != stored:
os.environ[env_var] = stored
+4
View File
@@ -635,6 +635,7 @@ def show_auth_help() -> None:
console.print()
console.print("[bold]Options:[/bold]", style=theme.PRIMARY)
console.print(" --from-env VAR With `set`, copy the key from env var VAR")
console.print(" --project NAME With `set langsmith`, set the trace project")
console.print(" -h, --help Show this help message")
console.print()
console.print(
@@ -648,6 +649,9 @@ def show_auth_help() -> None:
console.print(" dcode auth list")
console.print(" echo $ANTHROPIC_API_KEY | dcode auth set anthropic")
console.print(" dcode auth set openai --from-env OPENAI_API_KEY")
console.print(
" echo $LANGSMITH_API_KEY | dcode auth set langsmith --project my-app"
)
console.print(" dcode auth status anthropic")
console.print(" dcode auth remove anthropic")
console.print(" dcode auth path")
+92 -29
View File
@@ -41,7 +41,11 @@ if TYPE_CHECKING:
from deepagents_code import auth_store, theme
from deepagents_code.auth_display import format_auth_badge
from deepagents_code.config import get_glyphs, is_ascii_mode
from deepagents_code.config import (
apply_stored_langsmith_auth,
get_glyphs,
is_ascii_mode,
)
from deepagents_code.model_config import (
CODEX_PROVIDER,
PROVIDER_API_KEY_ENV,
@@ -58,6 +62,7 @@ from deepagents_code.model_config import (
get_default_base_url_env,
get_provider_auth_status,
get_service_auth_status,
is_langsmith,
is_service,
resolved_env_var_name,
)
@@ -83,6 +88,7 @@ PROVIDER_DISPLAY_NAMES: dict[str, str] = {
"groq": "Groq",
"huggingface": "Hugging Face",
"ibm": "IBM watsonx",
"langsmith": "LangSmith (tracing)",
"litellm": "LiteLLM",
"mistralai": "Mistral AI",
"nvidia": "NVIDIA",
@@ -105,6 +111,7 @@ PROVIDER_API_KEY_URLS: dict[str, str] = {
"groq": "https://console.groq.com/keys",
"huggingface": "https://huggingface.co/login?next=%2Fsettings%2Ftokens",
"ibm": "https://cloud.ibm.com/iam/apikeys",
"langsmith": "https://smith.langchain.com/settings",
"litellm": "https://docs.litellm.ai/docs/proxy/virtual_keys",
"mistralai": "https://console.mistral.ai/api-keys",
"nvidia": "https://build.nvidia.com/settings/api-keys",
@@ -495,6 +502,10 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
self._provider = provider
self._env_var = env_var
self._reason = reason
# LangSmith is configured as a tracing service: it has no base-URL
# override but does carry an optional project name, and saving a key
# turns tracing on.
self._is_langsmith = is_langsmith(provider)
# Resolve the current credential source and probe the store, but never
# let a corrupt `auth.json`/config crash the screen at construction
# time — Textual would propagate the exception before the modal mounts.
@@ -508,7 +519,10 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
self._auth_status = _auth_status_for(provider)
self._has_existing = auth_store.get_stored_key(provider) is not None
self._existing_base_url = auth_store.get_stored_base_url(provider) or ""
self._advanced_visible = bool(self._existing_base_url)
self._existing_project = auth_store.get_stored_project(provider) or ""
self._advanced_visible = bool(
self._existing_base_url or self._existing_project
)
self._store_warning: str | None = None
except RuntimeError as exc:
logger.warning(
@@ -520,6 +534,7 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
)
self._has_existing = False
self._existing_base_url = ""
self._existing_project = ""
self._advanced_visible = False
self._store_warning = (
f"Credential file is unreadable ({exc}). Saving here will overwrite it."
@@ -629,12 +644,20 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
password=True,
id="auth-prompt-input",
)
yield Static(
Content.from_markup(
if self._is_langsmith:
storage_note = Content.from_markup(
"Deep Agents Code stores the above key locally and turns on "
"LangSmith tracing. To pause tracing without removing the key, "
"set [bold]DEEPAGENTS_CODE_LANGSMITH_TRACING=false[/bold]."
)
else:
storage_note = Content.from_markup(
"Deep Agents Code stores the above key locally and uses it "
"when you select [bold]$provider[/bold] models.",
provider=provider_label,
),
)
yield Static(
storage_note,
classes="auth-prompt-meta",
id="auth-prompt-storage-note",
)
@@ -665,27 +688,53 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
)
key_meta.display = self._advanced_visible
yield key_meta
base_url_label = Static(
Content.from_markup("[bold]Base URL override[/bold]"),
classes="auth-prompt-meta",
id="auth-prompt-base-url-label",
)
base_url_label.display = self._advanced_visible
yield base_url_label
base_url_input = Input(
value=self._existing_base_url,
placeholder="Base URL",
id="auth-prompt-base-url",
)
base_url_input.display = self._advanced_visible
yield base_url_input
base_url_hint_widget = Static(
self._build_base_url_hint(),
classes="auth-prompt-meta",
id="auth-prompt-base-url-hint",
)
base_url_hint_widget.display = self._advanced_visible
yield base_url_hint_widget
if self._is_langsmith:
project_label = Static(
Content.from_markup("[bold]Project name[/bold]"),
classes="auth-prompt-meta",
id="auth-prompt-project-label",
)
project_label.display = self._advanced_visible
yield project_label
project_input = Input(
value=self._existing_project,
placeholder="LANGSMITH_PROJECT (default: deepagents-code)",
id="auth-prompt-project",
)
project_input.display = self._advanced_visible
yield project_input
project_hint_widget = Static(
Content.from_markup(
"Route agent traces to this LangSmith project. "
"Leave blank to use the default [bold]deepagents-code[/bold]."
),
classes="auth-prompt-meta",
id="auth-prompt-project-hint",
)
project_hint_widget.display = self._advanced_visible
yield project_hint_widget
else:
base_url_label = Static(
Content.from_markup("[bold]Base URL override[/bold]"),
classes="auth-prompt-meta",
id="auth-prompt-base-url-label",
)
base_url_label.display = self._advanced_visible
yield base_url_label
base_url_input = Input(
value=self._existing_base_url,
placeholder="Base URL",
id="auth-prompt-base-url",
)
base_url_input.display = self._advanced_visible
yield base_url_input
base_url_hint_widget = Static(
self._build_base_url_hint(),
classes="auth-prompt-meta",
id="auth-prompt-base-url-hint",
)
base_url_hint_widget.display = self._advanced_visible
yield base_url_hint_widget
yield Static("", classes="auth-prompt-error", id="auth-prompt-error")
save_label = "Enter replace" if self._has_existing else "Enter save"
help_parts = [f"{save_label} {glyphs.bullet} Esc cancel", "F2 advanced"]
@@ -824,6 +873,9 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
"#auth-prompt-base-url-label",
"#auth-prompt-base-url",
"#auth-prompt-base-url-hint",
"#auth-prompt-project-label",
"#auth-prompt-project",
"#auth-prompt-project-hint",
):
for widget in self.query(selector):
widget.display = self._advanced_visible
@@ -871,17 +923,26 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
"""Validate, persist, and dismiss.
Reads both fields regardless of which one was submitted, so pressing
Enter in either the key or the base-URL input saves the pair.
Enter in either the key or the secondary input (base URL, or the
LangSmith project name) saves the pair.
"""
event.stop()
cleaned = self.query_one("#auth-prompt-input", Input).value.strip()
base_url = self.query_one("#auth-prompt-base-url", Input).value.strip()
if self._is_langsmith:
base_url = ""
project = self.query_one("#auth-prompt-project", Input).value.strip()
else:
base_url = self.query_one("#auth-prompt-base-url", Input).value.strip()
project = ""
if not cleaned:
self._show_error("API key cannot be empty.")
return
try:
outcome = auth_store.set_stored_key(
self._provider, cleaned, base_url=base_url or None
self._provider,
cleaned,
base_url=base_url or None,
project=project or None,
)
except (ValueError, RuntimeError, OSError) as exc:
# `auth_store` exception messages never include the secret value,
@@ -897,6 +958,8 @@ class AuthPromptScreen(ModalScreen[AuthResult]):
# chmod failures are security regressions the user must see —
# `logger.warning` alone is invisible inside a Textual session.
self.app.notify(warning, severity="warning", markup=False)
if self._is_langsmith:
apply_stored_langsmith_auth(replace_project=True)
clear_caches()
self.dismiss(AuthResult.SAVED)
@@ -162,6 +162,64 @@ class TestSet:
assert auth_store.get_stored_key("openai") == "sk-new"
assert auth_store.get_stored_base_url("openai") == "https://gateway.example/v1"
def test_set_langsmith_with_project(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""`--project` stores a custom LangSmith project alongside the key."""
monkeypatch.setattr(sys, "stdin", io.StringIO("lsv2_test\n"))
code = run_auth_command(
_ns(
auth_command="set",
provider="langsmith",
from_env=None,
project="my-app",
)
)
assert code == 0
assert auth_store.get_stored_key("langsmith") == "lsv2_test"
assert auth_store.get_stored_project("langsmith") == "my-app"
def test_set_langsmith_preserves_existing_project(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Rotating the key without `--project` keeps the stored project."""
auth_store.set_stored_key("langsmith", "old", project="my-app")
monkeypatch.setattr(sys, "stdin", io.StringIO("new\n"))
code = run_auth_command(
_ns(auth_command="set", provider="langsmith", from_env=None, project=None)
)
assert code == 0
assert auth_store.get_stored_key("langsmith") == "new"
assert auth_store.get_stored_project("langsmith") == "my-app"
def test_set_langsmith_empty_project_clears_existing(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""An explicit empty `--project` clears a previously stored project."""
auth_store.set_stored_key("langsmith", "old", project="my-app")
monkeypatch.setattr(sys, "stdin", io.StringIO("new\n"))
code = run_auth_command(
_ns(auth_command="set", provider="langsmith", from_env=None, project="")
)
assert code == 0
assert auth_store.get_stored_key("langsmith") == "new"
assert auth_store.get_stored_project("langsmith") is None
def test_set_project_rejected_for_non_langsmith(
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
"""`--project` is only valid for the langsmith service."""
monkeypatch.setattr(sys, "stdin", io.StringIO("sk-ant\n"))
code = run_auth_command(
_ns(
auth_command="set",
provider="anthropic",
from_env=None,
project="my-app",
)
)
assert code == 1
assert auth_store.get_stored_key("anthropic") is None
assert "--project is only valid for langsmith" in capsys.readouterr().err
def test_set_from_unset_env_fails(
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
@@ -424,6 +482,15 @@ class TestStatus:
assert code == 0
assert "env: ANTHROPIC_API_KEY" in capsys.readouterr().out
def test_status_service_stored(self, capsys: pytest.CaptureFixture[str]) -> None:
"""A stored service (langsmith) resolves via the service status path."""
auth_store.set_stored_key("langsmith", "lsv2_test")
code = run_auth_command(_ns(auth_command="status", provider="langsmith"))
assert code == 0
out = capsys.readouterr().out
assert "langsmith" in out
assert "stored" in out
def test_status_env_uses_prefixed_override(
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
@@ -555,29 +622,39 @@ class TestList:
tmp_path / "missing.toml",
)
assert _known_providers() == (["openai", "openai_codex"], None)
from deepagents_code.model_config import SERVICE_API_KEY_ENV
expected = sorted({"openai", "openai_codex", *SERVICE_API_KEY_ENV})
assert _known_providers() == (expected, None)
code = run_auth_command(_ns(auth_command="list"))
assert code == 0
assert "openai_codex" in capsys.readouterr().out
@pytest.mark.usefixtures("clean_model_caches")
def test_list_empty_when_no_providers(
def test_list_shows_services_when_no_model_providers(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
"""With no installed, stored, or configured providers, say so plainly."""
"""Services (e.g. LangSmith tracing) are always listed, even with no models.
Mirrors the TUI `/auth` manager, where services are configurable
regardless of whether any model-provider package is installed.
"""
monkeypatch.setattr("deepagents_code.model_config.get_available_models", dict)
monkeypatch.setattr(
"deepagents_code.model_config.DEFAULT_CONFIG_PATH",
tmp_path / "missing.toml",
)
assert _known_providers() == ([], None)
from deepagents_code.model_config import SERVICE_API_KEY_ENV
assert _known_providers() == (sorted(SERVICE_API_KEY_ENV), None)
code = run_auth_command(_ns(auth_command="list"))
assert code == 0
assert "No providers found." in capsys.readouterr().out
assert "langsmith" in capsys.readouterr().out
def test_list_warns_when_store_corrupt(
self, capsys: pytest.CaptureFixture[str]
@@ -89,6 +89,67 @@ class TestRoundTrip:
assert auth_store.get_stored_base_url("openai") is None
assert auth_store.get_stored_key("openai") == "k"
def test_project_round_trips(self) -> None:
"""A stored project is persisted alongside the key."""
auth_store.set_stored_key("langsmith", "k", project=" my-app ")
assert auth_store.get_stored_key("langsmith") == "k"
assert auth_store.get_stored_project("langsmith") == "my-app"
def test_blank_project_not_stored(self) -> None:
"""A blank/omitted project stores nothing (use the default project)."""
auth_store.set_stored_key("langsmith", "k", project=" ")
assert auth_store.get_stored_project("langsmith") is None
auth_store.set_stored_key("langsmith", "k")
assert auth_store.get_stored_project("langsmith") is None
def test_resave_without_project_drops_it(self) -> None:
"""Replacing a credential without a project clears the prior one."""
auth_store.set_stored_key("langsmith", "k", project="my-app")
auth_store.set_stored_key("langsmith", "k2")
assert auth_store.get_stored_project("langsmith") is None
def test_project_rejected_for_non_langsmith(self) -> None:
"""A non-empty project may only pair with the langsmith service.
The invariant is enforced at the write boundary (not just in the CLI),
and nothing is persisted when it is violated.
"""
with pytest.raises(ValueError, match="langsmith"):
auth_store.set_stored_key("anthropic", "k", project="my-app")
assert auth_store.get_stored_key("anthropic") is None
def test_blank_project_allowed_for_non_langsmith(self) -> None:
"""A blank project is a no-op for any provider, so the key still stores."""
auth_store.set_stored_key("anthropic", "k", project=" ")
assert auth_store.get_stored_key("anthropic") == "k"
assert auth_store.get_stored_project("anthropic") is None
def test_malformed_project_is_dropped_and_logged(
self, fake_home: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A non-string project is dropped, the key survives, and it's logged."""
path = _auth_file(fake_home)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(
{
"version": 1,
"credentials": {
"langsmith": {
"type": "api_key",
"key": "k",
"added_at": "",
"project": 1234,
}
},
}
)
)
with caplog.at_level("WARNING", logger="deepagents_code.auth_store"):
assert auth_store.get_stored_project("langsmith") is None
assert auth_store.get_stored_key("langsmith") == "k"
assert any("malformed project" in r.getMessage() for r in caplog.records)
def test_malformed_base_url_is_dropped_and_logged(
self, fake_home: Path, caplog: pytest.LogCaptureFixture
) -> None:
@@ -140,15 +140,29 @@ class TestAuthPromptScreen:
def test_provider_metadata_maps_reference_known_providers(self) -> None:
"""Map keys stay in sync with real providers so none silently never resolve."""
known = set(model_config.PROVIDER_API_KEY_ENV) | {model_config.CODEX_PROVIDER}
# Services (e.g. LangSmith tracing) may carry a display name and key URL
# but live in SERVICE_API_KEY_ENV rather than PROVIDER_API_KEY_ENV.
known = (
set(model_config.PROVIDER_API_KEY_ENV)
| set(model_config.SERVICE_API_KEY_ENV)
| {model_config.CODEX_PROVIDER}
)
assert set(PROVIDER_DISPLAY_NAMES) <= known
# Codex uses ChatGPT login, not an API-key page, so it has no key URL.
# Services (e.g. Tavily) carry a key URL but live in SERVICE_API_KEY_ENV.
assert set(PROVIDER_API_KEY_URLS) <= (
set(model_config.PROVIDER_API_KEY_ENV)
| set(model_config.SERVICE_API_KEY_ENV)
)
def test_langsmith_service_has_display_name_and_key_url(self) -> None:
"""LangSmith tracing surfaces a branded label and a key-acquisition URL."""
assert PROVIDER_DISPLAY_NAMES[model_config.LANGSMITH_SERVICE] == (
"LangSmith (tracing)"
)
assert PROVIDER_API_KEY_URLS[model_config.LANGSMITH_SERVICE] == (
"https://smith.langchain.com/settings"
)
def test_every_known_provider_has_a_display_name(self) -> None:
"""A new provider can't ship without a branded `/auth` label.
@@ -516,6 +530,19 @@ api_key_env = "MY_GATEWAY_API_KEY"
assert base_url.display is True
assert base_url.value == "https://proxy.example/v1"
async def test_existing_project_expands_advanced_by_default(self) -> None:
"""A stored LangSmith project keeps the advanced section visible."""
auth_store.set_stored_key("langsmith", "lsv2_existing", project="my-app")
app = _AuthHostApp()
async with app.run_test() as pilot:
app.show_prompt("langsmith", "LANGSMITH_API_KEY")
await pilot.pause()
project_label = app.screen.query_one("#auth-prompt-project-label", Static)
project_input = app.screen.query_one("#auth-prompt-project", Input)
assert project_label.display is True
assert project_input.display is True
assert project_input.value == "my-app"
async def test_paste_and_submit_persists(self) -> None:
"""Submitting a non-empty value writes to the store and dismisses True."""
app = _AuthHostApp()
@@ -530,6 +557,35 @@ api_key_env = "MY_GATEWAY_API_KEY"
assert app.prompt_result is AuthResult.SAVED
assert auth_store.get_stored_key("anthropic") == "sk-ant-test-12345"
async def test_langsmith_submit_applies_tracing_env_immediately(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Saving LangSmith auth activates tracing in the running process."""
import os
for var in (
"LANGSMITH_API_KEY",
"LANGSMITH_TRACING",
"LANGCHAIN_TRACING_V2",
"LANGSMITH_PROJECT",
"DEEPAGENTS_CODE_LANGSMITH_API_KEY",
"DEEPAGENTS_CODE_LANGSMITH_TRACING",
"DEEPAGENTS_CODE_LANGSMITH_PROJECT",
):
monkeypatch.delenv(var, raising=False)
app = _AuthHostApp()
async with app.run_test() as pilot:
app.show_prompt("langsmith", "LANGSMITH_API_KEY")
await pilot.pause()
app.screen.query_one("#auth-prompt-input", Input).value = "lsv2_live"
await pilot.press("enter")
await pilot.pause()
assert app.prompt_result is AuthResult.SAVED
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_live"
assert os.environ["LANGSMITH_TRACING"] == "true"
async def test_base_url_round_trips_on_submit(self) -> None:
"""A base URL typed alongside the key is persisted as the pair."""
app = _AuthHostApp()
@@ -593,6 +649,34 @@ api_key_env = "MY_GATEWAY_API_KEY"
base_url_field = app.screen.query_one("#auth-prompt-base-url", Input)
assert base_url_field.value == "https://stored.example/v1"
async def test_langsmith_prompt_saves_key_and_project(self) -> None:
"""The LangSmith prompt persists a key plus its custom project name."""
app = _AuthHostApp()
async with app.run_test() as pilot:
app.show_prompt("langsmith", "LANGSMITH_API_KEY")
await pilot.pause()
await pilot.press("f2")
await pilot.pause()
app.screen.query_one("#auth-prompt-input", Input).value = "lsv2_test"
project_field = app.screen.query_one("#auth-prompt-project", Input)
project_field.value = "my-app"
project_field.focus()
await pilot.pause()
await pilot.press("enter")
await pilot.pause()
assert app.prompt_result is AuthResult.SAVED
assert auth_store.get_stored_key("langsmith") == "lsv2_test"
assert auth_store.get_stored_project("langsmith") == "my-app"
async def test_langsmith_prompt_has_no_base_url_field(self) -> None:
"""The LangSmith prompt swaps the base-URL field for the project field."""
app = _AuthHostApp()
async with app.run_test() as pilot:
app.show_prompt("langsmith", "LANGSMITH_API_KEY")
await pilot.pause()
assert not app.screen.query("#auth-prompt-base-url")
assert app.screen.query("#auth-prompt-project")
async def test_empty_submit_shows_error_and_does_not_dismiss(self) -> None:
"""Empty input renders an inline error instead of dismissing."""
app = _AuthHostApp()
@@ -1258,8 +1342,15 @@ api_key_env = "MY_GATEWAY_API_KEY"
ids = {
options.get_option_at_index(i).id for i in range(options.option_count)
}
# Non-model services (e.g. Tavily) are always listed for key entry.
assert ids == {"openai", "openai_codex", "anthropic", "tavily"}
# Non-model services (Tavily search, LangSmith tracing) are always
# listed for key entry.
assert ids == {
"openai",
"openai_codex",
"anthropic",
"tavily",
"langsmith",
}
async def test_selecting_service_opens_prompt_for_its_env_var(
self, monkeypatch: pytest.MonkeyPatch
+394 -44
View File
@@ -21,6 +21,7 @@ from deepagents_code.config import (
ModelResult,
Settings,
_apply_default_langsmith_project,
_apply_stored_langsmith_tracing,
_create_model_from_class,
_create_model_via_init,
_disable_orphaned_tracing,
@@ -29,6 +30,7 @@ from deepagents_code.config import (
_read_config_toml_retries,
_resolve_retry_kwargs,
_resolve_retry_param_name,
apply_stored_langsmith_auth,
build_langsmith_thread_url,
consume_orphaned_tracing_disabled_notice,
create_model,
@@ -129,11 +131,11 @@ class TestRuntimeDotenvReload:
tmp_path / "missing-global.env",
)
config_mod._dotenv_loaded_values.clear()
original_ls = config_mod._original_langsmith_project
original_ls = config_mod._bootstrap_state.original_langsmith_project
try:
# User never set LANGSMITH_PROJECT; tracing is active with a key.
config_mod._original_langsmith_project = None
config_mod._bootstrap_state.original_langsmith_project = None
monkeypatch.setenv("LANGSMITH_TRACING", "true")
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_test")
monkeypatch.delenv("LANGSMITH_PROJECT", raising=False)
@@ -148,7 +150,7 @@ class TestRuntimeDotenvReload:
assert os.environ["LANGSMITH_PROJECT"] == LANGSMITH_PROJECT_DEFAULT
finally:
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.original_langsmith_project = original_ls
config_mod._dotenv_loaded_values.clear()
@@ -2110,6 +2112,194 @@ class TestDisableOrphanedTracing:
assert os.environ["LANGSMITH_TRACING"] == "false"
class TestApplyStoredLangSmithTracing:
"""Tests for _apply_stored_langsmith_tracing()."""
@pytest.fixture
def fake_state_dir(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect the credential store into a temp directory."""
state = tmp_path / ".state"
monkeypatch.setattr("deepagents_code.model_config.DEFAULT_STATE_DIR", state)
return state
def test_noop_without_stored_key(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""No stored key leaves the environment untouched (no auto-enable)."""
import os
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
_apply_stored_langsmith_tracing()
assert "LANGSMITH_TRACING" not in os.environ
def test_enables_tracing_when_key_stored(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A stored LangSmith key turns tracing on by default."""
import os
from deepagents_code import auth_store
for var in ("LANGSMITH_TRACING", "LANGCHAIN_TRACING_V2"):
monkeypatch.delenv(var, raising=False)
auth_store.set_stored_key("langsmith", "lsv2_test")
_apply_stored_langsmith_tracing()
assert os.environ["LANGSMITH_TRACING"] == "true"
def test_respects_explicit_opt_out(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An explicit falsy tracing flag is honored as a temporary opt-out."""
import os
from deepagents_code import auth_store
monkeypatch.setenv("LANGSMITH_TRACING", "false")
auth_store.set_stored_key("langsmith", "lsv2_test")
_apply_stored_langsmith_tracing()
assert os.environ["LANGSMITH_TRACING"] == "false"
def test_scoped_opt_out_disables_sibling_tracing_flags(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The documented scoped opt-out wins over other truthy tracing flags."""
import os
from deepagents_code import auth_store
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_TRACING", "false")
monkeypatch.setenv("LANGSMITH_TRACING", "false")
monkeypatch.setenv("LANGCHAIN_TRACING_V2", "true")
auth_store.set_stored_key("langsmith", "lsv2_test")
_apply_stored_langsmith_tracing()
assert os.environ["LANGSMITH_TRACING"] == "false"
assert os.environ["LANGCHAIN_TRACING_V2"] == "false"
def test_leaves_explicit_enable_untouched(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An already-truthy tracing flag is left as-is."""
import os
from deepagents_code import auth_store
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
monkeypatch.setenv("LANGCHAIN_TRACING_V2", "true")
auth_store.set_stored_key("langsmith", "lsv2_test")
_apply_stored_langsmith_tracing()
assert os.environ["LANGCHAIN_TRACING_V2"] == "true"
assert "LANGSMITH_TRACING" not in os.environ
def test_applies_stored_custom_project(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A stored custom project is applied when none is already set."""
import os
from deepagents_code import auth_store
for var in ("LANGSMITH_TRACING", "LANGSMITH_PROJECT"):
monkeypatch.delenv(var, raising=False)
auth_store.set_stored_key("langsmith", "lsv2_test", project="my-app")
_apply_stored_langsmith_tracing()
assert os.environ["LANGSMITH_PROJECT"] == "my-app"
def test_stored_project_does_not_override_env(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""An explicit LANGSMITH_PROJECT wins over the stored project."""
import os
from deepagents_code import auth_store
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
monkeypatch.setenv("LANGSMITH_PROJECT", "from-env")
auth_store.set_stored_key("langsmith", "lsv2_test", project="my-app")
_apply_stored_langsmith_tracing()
assert os.environ["LANGSMITH_PROJECT"] == "from-env"
def test_replace_project_applies_stored_project_over_existing_env(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Immediate `/auth` save applies the latest stored project."""
import os
from deepagents_code import auth_store
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
monkeypatch.setenv("LANGSMITH_PROJECT", "old-project")
auth_store.set_stored_key("langsmith", "lsv2_test", project="my-app")
_apply_stored_langsmith_tracing(replace_project=True)
assert os.environ["LANGSMITH_PROJECT"] == "my-app"
def test_replace_project_clears_existing_env_when_project_removed(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Immediate `/auth` save clears the old project when the field is blank."""
import os
from deepagents_code import auth_store
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
monkeypatch.setenv("LANGSMITH_PROJECT", "old-project")
auth_store.set_stored_key("langsmith", "lsv2_test")
_apply_stored_langsmith_tracing(replace_project=True)
assert "LANGSMITH_PROJECT" not in os.environ
def test_immediate_auth_clear_restores_default_project(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Clearing `/auth` project updates the active traced session."""
import os
from deepagents_code import auth_store
from deepagents_code.config_manifest import LANGSMITH_PROJECT_DEFAULT
monkeypatch.setenv("LANGSMITH_PROJECT", "old-project")
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
auth_store.set_stored_key("langsmith", "lsv2_test")
apply_stored_langsmith_auth(replace_project=True)
assert os.environ["LANGSMITH_PROJECT"] == LANGSMITH_PROJECT_DEFAULT
assert os.environ["LANGSMITH_TRACING"] == "true"
def test_corrupt_store_warns_and_leaves_env_untouched(
self,
fake_state_dir: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""A corrupt credential file is logged and tracing is left untouched."""
import os
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
fake_state_dir.mkdir(parents=True, exist_ok=True)
(fake_state_dir / "auth.json").write_text("{ not json", encoding="utf-8")
with caplog.at_level("WARNING", logger="deepagents_code.config"):
_apply_stored_langsmith_tracing()
assert "LANGSMITH_TRACING" not in os.environ
assert any("may be corrupt" in r.getMessage() for r in caplog.records)
class TestGetTracingStatus:
"""Tests for get_tracing_status()."""
@@ -3929,8 +4119,8 @@ class TestLazyModuleAttributes:
from deepagents_code.config import _ensure_bootstrap
# Reset flag so bootstrap will re-enter
original = config_mod._bootstrap_done
config_mod._bootstrap_done = False
original = config_mod._bootstrap_state.done
config_mod._bootstrap_state.done = False
try:
with patch(
@@ -3939,9 +4129,9 @@ class TestLazyModuleAttributes:
_ensure_bootstrap() # should warn, not raise
# Flag must be set even after failure
assert config_mod._bootstrap_done is True
assert config_mod._bootstrap_state.done is True
finally:
config_mod._bootstrap_done = original
config_mod._bootstrap_state.done = original
def test_get_settings_returns_same_instance(self) -> None:
"""_get_settings caches in globals — two calls return the same object."""
@@ -3958,9 +4148,9 @@ class TestLazyModuleAttributes:
import deepagents_code.config as config_mod
from deepagents_code.config import _ensure_bootstrap
original_done = config_mod._bootstrap_done
original_ls = config_mod._original_langsmith_project
config_mod._bootstrap_done = False
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_PROJECT", "my-agent-project")
@@ -3975,13 +4165,13 @@ class TestLazyModuleAttributes:
):
_ensure_bootstrap()
assert config_mod._original_langsmith_project is None
assert config_mod._bootstrap_state.original_langsmith_project is None
import os
assert os.environ["LANGSMITH_PROJECT"] == "my-agent-project"
finally:
config_mod._bootstrap_done = original_done
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
def test_ensure_bootstrap_preserves_original_langsmith(
self, monkeypatch: pytest.MonkeyPatch
@@ -3990,9 +4180,9 @@ class TestLazyModuleAttributes:
import deepagents_code.config as config_mod
from deepagents_code.config import _ensure_bootstrap
original_done = config_mod._bootstrap_done
original_ls = config_mod._original_langsmith_project
config_mod._bootstrap_done = False
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("LANGSMITH_PROJECT", "user-project")
@@ -4007,13 +4197,15 @@ class TestLazyModuleAttributes:
):
_ensure_bootstrap()
assert config_mod._original_langsmith_project == "user-project"
assert (
config_mod._bootstrap_state.original_langsmith_project == "user-project"
)
import os
assert os.environ["LANGSMITH_PROJECT"] == "agent-project"
finally:
config_mod._bootstrap_done = original_done
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
def test_bootstrap_propagates_prefixed_langsmith_vars(
self, monkeypatch: pytest.MonkeyPatch
@@ -4022,9 +4214,9 @@ class TestLazyModuleAttributes:
import deepagents_code.config as config_mod
from deepagents_code.config import _ensure_bootstrap
original_done = config_mod._bootstrap_done
original_ls = config_mod._original_langsmith_project
config_mod._bootstrap_done = False
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_API_KEY", "lsv2_test")
@@ -4047,8 +4239,8 @@ class TestLazyModuleAttributes:
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_test"
assert os.environ["LANGSMITH_TRACING"] == "true"
finally:
config_mod._bootstrap_done = original_done
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
def test_bootstrap_overrides_canonical_with_prefixed_value(
self, monkeypatch: pytest.MonkeyPatch
@@ -4057,9 +4249,9 @@ class TestLazyModuleAttributes:
import deepagents_code.config as config_mod
from deepagents_code.config import _ensure_bootstrap
original_done = config_mod._bootstrap_done
original_ls = config_mod._original_langsmith_project
config_mod._bootstrap_done = False
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_original")
@@ -4080,8 +4272,8 @@ class TestLazyModuleAttributes:
# Prefixed value wins — canonical is overwritten.
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_override"
finally:
config_mod._bootstrap_done = original_done
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
def test_bootstrap_propagates_empty_string(
self, monkeypatch: pytest.MonkeyPatch
@@ -4090,9 +4282,9 @@ class TestLazyModuleAttributes:
import deepagents_code.config as config_mod
from deepagents_code.config import _ensure_bootstrap
original_done = config_mod._bootstrap_done
original_ls = config_mod._original_langsmith_project
config_mod._bootstrap_done = False
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_TRACING", "")
@@ -4113,8 +4305,8 @@ class TestLazyModuleAttributes:
# Empty string propagated — lets user explicitly disable tracing.
assert os.environ["LANGSMITH_TRACING"] == ""
finally:
config_mod._bootstrap_done = original_done
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
def test_bootstrap_defaults_project_when_tracing_and_key(
self, monkeypatch: pytest.MonkeyPatch
@@ -4131,9 +4323,9 @@ class TestLazyModuleAttributes:
from deepagents_code.config import _ensure_bootstrap
from deepagents_code.config_manifest import LANGSMITH_PROJECT_DEFAULT
original_done = config_mod._bootstrap_done
original_ls = config_mod._original_langsmith_project
config_mod._bootstrap_done = False
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("LANGSMITH_TRACING", "true")
@@ -4152,8 +4344,8 @@ class TestLazyModuleAttributes:
assert os.environ["LANGSMITH_PROJECT"] == LANGSMITH_PROJECT_DEFAULT
finally:
config_mod._bootstrap_done = original_done
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
def test_bootstrap_keyless_tracing_leaves_project_unset(
self, monkeypatch: pytest.MonkeyPatch
@@ -4171,9 +4363,9 @@ class TestLazyModuleAttributes:
import deepagents_code.config as config_mod
from deepagents_code.config import _ensure_bootstrap
original_done = config_mod._bootstrap_done
original_ls = config_mod._original_langsmith_project
config_mod._bootstrap_done = False
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("LANGSMITH_TRACING", "true")
@@ -4204,8 +4396,166 @@ class TestLazyModuleAttributes:
assert "LANGSMITH_PROJECT" not in os.environ
assert os.environ["LANGSMITH_TRACING"] == "false"
finally:
config_mod._bootstrap_done = original_done
config_mod._original_langsmith_project = original_ls
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
def test_bootstrap_stored_langsmith_key_keeps_tracing_enabled(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A `/auth`-stored LangSmith key survives `_disable_orphaned_tracing`.
End-to-end regression guard for the bootstrap ordering: a key stored via
`/auth` (never exported to the env) must be bridged onto
`LANGSMITH_API_KEY` and auto-enable tracing *before*
`_disable_orphaned_tracing` runs, so the orphan guard sees the key and
leaves tracing on. The helper-level `_apply_stored_langsmith_tracing`
tests cannot catch a regression that reorders the two bootstrap steps.
"""
import os
import deepagents_code.config as config_mod
from deepagents_code import auth_store
from deepagents_code.config import _ensure_bootstrap
monkeypatch.setattr(
"deepagents_code.model_config.DEFAULT_STATE_DIR", tmp_path / ".state"
)
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
original_tracing = dict(config_mod._bootstrap_state.original_tracing_env)
config_mod._bootstrap_state.done = False
try:
for var in (
"LANGSMITH_TRACING",
"LANGCHAIN_TRACING_V2",
"LANGSMITH_API_KEY",
"LANGCHAIN_API_KEY",
"LANGSMITH_PROJECT",
"DEEPAGENTS_CODE_LANGSMITH_TRACING",
"DEEPAGENTS_CODE_LANGSMITH_PROJECT",
):
monkeypatch.delenv(var, raising=False)
auth_store.set_stored_key("langsmith", "lsv2_stored")
with (
patch("deepagents_code.config._load_dotenv"),
patch(
"deepagents_code.project_utils.get_server_project_context",
return_value=None,
),
patch(
"deepagents_code.config._has_langsmith_profile_credentials",
return_value=False,
),
patch(
"deepagents_code.config._has_langsmith_profile_custom_endpoint",
return_value=False,
),
):
_ensure_bootstrap()
# The stored key was bridged onto the canonical env var, and tracing
# stayed on instead of being disabled as orphaned.
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_stored"
assert os.environ["LANGSMITH_TRACING"] == "true"
finally:
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
config_mod._bootstrap_state.original_tracing_env = original_tracing
def test_bootstrap_prefixed_langsmith_key_wins_over_stored_key(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A session-scoped LangSmith key remains authoritative at bootstrap."""
import os
import deepagents_code.config as config_mod
from deepagents_code import auth_store
from deepagents_code.config import _ensure_bootstrap
monkeypatch.setattr(
"deepagents_code.model_config.DEFAULT_STATE_DIR", tmp_path / ".state"
)
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
original_tracing = dict(config_mod._bootstrap_state.original_tracing_env)
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_API_KEY", "lsv2_prefixed")
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_TRACING", "true")
monkeypatch.delenv("LANGSMITH_API_KEY", raising=False)
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
monkeypatch.delenv("DEEPAGENTS_CODE_LANGSMITH_PROJECT", raising=False)
auth_store.set_stored_key("langsmith", "lsv2_stored")
with (
patch("deepagents_code.config._load_dotenv"),
patch(
"deepagents_code.project_utils.get_server_project_context",
return_value=None,
),
):
_ensure_bootstrap()
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_prefixed"
assert os.environ["LANGSMITH_TRACING"] == "true"
finally:
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
config_mod._bootstrap_state.original_tracing_env = original_tracing
def test_scoped_tracing_opt_out_restores_user_tracing_for_shell_env(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Deep Agents Code opt-out does not leak into child command envs."""
import os
import deepagents_code.config as config_mod
from deepagents_code import auth_store
from deepagents_code.config import _ensure_bootstrap, restore_user_tracing_env
monkeypatch.setattr(
"deepagents_code.model_config.DEFAULT_STATE_DIR", tmp_path / ".state"
)
original_done = config_mod._bootstrap_state.done
original_ls = config_mod._bootstrap_state.original_langsmith_project
original_tracing = dict(config_mod._bootstrap_state.original_tracing_env)
config_mod._bootstrap_state.done = False
try:
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_TRACING", "false")
monkeypatch.setenv("LANGCHAIN_TRACING_V2", "true")
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
monkeypatch.delenv("LANGSMITH_API_KEY", raising=False)
monkeypatch.delenv("DEEPAGENTS_CODE_LANGSMITH_PROJECT", raising=False)
auth_store.set_stored_key("langsmith", "lsv2_stored")
with (
patch("deepagents_code.config._load_dotenv"),
patch(
"deepagents_code.project_utils.get_server_project_context",
return_value=None,
),
):
_ensure_bootstrap()
assert os.environ["LANGSMITH_TRACING"] == "false"
assert os.environ["LANGCHAIN_TRACING_V2"] == "false"
shell_env = os.environ.copy()
restore_user_tracing_env(shell_env)
assert "LANGSMITH_TRACING" not in shell_env
assert shell_env["LANGCHAIN_TRACING_V2"] == "true"
finally:
config_mod._bootstrap_state.done = original_done
config_mod._bootstrap_state.original_langsmith_project = original_ls
config_mod._bootstrap_state.original_tracing_env = original_tracing
class TestApplyDefaultLangsmithProject:
@@ -432,8 +432,34 @@ class TestServiceCredentials:
from deepagents_code.model_config import is_service
assert is_service("tavily") is True
assert is_service("langsmith") is True
assert is_service("anthropic") is False
def test_langsmith_service_env_var(self) -> None:
"""LangSmith is registered as a service mapped to its API-key env var."""
from deepagents_code.model_config import (
LANGSMITH_SERVICE,
SERVICE_API_KEY_ENV,
)
assert SERVICE_API_KEY_ENV[LANGSMITH_SERVICE] == "LANGSMITH_API_KEY"
def test_apply_exports_stored_langsmith_key(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A stored LangSmith key is copied onto LANGSMITH_API_KEY."""
import os
from deepagents_code import auth_store
from deepagents_code.model_config import apply_stored_service_credentials
monkeypatch.delenv("LANGSMITH_API_KEY", raising=False)
auth_store.set_stored_key("langsmith", "lsv2_test")
apply_stored_service_credentials()
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_test"
def test_status_missing_when_unset(
self,
fake_state_dir: Path, # noqa: ARG002
@@ -520,6 +546,23 @@ class TestServiceCredentials:
apply_stored_service_credentials()
assert os.environ["TAVILY_API_KEY"] == "from-store"
def test_apply_stored_key_respects_prefixed_override(
self,
fake_state_dir: Path, # noqa: ARG002
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A scoped service key is not overwritten by a stored key."""
import os
from deepagents_code import auth_store
from deepagents_code.model_config import apply_stored_service_credentials
monkeypatch.setenv("DEEPAGENTS_CODE_LANGSMITH_API_KEY", "lsv2_prefixed")
monkeypatch.setenv("LANGSMITH_API_KEY", "lsv2_prefixed")
auth_store.set_stored_key("langsmith", "lsv2_stored")
apply_stored_service_credentials()
assert os.environ["LANGSMITH_API_KEY"] == "lsv2_prefixed"
class TestSplitCredentialSource:
"""`warn_on_split_credential_source` flags key/endpoint env-tier mismatches."""
@@ -58,11 +58,14 @@ def _run_cli_main(argv: list[str]) -> subprocess.CompletedProcess[str]:
name for name in sys.modules if name.startswith(prefixes)
)
config_module = sys.modules.get("deepagents_code.config")
bootstrap_done = (
getattr(config_module, "_bootstrap_done", None)
bootstrap_state = (
getattr(config_module, "_bootstrap_state", None)
if config_module is not None
else None
)
bootstrap_done = (
bootstrap_state.done if bootstrap_state is not None else None
)
print("LOADED_MODULES=" + json.dumps(loaded), file=sys.stderr)
print("BOOTSTRAP_DONE=" + json.dumps(bootstrap_done), file=sys.stderr)
"""
+3 -3
View File
@@ -95,13 +95,13 @@ def _make_banner(
# Temporarily clear the cached settings singleton so _get_settings()
# re-creates it from the patched env vars inside the context manager.
saved = _cfg.__dict__.pop("settings", None)
saved_bootstrap = _cfg._bootstrap_done
_cfg._bootstrap_done = False
saved_bootstrap = _cfg._bootstrap_state.done
_cfg._bootstrap_state.done = False
try:
with patch.dict("os.environ", env, clear=True):
return WelcomeBanner(thread_id=thread_id)
finally:
_cfg._bootstrap_done = saved_bootstrap
_cfg._bootstrap_state.done = saved_bootstrap
if saved is not None:
_cfg.__dict__["settings"] = saved
else: