zjcode 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents_code/__init__.py +42 -0
- deepagents_code/__main__.py +6 -0
- deepagents_code/_ask_user_types.py +90 -0
- deepagents_code/_cli_context.py +96 -0
- deepagents_code/_constants.py +41 -0
- deepagents_code/_debug.py +148 -0
- deepagents_code/_debug_buffer.py +204 -0
- deepagents_code/_env_vars.py +411 -0
- deepagents_code/_fake_models.py +66 -0
- deepagents_code/_git.py +521 -0
- deepagents_code/_paths.py +69 -0
- deepagents_code/_server_config.py +576 -0
- deepagents_code/_session_stats.py +235 -0
- deepagents_code/_startup_error.py +45 -0
- deepagents_code/_testing_models.py +305 -0
- deepagents_code/_textual_patches.py +420 -0
- deepagents_code/_tool_stream.py +694 -0
- deepagents_code/_version.py +46 -0
- deepagents_code/agent.py +1976 -0
- deepagents_code/app.py +19239 -0
- deepagents_code/app.tcss +438 -0
- deepagents_code/approval_mode.py +131 -0
- deepagents_code/ask_user.py +306 -0
- deepagents_code/auth_display.py +185 -0
- deepagents_code/auth_store.py +545 -0
- deepagents_code/built_in_skills/__init__.py +5 -0
- deepagents_code/built_in_skills/remember/SKILL.md +118 -0
- deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
- deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
- deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
- deepagents_code/client/__init__.py +1 -0
- deepagents_code/client/commands/__init__.py +1 -0
- deepagents_code/client/commands/auth.py +541 -0
- deepagents_code/client/commands/config.py +714 -0
- deepagents_code/client/commands/mcp.py +250 -0
- deepagents_code/client/commands/tools.py +416 -0
- deepagents_code/client/launch/__init__.py +1 -0
- deepagents_code/client/launch/server.py +978 -0
- deepagents_code/client/launch/server_manager.py +540 -0
- deepagents_code/client/non_interactive.py +1758 -0
- deepagents_code/client/remote_client.py +794 -0
- deepagents_code/clipboard.py +217 -0
- deepagents_code/command_registry.py +450 -0
- deepagents_code/config.py +4829 -0
- deepagents_code/config_manifest.py +1451 -0
- deepagents_code/configurable_model.py +577 -0
- deepagents_code/default_agent_prompt.md +12 -0
- deepagents_code/doctor.py +559 -0
- deepagents_code/editor.py +142 -0
- deepagents_code/event_bus.py +411 -0
- deepagents_code/extras_info.py +661 -0
- deepagents_code/file_ops.py +576 -0
- deepagents_code/formatting.py +135 -0
- deepagents_code/goal_rubric.py +497 -0
- deepagents_code/goal_tools.py +459 -0
- deepagents_code/hooks.py +348 -0
- deepagents_code/input.py +1041 -0
- deepagents_code/integrations/__init__.py +1 -0
- deepagents_code/integrations/openai_codex.py +551 -0
- deepagents_code/integrations/sandbox_config.py +198 -0
- deepagents_code/integrations/sandbox_factory.py +1124 -0
- deepagents_code/integrations/sandbox_provider.py +137 -0
- deepagents_code/integrations/sandbox_registry.py +350 -0
- deepagents_code/iterm_cursor_guide.py +176 -0
- deepagents_code/local_context.py +926 -0
- deepagents_code/main.py +3985 -0
- deepagents_code/managed_tools.py +642 -0
- deepagents_code/mcp_auth.py +1950 -0
- deepagents_code/mcp_config.py +176 -0
- deepagents_code/mcp_disabled.py +212 -0
- deepagents_code/mcp_login_service.py +281 -0
- deepagents_code/mcp_oauth_ui.py +199 -0
- deepagents_code/mcp_providers/__init__.py +23 -0
- deepagents_code/mcp_providers/_registry.py +39 -0
- deepagents_code/mcp_providers/base.py +133 -0
- deepagents_code/mcp_providers/github.py +102 -0
- deepagents_code/mcp_providers/slack.py +175 -0
- deepagents_code/mcp_tools.py +2427 -0
- deepagents_code/mcp_trust.py +207 -0
- deepagents_code/media_utils.py +826 -0
- deepagents_code/memory_guard.py +474 -0
- deepagents_code/model_config.py +4156 -0
- deepagents_code/notifications.py +247 -0
- deepagents_code/offload.py +402 -0
- deepagents_code/onboarding.py +223 -0
- deepagents_code/output.py +69 -0
- deepagents_code/paste_collapse.py +103 -0
- deepagents_code/project_utils.py +231 -0
- deepagents_code/py.typed +0 -0
- deepagents_code/reasoning_effort.py +641 -0
- deepagents_code/reliable_rubric.py +97 -0
- deepagents_code/resume_state.py +237 -0
- deepagents_code/server_graph.py +310 -0
- deepagents_code/session_end_summary.py +329 -0
- deepagents_code/sessions.py +1716 -0
- deepagents_code/skills/__init__.py +18 -0
- deepagents_code/skills/commands.py +1252 -0
- deepagents_code/skills/invocation.py +112 -0
- deepagents_code/skills/load.py +196 -0
- deepagents_code/skills/trust.py +547 -0
- deepagents_code/state_migration.py +136 -0
- deepagents_code/subagents.py +278 -0
- deepagents_code/system_prompt.md +204 -0
- deepagents_code/terminal_capabilities.py +115 -0
- deepagents_code/terminal_escape.py +287 -0
- deepagents_code/theme.py +891 -0
- deepagents_code/todo_list_prompt.md +12 -0
- deepagents_code/tool_catalog.py +509 -0
- deepagents_code/tool_display.py +367 -0
- deepagents_code/tools.py +516 -0
- deepagents_code/tui/__init__.py +1 -0
- deepagents_code/tui/textual_adapter.py +2553 -0
- deepagents_code/tui/widgets/__init__.py +9 -0
- deepagents_code/tui/widgets/_js_eval_display.py +139 -0
- deepagents_code/tui/widgets/_links.py +261 -0
- deepagents_code/tui/widgets/agent_selector.py +420 -0
- deepagents_code/tui/widgets/approval.py +602 -0
- deepagents_code/tui/widgets/ask_user.py +515 -0
- deepagents_code/tui/widgets/auth.py +1997 -0
- deepagents_code/tui/widgets/autocomplete.py +890 -0
- deepagents_code/tui/widgets/chat_input.py +3181 -0
- deepagents_code/tui/widgets/codex_auth.py +452 -0
- deepagents_code/tui/widgets/cwd_switch.py +242 -0
- deepagents_code/tui/widgets/debug_console.py +868 -0
- deepagents_code/tui/widgets/diff.py +252 -0
- deepagents_code/tui/widgets/effort_selector.py +189 -0
- deepagents_code/tui/widgets/goal_review.py +390 -0
- deepagents_code/tui/widgets/goal_status.py +50 -0
- deepagents_code/tui/widgets/history.py +194 -0
- deepagents_code/tui/widgets/install_confirm.py +248 -0
- deepagents_code/tui/widgets/launch_init.py +482 -0
- deepagents_code/tui/widgets/loading.py +227 -0
- deepagents_code/tui/widgets/mcp_login.py +539 -0
- deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
- deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
- deepagents_code/tui/widgets/message_store.py +999 -0
- deepagents_code/tui/widgets/messages.py +3846 -0
- deepagents_code/tui/widgets/model_selector.py +2343 -0
- deepagents_code/tui/widgets/notification_center.py +456 -0
- deepagents_code/tui/widgets/notification_detail.py +257 -0
- deepagents_code/tui/widgets/notification_settings.py +170 -0
- deepagents_code/tui/widgets/restart_prompt.py +158 -0
- deepagents_code/tui/widgets/skill_trust.py +131 -0
- deepagents_code/tui/widgets/startup_tip.py +91 -0
- deepagents_code/tui/widgets/status.py +781 -0
- deepagents_code/tui/widgets/subagent_panel.py +969 -0
- deepagents_code/tui/widgets/theme_selector.py +401 -0
- deepagents_code/tui/widgets/thread_selector.py +2564 -0
- deepagents_code/tui/widgets/tool_renderers.py +190 -0
- deepagents_code/tui/widgets/tool_widgets.py +304 -0
- deepagents_code/tui/widgets/update_available.py +311 -0
- deepagents_code/tui/widgets/update_confirm.py +184 -0
- deepagents_code/tui/widgets/update_progress.py +327 -0
- deepagents_code/tui/widgets/welcome.py +508 -0
- deepagents_code/turn_end_summary.py +522 -0
- deepagents_code/ui.py +858 -0
- deepagents_code/unicode_security.py +563 -0
- deepagents_code/update_check.py +3124 -0
- zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
- zjcode-0.0.1.dist-info/METADATA +220 -0
- zjcode-0.0.1.dist-info/RECORD +163 -0
- zjcode-0.0.1.dist-info/WHEEL +4 -0
- zjcode-0.0.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,1451 @@
|
|
|
1
|
+
"""Canonical manifest and resolver for every user-tunable scalar config option.
|
|
2
|
+
|
|
3
|
+
This module is the single source of truth for the configuration *surface*: the
|
|
4
|
+
set of options, their types, typed defaults, env-var names, and `config.toml`
|
|
5
|
+
locations. The typed defaults for config-file-only options (notably the
|
|
6
|
+
`[interpreter]` section) live here as module constants, and `Settings` derives
|
|
7
|
+
its dataclass defaults from them — so a default is defined in exactly one place.
|
|
8
|
+
|
|
9
|
+
`resolve_scalar` is the shared resolution engine used both by the runtime
|
|
10
|
+
(`Settings.from_environment`) and by the `config` CLI command, so introspection
|
|
11
|
+
can never drift from what the app actually reads. Resolution precedence mirrors
|
|
12
|
+
the loaders: a `DEEPAGENTS_CODE_`-prefixed env var beats the canonical name,
|
|
13
|
+
env beats `config.toml`, and the typed default is the final fallback. A
|
|
14
|
+
malformed numeric/list/PTC value, an unrecognized boolean token, or a
|
|
15
|
+
wrong-typed TOML value is logged and falls back to the next layer rather than
|
|
16
|
+
raising, so a bad config never blocks startup.
|
|
17
|
+
|
|
18
|
+
Structured, user-defined config is *not* a flat scalar option and is parsed by
|
|
19
|
+
dedicated typed loaders elsewhere. The manifest references `[threads].columns`
|
|
20
|
+
and `[warnings].suppress` as `STRUCTURED` options for discovery; other tables
|
|
21
|
+
such as `[models.providers.*]` and `[themes.*]` are handled entirely by their
|
|
22
|
+
own loaders and the manifest does not enumerate them at all.
|
|
23
|
+
|
|
24
|
+
Import discipline: the module top level stays stdlib + `_env_vars` only (both
|
|
25
|
+
light) so it is safe to import from `config.py` at class-definition time without
|
|
26
|
+
pulling the heavy `model_config`/agent runtime onto the startup fast path.
|
|
27
|
+
Anything needing `model_config` (provider credentials, the config path, env-var
|
|
28
|
+
prefix resolution) is imported lazily inside functions.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import logging
|
|
34
|
+
import os
|
|
35
|
+
from dataclasses import dataclass
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from functools import lru_cache
|
|
38
|
+
from typing import TYPE_CHECKING, Any, Literal, assert_never, cast, get_args
|
|
39
|
+
|
|
40
|
+
from deepagents_code import _env_vars
|
|
41
|
+
from deepagents_code._env_vars import classify_env_bool
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from collections.abc import Iterable
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# --- Canonical typed defaults ----------------------------------------------
|
|
50
|
+
# These are single sources of truth for defaults shared across the manifest and
|
|
51
|
+
# their runtime consumers.
|
|
52
|
+
|
|
53
|
+
INTERPRETER_ENABLE_DEFAULT = True
|
|
54
|
+
INTERPRETER_TIMEOUT_SECONDS_DEFAULT = 5.0
|
|
55
|
+
INTERPRETER_MEMORY_LIMIT_MB_DEFAULT = 64
|
|
56
|
+
INTERPRETER_MAX_PTC_CALLS_DEFAULT = 256
|
|
57
|
+
INTERPRETER_MAX_RESULT_CHARS_DEFAULT = 4000
|
|
58
|
+
INTERPRETER_PTC_DEFAULT: str | bool | list[str] = "safe"
|
|
59
|
+
INTERPRETER_PTC_ACKNOWLEDGE_UNSAFE_DEFAULT = False
|
|
60
|
+
|
|
61
|
+
LANGSMITH_PROJECT_DEFAULT = "deepagents-code"
|
|
62
|
+
"""Project agent traces fall back to when no project env var is set.
|
|
63
|
+
|
|
64
|
+
Single source of truth shared by the `tracing.langsmith_project` option and
|
|
65
|
+
`config.get_langsmith_project_name`."""
|
|
66
|
+
|
|
67
|
+
CursorStyle = Literal["block", "underline"]
|
|
68
|
+
"""Visual style for the chat input cursor (a block cell or an underline)."""
|
|
69
|
+
|
|
70
|
+
CURSOR_STYLE_DEFAULT: CursorStyle = "block"
|
|
71
|
+
VALID_CURSOR_STYLES: frozenset[str] = frozenset(get_args(CursorStyle))
|
|
72
|
+
"""Allowlist derived from `CursorStyle` so the two never drift."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class OptionKind(Enum):
|
|
76
|
+
"""How an option's raw env/TOML value is coerced to a typed value.
|
|
77
|
+
|
|
78
|
+
All kinds flow through `resolve_scalar`. The scalar kinds (`BOOL`,
|
|
79
|
+
`BOOL_PRESENCE`, `INT`, `FLOAT`, `STR`) are coerced inline by
|
|
80
|
+
`_coerce_env`/`_coerce_toml`. `LOG_LEVEL_DELEGATE`, `SHELL_LIST_DELEGATE`,
|
|
81
|
+
`SKILLS_DIRS_DELEGATE`, `PTC_DELEGATE`, and `STARTUP_MODE_DELEGATE` defer to
|
|
82
|
+
bespoke parsers (their semantics — dynamic debug fallback, colon-split Path
|
|
83
|
+
resolution, comma + `recommended`/`all` sentinels, and the PTC/startup-mode
|
|
84
|
+
allowlists — do not compress into a generic coercion). `THEME_DELEGATE` is
|
|
85
|
+
resolved separately at the top of `resolve_scalar` and never reaches the
|
|
86
|
+
inline coercers. `STRUCTURED` marks user-defined tables that the scalar
|
|
87
|
+
resolver only passes through for display.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
BOOL = "bool"
|
|
91
|
+
"""Recognized truthy (`1`/`true`/`yes`/`on`) or falsy (`0`/`false`/`no`/`off`)
|
|
92
|
+
tokens; an unrecognized value is logged and skipped to the next layer."""
|
|
93
|
+
|
|
94
|
+
BOOL_PRESENCE = "bool_presence"
|
|
95
|
+
"""Any non-empty env value enables the flag (e.g. debug injectors)."""
|
|
96
|
+
|
|
97
|
+
INT = "int"
|
|
98
|
+
|
|
99
|
+
FLOAT = "float"
|
|
100
|
+
|
|
101
|
+
STR = "str"
|
|
102
|
+
|
|
103
|
+
LOG_LEVEL_DELEGATE = "log_level"
|
|
104
|
+
"""Validates log levels and resolves the default from debug mode."""
|
|
105
|
+
|
|
106
|
+
SHELL_LIST_DELEGATE = "shell_list"
|
|
107
|
+
"""Delegates to `config.parse_shell_allow_list`."""
|
|
108
|
+
|
|
109
|
+
SKILLS_DIRS_DELEGATE = "skills_dirs"
|
|
110
|
+
"""Delegates to `config._parse_extra_skills_dirs`."""
|
|
111
|
+
|
|
112
|
+
PTC_DELEGATE = "ptc"
|
|
113
|
+
"""Delegates to `config._parse_interpreter_ptc`."""
|
|
114
|
+
|
|
115
|
+
CURSOR_STYLE_DELEGATE = "cursor_style"
|
|
116
|
+
"""Validates the `[ui].cursor_style` display allowlist."""
|
|
117
|
+
|
|
118
|
+
STARTUP_MODE_DELEGATE = "startup_mode"
|
|
119
|
+
"""Delegates to the `[startup].mode` runtime allowlist."""
|
|
120
|
+
|
|
121
|
+
THEME_DELEGATE = "theme"
|
|
122
|
+
"""Delegates to the app theme-preference loader semantics."""
|
|
123
|
+
|
|
124
|
+
STRUCTURED = "structured"
|
|
125
|
+
"""User-defined table parsed by a dedicated loader; not scalar-coerced."""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
_KIND_TYPE_LABEL: dict[OptionKind, str] = {
|
|
129
|
+
OptionKind.BOOL: "bool",
|
|
130
|
+
OptionKind.BOOL_PRESENCE: "bool",
|
|
131
|
+
OptionKind.INT: "int",
|
|
132
|
+
OptionKind.FLOAT: "float",
|
|
133
|
+
OptionKind.STR: "str",
|
|
134
|
+
OptionKind.LOG_LEVEL_DELEGATE: "str",
|
|
135
|
+
OptionKind.SHELL_LIST_DELEGATE: "list[str]",
|
|
136
|
+
OptionKind.SKILLS_DIRS_DELEGATE: "list[path]",
|
|
137
|
+
OptionKind.PTC_DELEGATE: "str | list[str]",
|
|
138
|
+
OptionKind.CURSOR_STYLE_DELEGATE: "str",
|
|
139
|
+
OptionKind.STARTUP_MODE_DELEGATE: "str",
|
|
140
|
+
OptionKind.THEME_DELEGATE: "theme",
|
|
141
|
+
OptionKind.STRUCTURED: "table",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if _KIND_TYPE_LABEL.keys() != set(OptionKind):
|
|
145
|
+
# Fail at import (and in the test suite) rather than KeyError-ing from
|
|
146
|
+
# `ConfigOption.type` only when an unlabeled kind happens to be rendered.
|
|
147
|
+
msg = "_KIND_TYPE_LABEL is missing an OptionKind entry"
|
|
148
|
+
raise RuntimeError(msg)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Python types accepted for a `ConfigOption.default` of each scalar kind,
|
|
152
|
+
# enforced by `ConfigOption.__post_init__`. Delegate kinds accept their parser's
|
|
153
|
+
# output shape and are validated by those parsers, so they are omitted here.
|
|
154
|
+
_KIND_DEFAULT_TYPES: dict[OptionKind, tuple[type, ...]] = {
|
|
155
|
+
OptionKind.BOOL: (bool,),
|
|
156
|
+
OptionKind.BOOL_PRESENCE: (bool,),
|
|
157
|
+
OptionKind.INT: (int,),
|
|
158
|
+
OptionKind.FLOAT: (int, float),
|
|
159
|
+
OptionKind.STR: (str,),
|
|
160
|
+
OptionKind.CURSOR_STYLE_DELEGATE: (str,),
|
|
161
|
+
OptionKind.STARTUP_MODE_DELEGATE: (str,),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True)
|
|
166
|
+
class ConfigOption:
|
|
167
|
+
"""One user-tunable configuration option and where it can be set."""
|
|
168
|
+
|
|
169
|
+
key: str
|
|
170
|
+
"""Canonical dotted identifier used by `config get`.
|
|
171
|
+
|
|
172
|
+
Also used as the stable display key.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
group: str
|
|
176
|
+
"""Human-readable grouping for `config list` and `config show`."""
|
|
177
|
+
|
|
178
|
+
summary: str
|
|
179
|
+
"""One-line description of what the option controls."""
|
|
180
|
+
|
|
181
|
+
kind: OptionKind
|
|
182
|
+
"""How env/TOML values are coerced to a typed value."""
|
|
183
|
+
|
|
184
|
+
default: Any = None
|
|
185
|
+
"""Typed default value, or `None` when there is no static default."""
|
|
186
|
+
|
|
187
|
+
env_var: str | None = None
|
|
188
|
+
"""Primary environment variable name the loader reads, or `None`.
|
|
189
|
+
|
|
190
|
+
For provider credentials this is the canonical name; the
|
|
191
|
+
`DEEPAGENTS_CODE_` prefix override is applied dynamically at resolution time.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
fallback_env_vars: tuple[str, ...] = ()
|
|
195
|
+
"""Secondary env vars read (in order) when `env_var` is unset.
|
|
196
|
+
|
|
197
|
+
Read literally — no `DEEPAGENTS_CODE_` prefix logic — so `config show`/`get`
|
|
198
|
+
mirror runtime fallbacks such as `get_langsmith_project_name` reading bare
|
|
199
|
+
`LANGSMITH_PROJECT`.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
toml_keys: tuple[str, ...] | None = None
|
|
203
|
+
"""Section/key path within `config.toml`, or `None`."""
|
|
204
|
+
|
|
205
|
+
invert_toml_bool: bool = False
|
|
206
|
+
"""Whether a TOML bool should be negated after validation."""
|
|
207
|
+
|
|
208
|
+
cli_flag: str | None = None
|
|
209
|
+
"""Representative CLI flag that sets the option, or `None`."""
|
|
210
|
+
|
|
211
|
+
redacted: bool = False
|
|
212
|
+
"""Whether `config show` reports only set/not-set, never the raw value.
|
|
213
|
+
|
|
214
|
+
Named `redacted` rather than `secret` so the value (and the JSON field it
|
|
215
|
+
populates) carries no credential-suggesting identifier — the flag is
|
|
216
|
+
boolean metadata, and a `secret`-named value tripped CodeQL's clear-text
|
|
217
|
+
logging heuristic when written to stdout.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
settings_field: str | None = None
|
|
221
|
+
"""Name of the `Settings` attribute this option backs, or `None`.
|
|
222
|
+
|
|
223
|
+
`None` means the option is read elsewhere inline or is descriptive.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
dependency_module: str | None = None
|
|
227
|
+
"""Import module required to use the option, or `None`.
|
|
228
|
+
|
|
229
|
+
`None` means the option is always available or descriptive only.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
install_extra: str | None = None
|
|
233
|
+
"""Optional `deepagents-code[...]` extra that provides `dependency_module`."""
|
|
234
|
+
|
|
235
|
+
provider: str | None = None
|
|
236
|
+
"""Provider/service name a credential option authenticates, or `None`.
|
|
237
|
+
|
|
238
|
+
Set only for `Credentials`-group options (e.g. `"anthropic"`, `"tavily"`),
|
|
239
|
+
where it is the key `/auth` stores the credential under and the name passed
|
|
240
|
+
to `model_config.is_service`. Carrying it as a structured field lets
|
|
241
|
+
`config show`/`get` look up the stored credential without re-parsing it out
|
|
242
|
+
of `key`. `None` for every other option.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
empty_env_is_false: bool = False
|
|
246
|
+
"""Whether an explicitly present empty env value disables a bool option."""
|
|
247
|
+
|
|
248
|
+
def __post_init__(self) -> None:
|
|
249
|
+
"""Reject a `default` that contradicts `kind` at construction time.
|
|
250
|
+
|
|
251
|
+
The manifest is a hand-edited literal table with `default: Any`, so a
|
|
252
|
+
mistyped default (an `INT` option defaulting to a `str`) or a mutable
|
|
253
|
+
one would otherwise slip through to runtime — a wrong-typed default
|
|
254
|
+
feeds `Settings` unchecked, and a mutable default is shared by reference
|
|
255
|
+
through the `get_config_options` `lru_cache` and returned verbatim by
|
|
256
|
+
`resolve_scalar`. Catching it here fails the import (and the test suite).
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
TypeError: When `fallback_env_vars` is not a tuple of non-empty
|
|
260
|
+
strings, `empty_env_is_false` is set on a non-bool option,
|
|
261
|
+
`default` is mutable, a `STRUCTURED` option declares a default,
|
|
262
|
+
or a scalar option's default has the wrong type.
|
|
263
|
+
"""
|
|
264
|
+
# Guard `fallback_env_vars` independently of `default` (which has its own
|
|
265
|
+
# early-return path below): like `default`, it is shared by reference
|
|
266
|
+
# through the `get_config_options` `lru_cache`, so a mutable value (a
|
|
267
|
+
# `list`) would reintroduce the aliasing hazard the default guard exists
|
|
268
|
+
# to prevent. Empty names never match any env var, so reject those too.
|
|
269
|
+
if not isinstance(self.fallback_env_vars, tuple) or any(
|
|
270
|
+
not isinstance(name, str) or not name for name in self.fallback_env_vars
|
|
271
|
+
):
|
|
272
|
+
msg = (
|
|
273
|
+
f"{self.key}: fallback_env_vars must be a tuple of non-empty "
|
|
274
|
+
f"strings, got {self.fallback_env_vars!r}"
|
|
275
|
+
)
|
|
276
|
+
raise TypeError(msg)
|
|
277
|
+
if self.empty_env_is_false and self.kind is not OptionKind.BOOL:
|
|
278
|
+
msg = f"{self.key}: empty_env_is_false requires a bool option kind"
|
|
279
|
+
raise TypeError(msg)
|
|
280
|
+
|
|
281
|
+
default = self.default
|
|
282
|
+
if default is None:
|
|
283
|
+
if self.invert_toml_bool:
|
|
284
|
+
self._validate_invert_toml_bool()
|
|
285
|
+
return
|
|
286
|
+
if isinstance(default, (list, dict, set)):
|
|
287
|
+
msg = (
|
|
288
|
+
f"{self.key}: mutable default {default!r} is unsafe under the "
|
|
289
|
+
"shared lru_cache; use an immutable value (e.g. a tuple)"
|
|
290
|
+
)
|
|
291
|
+
raise TypeError(msg)
|
|
292
|
+
if self.kind is OptionKind.STRUCTURED:
|
|
293
|
+
msg = f"{self.key}: STRUCTURED options must not declare a default"
|
|
294
|
+
raise TypeError(msg)
|
|
295
|
+
if self.invert_toml_bool:
|
|
296
|
+
self._validate_invert_toml_bool()
|
|
297
|
+
expected = _KIND_DEFAULT_TYPES.get(self.kind)
|
|
298
|
+
if expected is None:
|
|
299
|
+
# Delegate kinds validate their own (immutable) default shapes.
|
|
300
|
+
return
|
|
301
|
+
# `bool` is an `int` subclass; an INT/FLOAT default must not be a bool.
|
|
302
|
+
if not isinstance(default, expected) or (
|
|
303
|
+
self.kind in {OptionKind.INT, OptionKind.FLOAT}
|
|
304
|
+
and isinstance(default, bool)
|
|
305
|
+
):
|
|
306
|
+
msg = (
|
|
307
|
+
f"{self.key}: default {default!r} is not valid for kind "
|
|
308
|
+
f"{self.kind.value}"
|
|
309
|
+
)
|
|
310
|
+
raise TypeError(msg)
|
|
311
|
+
|
|
312
|
+
def _validate_invert_toml_bool(self) -> None:
|
|
313
|
+
"""Validate the inverted TOML bool marker is only used where coherent.
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
TypeError: When the marker is used without a boolean TOML source.
|
|
317
|
+
"""
|
|
318
|
+
if self.kind not in {OptionKind.BOOL, OptionKind.BOOL_PRESENCE}:
|
|
319
|
+
msg = f"{self.key}: invert_toml_bool requires a boolean option kind"
|
|
320
|
+
raise TypeError(msg)
|
|
321
|
+
if self.toml_keys is None:
|
|
322
|
+
msg = f"{self.key}: invert_toml_bool requires toml_keys"
|
|
323
|
+
raise TypeError(msg)
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def type(self) -> str:
|
|
327
|
+
"""Human-readable type label derived from `kind`."""
|
|
328
|
+
return _KIND_TYPE_LABEL[self.kind]
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def toml_path(self) -> str | None:
|
|
332
|
+
"""Render `toml_keys` as a `[section].key` display string."""
|
|
333
|
+
if not self.toml_keys:
|
|
334
|
+
return None
|
|
335
|
+
*sections, leaf = self.toml_keys
|
|
336
|
+
if not sections:
|
|
337
|
+
return leaf
|
|
338
|
+
return f"[{'.'.join(sections)}].{leaf}"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# --- Resolution -------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
_INVALID = object()
|
|
344
|
+
"""Sentinel: a raw value failed coercion and the next layer should be tried."""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def load_config_toml() -> dict[str, Any]:
|
|
348
|
+
"""Load `~/.deepagents/config.toml`.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
The parsed config mapping, or `{}` when the file is absent or invalid.
|
|
352
|
+
"""
|
|
353
|
+
import tomllib
|
|
354
|
+
|
|
355
|
+
from deepagents_code.model_config import DEFAULT_CONFIG_PATH
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
with DEFAULT_CONFIG_PATH.open("rb") as f:
|
|
359
|
+
return tomllib.load(f)
|
|
360
|
+
except FileNotFoundError:
|
|
361
|
+
return {}
|
|
362
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
363
|
+
# `exc_info=True` preserves the TOML line/column (or permission cause):
|
|
364
|
+
# a corrupt file makes every option fall back to its default, so the
|
|
365
|
+
# log must say *why*, not just that the read failed.
|
|
366
|
+
logger.warning(
|
|
367
|
+
"Could not read config from %s; using defaults for all options",
|
|
368
|
+
DEFAULT_CONFIG_PATH,
|
|
369
|
+
exc_info=True,
|
|
370
|
+
)
|
|
371
|
+
return {}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _toml_lookup(data: dict[str, Any], keys: tuple[str, ...]) -> tuple[bool, Any]:
|
|
375
|
+
"""Navigate nested `keys` in `data`.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
`(found, value)`, where `found` is `False` if any key was missing.
|
|
379
|
+
"""
|
|
380
|
+
node: Any = data
|
|
381
|
+
for key in keys:
|
|
382
|
+
if not isinstance(node, dict) or key not in node:
|
|
383
|
+
return False, None
|
|
384
|
+
node = node[key]
|
|
385
|
+
return True, node
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _coerce_env(option: ConfigOption, raw: str, name: str) -> object:
|
|
389
|
+
"""Coerce a raw environment-variable string by the option's kind.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
The typed value, or `_INVALID` when the raw value cannot be coerced.
|
|
393
|
+
"""
|
|
394
|
+
kind = option.kind
|
|
395
|
+
if kind is OptionKind.BOOL:
|
|
396
|
+
classified = classify_env_bool(raw)
|
|
397
|
+
if classified is None:
|
|
398
|
+
# Unrecognized boolean token: log and fall through like every other
|
|
399
|
+
# malformed scalar, so `config show` reports the real source
|
|
400
|
+
# (config.toml/default) instead of crediting the env var with a
|
|
401
|
+
# value it did not actually supply.
|
|
402
|
+
logger.warning("Ignoring %s=%r (expected bool)", name, raw)
|
|
403
|
+
return _INVALID
|
|
404
|
+
return classified
|
|
405
|
+
if kind is OptionKind.BOOL_PRESENCE:
|
|
406
|
+
return bool(raw)
|
|
407
|
+
if kind is OptionKind.STR:
|
|
408
|
+
return raw
|
|
409
|
+
if kind is OptionKind.LOG_LEVEL_DELEGATE:
|
|
410
|
+
from deepagents_code._debug import LOG_LEVELS
|
|
411
|
+
|
|
412
|
+
level = raw.strip().upper()
|
|
413
|
+
if level in LOG_LEVELS:
|
|
414
|
+
return level
|
|
415
|
+
valid = ", ".join(LOG_LEVELS)
|
|
416
|
+
logger.warning("Ignoring %s=%r (expected one of %s)", name, raw, valid)
|
|
417
|
+
return _INVALID
|
|
418
|
+
if kind is OptionKind.INT:
|
|
419
|
+
try:
|
|
420
|
+
return int(raw.strip())
|
|
421
|
+
except ValueError:
|
|
422
|
+
logger.warning("Ignoring %s=%r (expected int)", name, raw)
|
|
423
|
+
return _INVALID
|
|
424
|
+
if kind is OptionKind.FLOAT:
|
|
425
|
+
try:
|
|
426
|
+
return float(raw.strip())
|
|
427
|
+
except ValueError:
|
|
428
|
+
logger.warning("Ignoring %s=%r (expected number)", name, raw)
|
|
429
|
+
return _INVALID
|
|
430
|
+
if kind is OptionKind.SHELL_LIST_DELEGATE:
|
|
431
|
+
from deepagents_code.config import parse_shell_allow_list
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
return parse_shell_allow_list(raw)
|
|
435
|
+
except ValueError:
|
|
436
|
+
logger.warning("Ignoring invalid %s", name)
|
|
437
|
+
return _INVALID
|
|
438
|
+
if kind is OptionKind.SKILLS_DIRS_DELEGATE:
|
|
439
|
+
from deepagents_code.config import _parse_extra_skills_dirs
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
return _parse_extra_skills_dirs(raw, None)
|
|
443
|
+
except (ValueError, RuntimeError):
|
|
444
|
+
# `Path.expanduser()` raises on an unresolvable `~user`, `.resolve()`
|
|
445
|
+
# on a NUL byte; fall back rather than crash resolution/startup.
|
|
446
|
+
logger.warning("Ignoring %s (could not resolve a path)", name)
|
|
447
|
+
return _INVALID
|
|
448
|
+
if kind is OptionKind.THEME_DELEGATE:
|
|
449
|
+
# Resolved upstream in `resolve_scalar` and never reaches here; the raw
|
|
450
|
+
# passthrough is a defensive fallback only.
|
|
451
|
+
return raw
|
|
452
|
+
if kind is OptionKind.CURSOR_STYLE_DELEGATE:
|
|
453
|
+
if raw in VALID_CURSOR_STYLES:
|
|
454
|
+
return raw
|
|
455
|
+
logger.warning(
|
|
456
|
+
"Ignoring %s=%r (expected 'block' or 'underline')",
|
|
457
|
+
name,
|
|
458
|
+
raw,
|
|
459
|
+
)
|
|
460
|
+
return _INVALID
|
|
461
|
+
if kind is OptionKind.PTC_DELEGATE or kind is OptionKind.STRUCTURED:
|
|
462
|
+
# Neither kind declares an `env_var`, so the `if option.env_var` guard in
|
|
463
|
+
# `resolve_scalar` means this is unreachable today. If a future option
|
|
464
|
+
# ever adds an env var for one of these, return `_INVALID` rather than
|
|
465
|
+
# the raw string: passing an uncoerced value into a typed `Settings`
|
|
466
|
+
# field (e.g. `interpreter_ptc`) would bypass the delegate parser's
|
|
467
|
+
# validation. Falling back to the validated default is the safe choice.
|
|
468
|
+
logger.warning("%s is not env-backed; ignoring %s=%r", option.key, name, raw)
|
|
469
|
+
return _INVALID
|
|
470
|
+
if kind is OptionKind.STARTUP_MODE_DELEGATE:
|
|
471
|
+
from deepagents_code.model_config import VALID_STARTUP_MODES
|
|
472
|
+
|
|
473
|
+
if raw in VALID_STARTUP_MODES:
|
|
474
|
+
return raw
|
|
475
|
+
logger.warning(
|
|
476
|
+
"Ignoring %s=%r (expected 'manual' or 'dangerously-auto')",
|
|
477
|
+
name,
|
|
478
|
+
raw,
|
|
479
|
+
)
|
|
480
|
+
return _INVALID
|
|
481
|
+
assert_never(kind)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _coerce_toml(option: ConfigOption, raw: object) -> object:
|
|
485
|
+
"""Coerce a raw TOML value by the option's kind, logging on mismatch.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
The typed value, or `_INVALID` when the raw value has the wrong shape.
|
|
489
|
+
"""
|
|
490
|
+
kind = option.kind
|
|
491
|
+
label = option.toml_path or option.key
|
|
492
|
+
|
|
493
|
+
if kind in {OptionKind.BOOL, OptionKind.BOOL_PRESENCE}:
|
|
494
|
+
if isinstance(raw, bool):
|
|
495
|
+
return not raw if option.invert_toml_bool else raw
|
|
496
|
+
elif kind is OptionKind.INT:
|
|
497
|
+
if isinstance(raw, int) and not isinstance(raw, bool):
|
|
498
|
+
return raw
|
|
499
|
+
elif kind is OptionKind.FLOAT:
|
|
500
|
+
if isinstance(raw, (int, float)) and not isinstance(raw, bool):
|
|
501
|
+
return float(raw)
|
|
502
|
+
elif kind is OptionKind.STR:
|
|
503
|
+
if isinstance(raw, str):
|
|
504
|
+
return raw
|
|
505
|
+
elif kind is OptionKind.SKILLS_DIRS_DELEGATE:
|
|
506
|
+
if isinstance(raw, list):
|
|
507
|
+
from deepagents_code.config import _parse_extra_skills_dirs
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
# `raw` is a TOML list of unknown element type; the callee
|
|
511
|
+
# guards each entry with `isinstance(p, str)`.
|
|
512
|
+
return _parse_extra_skills_dirs(None, cast("list[str]", raw))
|
|
513
|
+
except (ValueError, RuntimeError):
|
|
514
|
+
# Unresolvable `~user` / NUL byte in a path string: fall back
|
|
515
|
+
# rather than crash resolution.
|
|
516
|
+
logger.warning(
|
|
517
|
+
"Ignoring %s in config.toml (could not resolve a path)", label
|
|
518
|
+
)
|
|
519
|
+
return _INVALID
|
|
520
|
+
elif kind is OptionKind.PTC_DELEGATE:
|
|
521
|
+
from deepagents_code.config import _parse_interpreter_ptc
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
return _parse_interpreter_ptc(raw)
|
|
525
|
+
except ValueError as exc:
|
|
526
|
+
logger.warning("Ignoring %s in config.toml: %s", label, exc)
|
|
527
|
+
return _INVALID
|
|
528
|
+
elif kind is OptionKind.CURSOR_STYLE_DELEGATE:
|
|
529
|
+
if isinstance(raw, str) and raw in VALID_CURSOR_STYLES:
|
|
530
|
+
return raw
|
|
531
|
+
logger.warning(
|
|
532
|
+
"Ignoring %s=%r in config.toml (expected 'block' or 'underline')",
|
|
533
|
+
label,
|
|
534
|
+
raw,
|
|
535
|
+
)
|
|
536
|
+
return _INVALID
|
|
537
|
+
elif kind is OptionKind.STARTUP_MODE_DELEGATE:
|
|
538
|
+
from deepagents_code.model_config import VALID_STARTUP_MODES
|
|
539
|
+
|
|
540
|
+
if isinstance(raw, str) and raw in VALID_STARTUP_MODES:
|
|
541
|
+
return raw
|
|
542
|
+
logger.warning(
|
|
543
|
+
"Ignoring %s=%r in config.toml (expected 'manual' or 'dangerously-auto')",
|
|
544
|
+
label,
|
|
545
|
+
raw,
|
|
546
|
+
)
|
|
547
|
+
return _INVALID
|
|
548
|
+
elif kind is OptionKind.STRUCTURED:
|
|
549
|
+
# Passed through verbatim for display; parsed by a dedicated loader.
|
|
550
|
+
return raw
|
|
551
|
+
elif kind is OptionKind.SHELL_LIST_DELEGATE:
|
|
552
|
+
# Env-only; never read from TOML, so passed through untouched.
|
|
553
|
+
return raw
|
|
554
|
+
# Any other (future) kind falls through to the warning below, so a missing
|
|
555
|
+
# branch logs and falls back rather than passing a raw value through.
|
|
556
|
+
|
|
557
|
+
logger.warning(
|
|
558
|
+
"Ignoring %s=%r in config.toml (expected %s)", label, raw, option.type
|
|
559
|
+
)
|
|
560
|
+
return _INVALID
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _resolve_theme(toml_data: dict[str, Any]) -> tuple[str, str]:
|
|
564
|
+
"""Resolve the active theme using the same precedence as app startup.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
`(theme_name, source)` for the effective Textual theme.
|
|
568
|
+
"""
|
|
569
|
+
from deepagents_code import theme
|
|
570
|
+
from deepagents_code._env_vars import THEME
|
|
571
|
+
from deepagents_code.app import _resolve_terminal_mapping, _resolve_theme_name
|
|
572
|
+
|
|
573
|
+
env_name = os.environ.get(THEME)
|
|
574
|
+
if env_name is not None:
|
|
575
|
+
resolved = _resolve_theme_name(env_name)
|
|
576
|
+
if resolved is not None:
|
|
577
|
+
return resolved, f"env ({THEME})"
|
|
578
|
+
logger.warning(
|
|
579
|
+
"Unknown theme '%s' in %s; falling back to default",
|
|
580
|
+
env_name,
|
|
581
|
+
THEME,
|
|
582
|
+
)
|
|
583
|
+
return theme.DEFAULT_THEME, "default"
|
|
584
|
+
|
|
585
|
+
ui = toml_data.get("ui", {})
|
|
586
|
+
if not isinstance(ui, dict):
|
|
587
|
+
if ui is not None:
|
|
588
|
+
logger.warning(
|
|
589
|
+
"[ui] should be a table; got %s while resolving theme",
|
|
590
|
+
type(ui).__name__,
|
|
591
|
+
)
|
|
592
|
+
return theme.DEFAULT_THEME, "default"
|
|
593
|
+
|
|
594
|
+
resolved = _resolve_terminal_mapping(ui)
|
|
595
|
+
if resolved is not None:
|
|
596
|
+
term_program = os.environ.get("TERM_PROGRAM", "").strip()
|
|
597
|
+
return resolved, f"config.toml [ui.terminal_themes.{term_program}]"
|
|
598
|
+
|
|
599
|
+
saved = ui.get("theme")
|
|
600
|
+
resolved = _resolve_theme_name(saved)
|
|
601
|
+
if resolved is not None:
|
|
602
|
+
return resolved, "config.toml [ui.theme]"
|
|
603
|
+
if isinstance(saved, str):
|
|
604
|
+
logger.warning("Unknown theme '%s' in config; falling back to default", saved)
|
|
605
|
+
|
|
606
|
+
return theme.DEFAULT_THEME, "default"
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def resolve_scalar(
|
|
610
|
+
option: ConfigOption, *, toml_data: dict[str, Any]
|
|
611
|
+
) -> tuple[Any, str]:
|
|
612
|
+
"""Resolve an option against the environment then `config.toml`.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
option: The option to resolve.
|
|
616
|
+
toml_data: Parsed `config.toml` mapping (see `load_config_toml`).
|
|
617
|
+
|
|
618
|
+
Resolution order is: the prefixed primary `env_var`, then each
|
|
619
|
+
`fallback_env_vars` name in declaration order, then `config.toml`, then the
|
|
620
|
+
typed `default`.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
`(value, source)`, where `source` is `env (<name>)`, `config.toml`, or
|
|
624
|
+
`default`. A malformed `int`/`float`/list/PTC value, an unrecognized
|
|
625
|
+
boolean token, or any TOML value of the wrong type is logged and skipped
|
|
626
|
+
so the next layer (or the typed default) applies. An empty env value is
|
|
627
|
+
normally treated as unset (mirroring `resolve_env_var`), so it falls
|
|
628
|
+
through to the next env var, then `config.toml`/`default`, rather than
|
|
629
|
+
counting as set. Options declaring `empty_env_is_false` instead resolve
|
|
630
|
+
an explicitly present empty value to `False`. Theme resolution
|
|
631
|
+
(`THEME_DELEGATE`) reports its own richer `config.toml [ui.*]` sources.
|
|
632
|
+
"""
|
|
633
|
+
if option.kind is OptionKind.THEME_DELEGATE:
|
|
634
|
+
return _resolve_theme(toml_data)
|
|
635
|
+
|
|
636
|
+
if option.env_var or option.fallback_env_vars:
|
|
637
|
+
from deepagents_code.model_config import resolved_env_var_name
|
|
638
|
+
|
|
639
|
+
names: list[str] = []
|
|
640
|
+
if option.env_var:
|
|
641
|
+
names.append(resolved_env_var_name(option.env_var))
|
|
642
|
+
names.extend(option.fallback_env_vars)
|
|
643
|
+
# An empty string normally counts as unset, matching `resolve_env_var`,
|
|
644
|
+
# so it is skipped and the loop continues to the next name. Options
|
|
645
|
+
# with an explicitly documented empty-value opt-out declare
|
|
646
|
+
# `empty_env_is_false`. Names are tried in order, so the primary
|
|
647
|
+
# `env_var` wins over any fallback.
|
|
648
|
+
for name in names:
|
|
649
|
+
raw = os.environ.get(name)
|
|
650
|
+
if raw is None:
|
|
651
|
+
continue
|
|
652
|
+
if not raw:
|
|
653
|
+
if option.empty_env_is_false:
|
|
654
|
+
return False, f"env ({name})"
|
|
655
|
+
continue
|
|
656
|
+
value = _coerce_env(option, raw, name)
|
|
657
|
+
if value is not _INVALID:
|
|
658
|
+
return value, f"env ({name})"
|
|
659
|
+
|
|
660
|
+
if option.toml_keys:
|
|
661
|
+
found, raw = _toml_lookup(toml_data, option.toml_keys)
|
|
662
|
+
if found:
|
|
663
|
+
value = _coerce_toml(option, raw)
|
|
664
|
+
if value is not _INVALID:
|
|
665
|
+
return value, "config.toml"
|
|
666
|
+
|
|
667
|
+
if option.kind is OptionKind.LOG_LEVEL_DELEGATE:
|
|
668
|
+
from deepagents_code._env_vars import DEBUG, is_env_truthy
|
|
669
|
+
|
|
670
|
+
return ("DEBUG" if is_env_truthy(DEBUG) else "INFO"), "default"
|
|
671
|
+
|
|
672
|
+
return option.default, "default"
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def resolve_interpreter_kwargs(
|
|
676
|
+
*, toml_data: dict[str, Any] | None = None
|
|
677
|
+
) -> dict[str, Any]:
|
|
678
|
+
"""Resolve the `[interpreter]` options into `Settings` constructor kwargs.
|
|
679
|
+
|
|
680
|
+
Only the interpreter group is resolved through the manifest. Credentials,
|
|
681
|
+
the shell allow-list, and the LangSmith project keep their dedicated
|
|
682
|
+
loaders in `config.py` (their empty-string-to-`None` and reload semantics
|
|
683
|
+
do not fit the generic resolver), so this stays scoped to the section whose
|
|
684
|
+
defaults this module owns.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
toml_data: Parsed `config.toml`; loaded automatically when omitted.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
Mapping of `Settings` field name to resolved value for the interpreter
|
|
691
|
+
section, suitable for splatting into `Settings(...)`.
|
|
692
|
+
"""
|
|
693
|
+
data = load_config_toml() if toml_data is None else toml_data
|
|
694
|
+
resolved: dict[str, Any] = {}
|
|
695
|
+
for option in get_config_options():
|
|
696
|
+
if option.group != "Interpreter" or option.settings_field is None:
|
|
697
|
+
continue
|
|
698
|
+
value, _ = resolve_scalar(option, toml_data=data)
|
|
699
|
+
resolved[option.settings_field] = value
|
|
700
|
+
return resolved
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# --- Option definitions -----------------------------------------------------
|
|
704
|
+
|
|
705
|
+
# Search credentials that are not provider API keys live outside
|
|
706
|
+
# `PROVIDER_API_KEY_ENV`, so they are declared explicitly.
|
|
707
|
+
_EXTRA_CREDENTIAL_ENV: dict[str, str] = {
|
|
708
|
+
"tavily": "TAVILY_API_KEY",
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
_SECRET_NAME_MARKERS = ("KEY", "TOKEN", "SECRET", "PASSWORD", "APIKEY")
|
|
712
|
+
|
|
713
|
+
_PROVIDER_DEPENDENCIES: dict[str, tuple[str, str]] = {
|
|
714
|
+
"anthropic": ("langchain_anthropic", "anthropic"),
|
|
715
|
+
"azure_openai": ("langchain_openai", "openai"),
|
|
716
|
+
"baseten": ("langchain_baseten", "baseten"),
|
|
717
|
+
"bedrock": ("langchain_aws", "bedrock"),
|
|
718
|
+
"cohere": ("langchain_cohere", "cohere"),
|
|
719
|
+
"deepseek": ("langchain_deepseek", "deepseek"),
|
|
720
|
+
"fireworks": ("langchain_fireworks", "fireworks"),
|
|
721
|
+
"google_genai": ("langchain_google_genai", "google-genai"),
|
|
722
|
+
"google_vertexai": ("langchain_google_vertexai", "vertex"),
|
|
723
|
+
"groq": ("langchain_groq", "groq"),
|
|
724
|
+
"huggingface": ("langchain_huggingface", "huggingface"),
|
|
725
|
+
"ibm": ("langchain_ibm", "ibm"),
|
|
726
|
+
"litellm": ("langchain_litellm", "litellm"),
|
|
727
|
+
"meta": ("langchain_meta", "meta"),
|
|
728
|
+
"mistralai": ("langchain_mistralai", "mistralai"),
|
|
729
|
+
"nvidia": ("langchain_nvidia_ai_endpoints", "nvidia"),
|
|
730
|
+
"ollama": ("langchain_ollama", "ollama"),
|
|
731
|
+
"openai": ("langchain_openai", "openai"),
|
|
732
|
+
"openrouter": ("langchain_openrouter", "openrouter"),
|
|
733
|
+
"perplexity": ("langchain_perplexity", "perplexity"),
|
|
734
|
+
"together": ("langchain_together", "together"),
|
|
735
|
+
"xai": ("langchain_xai", "xai"),
|
|
736
|
+
}
|
|
737
|
+
"""Provider integration import modules and the extras that install them."""
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def provider_install_extra(provider: str) -> str | None:
|
|
741
|
+
"""Return the `deepagents-code` extra that installs `provider`, if known.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
provider: Provider name (e.g. `"baseten"`, `"google_genai"`).
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
The extra name (e.g. `"baseten"`, `"google-genai"`), or `None` when the
|
|
748
|
+
provider has no curated extra (custom `class_path` providers,
|
|
749
|
+
ambient-auth providers, etc.).
|
|
750
|
+
"""
|
|
751
|
+
dependency = _PROVIDER_DEPENDENCIES.get(provider)
|
|
752
|
+
return dependency[1] if dependency else None
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def is_provider_package_installed(provider: str) -> bool:
|
|
756
|
+
"""Return whether `provider`'s integration package is importable.
|
|
757
|
+
|
|
758
|
+
Providers without a curated extra (no `_PROVIDER_DEPENDENCIES` entry) are
|
|
759
|
+
reported as installed — they manage their own dependencies, so the app
|
|
760
|
+
should never prompt to install an extra for them.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
provider: Provider name (e.g. `"baseten"`).
|
|
764
|
+
|
|
765
|
+
Returns:
|
|
766
|
+
`True` when the integration package is importable or the provider has
|
|
767
|
+
no curated extra; `False` when the curated package is missing or
|
|
768
|
+
cannot be resolved.
|
|
769
|
+
"""
|
|
770
|
+
import importlib.util
|
|
771
|
+
|
|
772
|
+
dependency = _PROVIDER_DEPENDENCIES.get(provider)
|
|
773
|
+
if dependency is None:
|
|
774
|
+
return True
|
|
775
|
+
try:
|
|
776
|
+
return importlib.util.find_spec(dependency[0]) is not None
|
|
777
|
+
except (ImportError, ValueError):
|
|
778
|
+
# `find_spec` re-raises errors from a broken parent package and raises
|
|
779
|
+
# `ValueError` for a malformed spec. Treat "can't tell" as "missing"
|
|
780
|
+
# so the model selector routes to the install prompt rather than
|
|
781
|
+
# crashing a synchronous Textual handler.
|
|
782
|
+
logger.warning(
|
|
783
|
+
"Could not resolve provider package %r; treating as not installed",
|
|
784
|
+
dependency[0],
|
|
785
|
+
exc_info=True,
|
|
786
|
+
)
|
|
787
|
+
return False
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
# Credentials that back a `Settings` field, keyed by canonical env var.
|
|
791
|
+
_CREDENTIAL_SETTINGS_FIELD: dict[str, str] = {
|
|
792
|
+
"OPENAI_API_KEY": "openai_api_key",
|
|
793
|
+
"ANTHROPIC_API_KEY": "anthropic_api_key",
|
|
794
|
+
"GOOGLE_API_KEY": "google_api_key",
|
|
795
|
+
"NVIDIA_API_KEY": "nvidia_api_key",
|
|
796
|
+
"TAVILY_API_KEY": "tavily_api_key",
|
|
797
|
+
"GOOGLE_CLOUD_PROJECT": "google_cloud_project",
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _is_secret_env(name: str) -> bool:
|
|
802
|
+
"""Return whether a credential env var name carries secret material."""
|
|
803
|
+
return any(marker in name for marker in _SECRET_NAME_MARKERS)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _credential_options() -> tuple[ConfigOption, ...]:
|
|
807
|
+
"""Build credential options from the canonical provider/key registries.
|
|
808
|
+
|
|
809
|
+
Generating these from `PROVIDER_API_KEY_ENV` (rather than hand-listing
|
|
810
|
+
them) guarantees every provider the app knows how to authenticate has a
|
|
811
|
+
manifest entry, so new providers can never silently miss the config
|
|
812
|
+
surface.
|
|
813
|
+
|
|
814
|
+
Returns:
|
|
815
|
+
One credential `ConfigOption` per known provider/key env var.
|
|
816
|
+
"""
|
|
817
|
+
from deepagents_code.model_config import PROVIDER_API_KEY_ENV
|
|
818
|
+
|
|
819
|
+
options: list[ConfigOption] = []
|
|
820
|
+
seen: set[str] = set()
|
|
821
|
+
sources = {**PROVIDER_API_KEY_ENV, **_EXTRA_CREDENTIAL_ENV}
|
|
822
|
+
for name, env_var in sorted(sources.items()):
|
|
823
|
+
if env_var in seen:
|
|
824
|
+
continue
|
|
825
|
+
seen.add(env_var)
|
|
826
|
+
redacted = _is_secret_env(env_var)
|
|
827
|
+
summary = (
|
|
828
|
+
f"Credential for the {name} provider."
|
|
829
|
+
if redacted
|
|
830
|
+
else f"Project/identifier for the {name} provider."
|
|
831
|
+
)
|
|
832
|
+
dependency = _PROVIDER_DEPENDENCIES.get(name)
|
|
833
|
+
options.append(
|
|
834
|
+
ConfigOption(
|
|
835
|
+
key=f"credentials.{name}",
|
|
836
|
+
group="Credentials",
|
|
837
|
+
summary=summary,
|
|
838
|
+
kind=OptionKind.STR,
|
|
839
|
+
env_var=env_var,
|
|
840
|
+
redacted=redacted,
|
|
841
|
+
provider=name,
|
|
842
|
+
settings_field=_CREDENTIAL_SETTINGS_FIELD.get(env_var),
|
|
843
|
+
dependency_module=dependency[0] if dependency else None,
|
|
844
|
+
install_extra=dependency[1] if dependency else None,
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
return tuple(options)
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# Options with a static (non-credential) definition, grouped by domain. The
|
|
851
|
+
# drift test asserts every `DEEPAGENTS_CODE_*` constant in `_env_vars` appears
|
|
852
|
+
# here (or in `NON_OPTION_ENV_VARS`).
|
|
853
|
+
_STATIC_OPTIONS: tuple[ConfigOption, ...] = (
|
|
854
|
+
# --- Display / UI ---------------------------------------------------
|
|
855
|
+
ConfigOption(
|
|
856
|
+
key="display.charset",
|
|
857
|
+
group="Display",
|
|
858
|
+
summary="Glyph set for the TUI ('unicode', 'ascii', or 'auto').",
|
|
859
|
+
kind=OptionKind.STR,
|
|
860
|
+
default="auto",
|
|
861
|
+
env_var="UI_CHARSET_MODE",
|
|
862
|
+
),
|
|
863
|
+
ConfigOption(
|
|
864
|
+
key="display.theme",
|
|
865
|
+
group="Display",
|
|
866
|
+
summary="Active CLI theme from env, terminal mapping, or saved preference.",
|
|
867
|
+
kind=OptionKind.THEME_DELEGATE,
|
|
868
|
+
env_var=_env_vars.THEME,
|
|
869
|
+
toml_keys=("ui", "theme"),
|
|
870
|
+
),
|
|
871
|
+
ConfigOption(
|
|
872
|
+
key="display.cursor_style",
|
|
873
|
+
group="Display",
|
|
874
|
+
summary="Chat input cursor style ('block' or 'underline').",
|
|
875
|
+
kind=OptionKind.CURSOR_STYLE_DELEGATE,
|
|
876
|
+
default=CURSOR_STYLE_DEFAULT,
|
|
877
|
+
env_var=_env_vars.CURSOR_STYLE,
|
|
878
|
+
toml_keys=("ui", "cursor_style"),
|
|
879
|
+
),
|
|
880
|
+
ConfigOption(
|
|
881
|
+
key="display.show_header",
|
|
882
|
+
group="Display",
|
|
883
|
+
summary="Show Textual's native header bar at the top of the TUI.",
|
|
884
|
+
kind=OptionKind.BOOL,
|
|
885
|
+
default=False,
|
|
886
|
+
env_var=_env_vars.SHOW_HEADER,
|
|
887
|
+
),
|
|
888
|
+
ConfigOption(
|
|
889
|
+
key="display.splash_show_model",
|
|
890
|
+
group="Display",
|
|
891
|
+
summary="Show the active model row in the startup welcome banner.",
|
|
892
|
+
kind=OptionKind.BOOL,
|
|
893
|
+
default=False,
|
|
894
|
+
env_var=_env_vars.SPLASH_SHOW_MODEL,
|
|
895
|
+
),
|
|
896
|
+
ConfigOption(
|
|
897
|
+
key="display.splash_show_cwd",
|
|
898
|
+
group="Display",
|
|
899
|
+
summary="Show the working-directory row in the startup welcome banner.",
|
|
900
|
+
kind=OptionKind.BOOL,
|
|
901
|
+
default=False,
|
|
902
|
+
env_var=_env_vars.SPLASH_SHOW_CWD,
|
|
903
|
+
),
|
|
904
|
+
ConfigOption(
|
|
905
|
+
key="display.kitty_keyboard",
|
|
906
|
+
group="Display",
|
|
907
|
+
summary="Override kitty-keyboard detection (1 forces on, 0 forces off).",
|
|
908
|
+
kind=OptionKind.BOOL,
|
|
909
|
+
env_var=_env_vars.KITTY_KEYBOARD,
|
|
910
|
+
),
|
|
911
|
+
ConfigOption(
|
|
912
|
+
key="display.show_scrollbar",
|
|
913
|
+
group="Display",
|
|
914
|
+
summary="Show the vertical scrollbar in the chat area (off by default).",
|
|
915
|
+
kind=OptionKind.BOOL,
|
|
916
|
+
default=False,
|
|
917
|
+
env_var=_env_vars.SHOW_SCROLLBAR,
|
|
918
|
+
toml_keys=("ui", "show_scrollbar"),
|
|
919
|
+
),
|
|
920
|
+
ConfigOption(
|
|
921
|
+
key="display.collapse_pastes",
|
|
922
|
+
group="Display",
|
|
923
|
+
summary="Collapse large chat-input pastes into compact placeholders.",
|
|
924
|
+
kind=OptionKind.BOOL,
|
|
925
|
+
default=True,
|
|
926
|
+
env_var=_env_vars.COLLAPSE_PASTES,
|
|
927
|
+
toml_keys=("ui", "collapse_pastes"),
|
|
928
|
+
),
|
|
929
|
+
ConfigOption(
|
|
930
|
+
key="display.hide_cwd",
|
|
931
|
+
group="Display",
|
|
932
|
+
summary="Hide local path displays in the footer and startup splash.",
|
|
933
|
+
kind=OptionKind.BOOL,
|
|
934
|
+
default=False,
|
|
935
|
+
env_var=_env_vars.HIDE_CWD,
|
|
936
|
+
),
|
|
937
|
+
ConfigOption(
|
|
938
|
+
key="display.hide_git_branch",
|
|
939
|
+
group="Display",
|
|
940
|
+
summary="Hide the current git branch in the TUI footer.",
|
|
941
|
+
kind=OptionKind.BOOL,
|
|
942
|
+
default=False,
|
|
943
|
+
env_var=_env_vars.HIDE_GIT_BRANCH,
|
|
944
|
+
),
|
|
945
|
+
ConfigOption(
|
|
946
|
+
key="display.hide_langsmith_tracing",
|
|
947
|
+
group="Display",
|
|
948
|
+
summary="Hide LangSmith tracing info in the startup splash.",
|
|
949
|
+
kind=OptionKind.BOOL,
|
|
950
|
+
default=False,
|
|
951
|
+
env_var=_env_vars.HIDE_LANGSMITH_TRACING,
|
|
952
|
+
),
|
|
953
|
+
ConfigOption(
|
|
954
|
+
key="display.show_langsmith_replica_tracing",
|
|
955
|
+
group="Display",
|
|
956
|
+
summary="Show LangSmith replica project info in the startup splash.",
|
|
957
|
+
kind=OptionKind.BOOL,
|
|
958
|
+
default=True,
|
|
959
|
+
env_var=_env_vars.SHOW_LANGSMITH_REPLICA_TRACING,
|
|
960
|
+
),
|
|
961
|
+
ConfigOption(
|
|
962
|
+
key="display.hide_splash_tips",
|
|
963
|
+
group="Display",
|
|
964
|
+
summary="Hide the startup tip shown above the chat input.",
|
|
965
|
+
kind=OptionKind.BOOL,
|
|
966
|
+
default=False,
|
|
967
|
+
env_var=_env_vars.HIDE_SPLASH_TIPS,
|
|
968
|
+
),
|
|
969
|
+
ConfigOption(
|
|
970
|
+
key="display.hide_splash_version",
|
|
971
|
+
group="Display",
|
|
972
|
+
summary="Hide version and local-install details in the splash screen.",
|
|
973
|
+
kind=OptionKind.BOOL,
|
|
974
|
+
default=False,
|
|
975
|
+
env_var=_env_vars.HIDE_SPLASH_VERSION,
|
|
976
|
+
),
|
|
977
|
+
ConfigOption(
|
|
978
|
+
key="display.no_terminal_escape",
|
|
979
|
+
group="Display",
|
|
980
|
+
summary="Disable all terminal escape/control sequence output.",
|
|
981
|
+
kind=OptionKind.BOOL,
|
|
982
|
+
default=False,
|
|
983
|
+
env_var=_env_vars.NO_TERMINAL_ESCAPE,
|
|
984
|
+
),
|
|
985
|
+
ConfigOption(
|
|
986
|
+
key="display.no_mouse",
|
|
987
|
+
group="Display",
|
|
988
|
+
summary=(
|
|
989
|
+
"Disable Textual mouse tracking. Useful for web-based terminals "
|
|
990
|
+
"(1Panel, ttyd, wetty) that strip the ESC prefix from SGR "
|
|
991
|
+
"mouse-report sequences and leak garbled input into the prompt."
|
|
992
|
+
),
|
|
993
|
+
kind=OptionKind.BOOL,
|
|
994
|
+
default=False,
|
|
995
|
+
env_var=_env_vars.NO_MOUSE,
|
|
996
|
+
cli_flag="--no-mouse",
|
|
997
|
+
),
|
|
998
|
+
ConfigOption(
|
|
999
|
+
key="display.show_url_open_toast",
|
|
1000
|
+
group="Display",
|
|
1001
|
+
summary="Show a confirmation toast after clicking a URL.",
|
|
1002
|
+
kind=OptionKind.BOOL,
|
|
1003
|
+
default=True,
|
|
1004
|
+
env_var=_env_vars.SHOW_URL_OPEN_TOAST,
|
|
1005
|
+
toml_keys=("ui", "show_url_open_toast"),
|
|
1006
|
+
),
|
|
1007
|
+
ConfigOption(
|
|
1008
|
+
key="display.onboarding_integrations_screen",
|
|
1009
|
+
group="Display",
|
|
1010
|
+
summary="Show the integrations summary screen during first-run onboarding.",
|
|
1011
|
+
kind=OptionKind.BOOL,
|
|
1012
|
+
default=False,
|
|
1013
|
+
env_var=_env_vars.ONBOARDING_INTEGRATIONS_SCREEN,
|
|
1014
|
+
),
|
|
1015
|
+
# --- Models --------------------------------------------------------
|
|
1016
|
+
ConfigOption(
|
|
1017
|
+
key="models.default",
|
|
1018
|
+
group="Models",
|
|
1019
|
+
summary="Default model spec ('provider:model') used at launch.",
|
|
1020
|
+
kind=OptionKind.STR,
|
|
1021
|
+
toml_keys=("models", "default"),
|
|
1022
|
+
cli_flag="--set-default-model",
|
|
1023
|
+
),
|
|
1024
|
+
ConfigOption(
|
|
1025
|
+
key="models.recent",
|
|
1026
|
+
group="Models",
|
|
1027
|
+
summary="Most recently switched-to model (managed by the app).",
|
|
1028
|
+
kind=OptionKind.STR,
|
|
1029
|
+
toml_keys=("models", "recent"),
|
|
1030
|
+
),
|
|
1031
|
+
# --- Tracing -------------------------------------------------------
|
|
1032
|
+
ConfigOption(
|
|
1033
|
+
key="tracing.langsmith_project",
|
|
1034
|
+
group="Tracing",
|
|
1035
|
+
summary="LangSmith project name for deepagents agent traces.",
|
|
1036
|
+
kind=OptionKind.STR,
|
|
1037
|
+
default=LANGSMITH_PROJECT_DEFAULT,
|
|
1038
|
+
env_var=_env_vars.LANGSMITH_PROJECT,
|
|
1039
|
+
fallback_env_vars=("LANGSMITH_PROJECT",),
|
|
1040
|
+
settings_field="deepagents_langchain_project",
|
|
1041
|
+
),
|
|
1042
|
+
ConfigOption(
|
|
1043
|
+
key="tracing.langsmith_redact",
|
|
1044
|
+
group="Tracing",
|
|
1045
|
+
summary="Redact detected secrets from LangSmith agent traces before upload.",
|
|
1046
|
+
kind=OptionKind.BOOL,
|
|
1047
|
+
default=True,
|
|
1048
|
+
env_var=_env_vars.LANGSMITH_REDACT,
|
|
1049
|
+
toml_keys=("tracing", "langsmith_redact"),
|
|
1050
|
+
),
|
|
1051
|
+
ConfigOption(
|
|
1052
|
+
key="tracing.user_id",
|
|
1053
|
+
group="Tracing",
|
|
1054
|
+
summary="User identifier attached to LangSmith trace metadata.",
|
|
1055
|
+
kind=OptionKind.STR,
|
|
1056
|
+
env_var=_env_vars.USER_ID,
|
|
1057
|
+
),
|
|
1058
|
+
ConfigOption(
|
|
1059
|
+
key="tracing.langsmith_replica_projects",
|
|
1060
|
+
group="Tracing",
|
|
1061
|
+
summary=(
|
|
1062
|
+
"Extra LangSmith project to also write agent traces to. "
|
|
1063
|
+
"Comma-separated for forward-compatibility, but only the first "
|
|
1064
|
+
"project is used; the server mirrors runs to one extra project."
|
|
1065
|
+
),
|
|
1066
|
+
kind=OptionKind.STR,
|
|
1067
|
+
env_var=_env_vars.LANGSMITH_REPLICA_PROJECTS,
|
|
1068
|
+
),
|
|
1069
|
+
# --- Tools / Features ----------------------------------------------
|
|
1070
|
+
ConfigOption(
|
|
1071
|
+
key="shell.allow_list",
|
|
1072
|
+
group="Tools",
|
|
1073
|
+
summary=(
|
|
1074
|
+
"Shell commands allowed without approval (comma-separated, or "
|
|
1075
|
+
"'recommended'/'all')."
|
|
1076
|
+
),
|
|
1077
|
+
kind=OptionKind.SHELL_LIST_DELEGATE,
|
|
1078
|
+
env_var=_env_vars.SHELL_ALLOW_LIST,
|
|
1079
|
+
cli_flag="--shell-allow-list",
|
|
1080
|
+
settings_field="shell_allow_list",
|
|
1081
|
+
),
|
|
1082
|
+
ConfigOption(
|
|
1083
|
+
key="skills.extra_allowed_dirs",
|
|
1084
|
+
group="Tools",
|
|
1085
|
+
summary=(
|
|
1086
|
+
"Extra directories added to the skill symlink containment "
|
|
1087
|
+
"allowlist (env is colon-separated)."
|
|
1088
|
+
),
|
|
1089
|
+
kind=OptionKind.SKILLS_DIRS_DELEGATE,
|
|
1090
|
+
env_var=_env_vars.EXTRA_SKILLS_DIRS,
|
|
1091
|
+
toml_keys=("skills", "extra_allowed_dirs"),
|
|
1092
|
+
settings_field="extra_skills_dirs",
|
|
1093
|
+
),
|
|
1094
|
+
ConfigOption(
|
|
1095
|
+
key="models.ollama_discovery",
|
|
1096
|
+
group="Tools",
|
|
1097
|
+
summary="Toggle Ollama model and profile discovery probes.",
|
|
1098
|
+
kind=OptionKind.BOOL,
|
|
1099
|
+
default=True,
|
|
1100
|
+
env_var=_env_vars.OLLAMA_DISCOVERY,
|
|
1101
|
+
),
|
|
1102
|
+
ConfigOption(
|
|
1103
|
+
key="memory.auto_save",
|
|
1104
|
+
group="Tools",
|
|
1105
|
+
summary=(
|
|
1106
|
+
"Let the agent proactively save learnings to memory (AGENTS.md); "
|
|
1107
|
+
"disable to keep loading memory but stop auto-saving."
|
|
1108
|
+
),
|
|
1109
|
+
kind=OptionKind.BOOL,
|
|
1110
|
+
default=True,
|
|
1111
|
+
env_var=_env_vars.MEMORY_AUTO_SAVE,
|
|
1112
|
+
empty_env_is_false=True,
|
|
1113
|
+
toml_keys=("memory", "auto_save"),
|
|
1114
|
+
),
|
|
1115
|
+
ConfigOption(
|
|
1116
|
+
key="features.experimental",
|
|
1117
|
+
group="Tools",
|
|
1118
|
+
summary="Opt into experimental, unstable dcode behavior.",
|
|
1119
|
+
kind=OptionKind.BOOL,
|
|
1120
|
+
default=False,
|
|
1121
|
+
env_var=_env_vars.EXPERIMENTAL,
|
|
1122
|
+
),
|
|
1123
|
+
ConfigOption(
|
|
1124
|
+
key="events.external_socket",
|
|
1125
|
+
group="Tools",
|
|
1126
|
+
summary="Enable the local Unix-socket external event listener (experimental).",
|
|
1127
|
+
kind=OptionKind.BOOL,
|
|
1128
|
+
default=False,
|
|
1129
|
+
env_var=_env_vars.EXTERNAL_EVENT_SOCKET,
|
|
1130
|
+
),
|
|
1131
|
+
ConfigOption(
|
|
1132
|
+
key="events.external_socket_path",
|
|
1133
|
+
group="Tools",
|
|
1134
|
+
summary="Override the default Unix-socket path for the event listener.",
|
|
1135
|
+
kind=OptionKind.STR,
|
|
1136
|
+
env_var=_env_vars.EXTERNAL_EVENT_SOCKET_PATH,
|
|
1137
|
+
),
|
|
1138
|
+
# --- Interpreter (config.toml-only; defaults owned by this module) --
|
|
1139
|
+
ConfigOption(
|
|
1140
|
+
key="interpreter.enable_interpreter",
|
|
1141
|
+
group="Interpreter",
|
|
1142
|
+
summary="Wire the QuickJS REPL middleware into the main agent (local only).",
|
|
1143
|
+
kind=OptionKind.BOOL,
|
|
1144
|
+
default=INTERPRETER_ENABLE_DEFAULT,
|
|
1145
|
+
toml_keys=("interpreter", "enable_interpreter"),
|
|
1146
|
+
cli_flag="--interpreter",
|
|
1147
|
+
settings_field="enable_interpreter",
|
|
1148
|
+
),
|
|
1149
|
+
ConfigOption(
|
|
1150
|
+
key="interpreter.timeout_seconds",
|
|
1151
|
+
group="Interpreter",
|
|
1152
|
+
summary="Per-call wall-clock timeout for the QuickJS REPL.",
|
|
1153
|
+
kind=OptionKind.FLOAT,
|
|
1154
|
+
default=INTERPRETER_TIMEOUT_SECONDS_DEFAULT,
|
|
1155
|
+
toml_keys=("interpreter", "timeout_seconds"),
|
|
1156
|
+
settings_field="interpreter_timeout_seconds",
|
|
1157
|
+
),
|
|
1158
|
+
ConfigOption(
|
|
1159
|
+
key="interpreter.memory_limit_mb",
|
|
1160
|
+
group="Interpreter",
|
|
1161
|
+
summary="QuickJS heap memory cap (MB) shared across a session.",
|
|
1162
|
+
kind=OptionKind.INT,
|
|
1163
|
+
default=INTERPRETER_MEMORY_LIMIT_MB_DEFAULT,
|
|
1164
|
+
toml_keys=("interpreter", "memory_limit_mb"),
|
|
1165
|
+
settings_field="interpreter_memory_limit_mb",
|
|
1166
|
+
),
|
|
1167
|
+
ConfigOption(
|
|
1168
|
+
key="interpreter.max_ptc_calls",
|
|
1169
|
+
group="Interpreter",
|
|
1170
|
+
summary="Maximum tools.* host-bridge invocations per js_eval call.",
|
|
1171
|
+
kind=OptionKind.INT,
|
|
1172
|
+
default=INTERPRETER_MAX_PTC_CALLS_DEFAULT,
|
|
1173
|
+
toml_keys=("interpreter", "max_ptc_calls"),
|
|
1174
|
+
settings_field="interpreter_max_ptc_calls",
|
|
1175
|
+
),
|
|
1176
|
+
ConfigOption(
|
|
1177
|
+
key="interpreter.max_result_chars",
|
|
1178
|
+
group="Interpreter",
|
|
1179
|
+
summary="Cap (chars) on js_eval result and stdout before truncation.",
|
|
1180
|
+
kind=OptionKind.INT,
|
|
1181
|
+
default=INTERPRETER_MAX_RESULT_CHARS_DEFAULT,
|
|
1182
|
+
toml_keys=("interpreter", "max_result_chars"),
|
|
1183
|
+
settings_field="interpreter_max_result_chars",
|
|
1184
|
+
),
|
|
1185
|
+
ConfigOption(
|
|
1186
|
+
key="interpreter.ptc",
|
|
1187
|
+
group="Interpreter",
|
|
1188
|
+
summary="Programmatic tool-calling allowlist ('safe', 'all', or names).",
|
|
1189
|
+
kind=OptionKind.PTC_DELEGATE,
|
|
1190
|
+
default=INTERPRETER_PTC_DEFAULT,
|
|
1191
|
+
toml_keys=("interpreter", "ptc"),
|
|
1192
|
+
cli_flag="--interpreter-tools",
|
|
1193
|
+
settings_field="interpreter_ptc",
|
|
1194
|
+
),
|
|
1195
|
+
ConfigOption(
|
|
1196
|
+
key="interpreter.ptc_acknowledge_unsafe",
|
|
1197
|
+
group="Interpreter",
|
|
1198
|
+
summary="Acknowledge exposing every tool when interpreter.ptc='all'.",
|
|
1199
|
+
kind=OptionKind.BOOL,
|
|
1200
|
+
default=INTERPRETER_PTC_ACKNOWLEDGE_UNSAFE_DEFAULT,
|
|
1201
|
+
toml_keys=("interpreter", "ptc_acknowledge_unsafe"),
|
|
1202
|
+
settings_field="interpreter_ptc_acknowledge_unsafe",
|
|
1203
|
+
),
|
|
1204
|
+
# --- Threads (config.toml-only; structured column table excepted) ---
|
|
1205
|
+
ConfigOption(
|
|
1206
|
+
key="threads.relative_time",
|
|
1207
|
+
group="Threads",
|
|
1208
|
+
summary="Show thread timestamps as relative time.",
|
|
1209
|
+
kind=OptionKind.BOOL,
|
|
1210
|
+
default=True,
|
|
1211
|
+
toml_keys=("threads", "relative_time"),
|
|
1212
|
+
cli_flag="--relative",
|
|
1213
|
+
),
|
|
1214
|
+
ConfigOption(
|
|
1215
|
+
key="threads.sort_order",
|
|
1216
|
+
group="Threads",
|
|
1217
|
+
summary="Default thread sort key ('updated_at' or 'created_at').",
|
|
1218
|
+
kind=OptionKind.STR,
|
|
1219
|
+
default="updated_at",
|
|
1220
|
+
toml_keys=("threads", "sort_order"),
|
|
1221
|
+
cli_flag="--sort",
|
|
1222
|
+
),
|
|
1223
|
+
ConfigOption(
|
|
1224
|
+
key="threads.columns",
|
|
1225
|
+
group="Threads",
|
|
1226
|
+
summary="Per-column visibility for the threads list.",
|
|
1227
|
+
kind=OptionKind.STRUCTURED,
|
|
1228
|
+
toml_keys=("threads", "columns"),
|
|
1229
|
+
),
|
|
1230
|
+
# --- Warnings ------------------------------------------------------
|
|
1231
|
+
ConfigOption(
|
|
1232
|
+
key="warnings.suppress",
|
|
1233
|
+
group="Warnings",
|
|
1234
|
+
summary="Warning keys to suppress (e.g. 'ripgrep').",
|
|
1235
|
+
kind=OptionKind.STRUCTURED,
|
|
1236
|
+
toml_keys=("warnings", "suppress"),
|
|
1237
|
+
),
|
|
1238
|
+
ConfigOption(
|
|
1239
|
+
key="warnings.suppress_env_override",
|
|
1240
|
+
group="Warnings",
|
|
1241
|
+
summary="Silence the LangSmith env-var override warning at startup.",
|
|
1242
|
+
kind=OptionKind.BOOL,
|
|
1243
|
+
default=False,
|
|
1244
|
+
env_var=_env_vars.SUPPRESS_ENV_OVERRIDE_WARNING,
|
|
1245
|
+
),
|
|
1246
|
+
# --- MCP ------------------------------------------------------------
|
|
1247
|
+
# Project trust lists are parsed by `model_config.load_mcp_server_trust_lists`,
|
|
1248
|
+
# which reads them only from the user-level config.toml (never a project file),
|
|
1249
|
+
# so they are STRUCTURED-for-discovery here rather than env-backed scalars. The
|
|
1250
|
+
# env overrides are named in the summaries instead of `env_var` because the
|
|
1251
|
+
# scalar resolver rejects env-backed STRUCTURED options by design.
|
|
1252
|
+
ConfigOption(
|
|
1253
|
+
key="mcp.enabled_project_servers",
|
|
1254
|
+
group="MCP",
|
|
1255
|
+
summary=(
|
|
1256
|
+
"Project MCP server names to pre-approve by name from an untrusted "
|
|
1257
|
+
".mcp.json; command/URL changes under the same name still match "
|
|
1258
|
+
"(env: DEEPAGENTS_CODE_ENABLED_PROJECT_MCP_SERVERS)."
|
|
1259
|
+
),
|
|
1260
|
+
kind=OptionKind.STRUCTURED,
|
|
1261
|
+
toml_keys=("mcp", "enabled_project_servers"),
|
|
1262
|
+
),
|
|
1263
|
+
ConfigOption(
|
|
1264
|
+
key="mcp.disabled_project_servers",
|
|
1265
|
+
group="MCP",
|
|
1266
|
+
summary=(
|
|
1267
|
+
"Project MCP server names to always reject; reject wins over approval "
|
|
1268
|
+
"and trust (env: DEEPAGENTS_CODE_DISABLED_PROJECT_MCP_SERVERS)."
|
|
1269
|
+
),
|
|
1270
|
+
kind=OptionKind.STRUCTURED,
|
|
1271
|
+
toml_keys=("mcp", "disabled_project_servers"),
|
|
1272
|
+
),
|
|
1273
|
+
# Unlike the two trust lists above, this one is managed by `mcp_disabled`
|
|
1274
|
+
# (the server viewer's disable toggle), not `load_mcp_server_trust_lists`;
|
|
1275
|
+
# it plays no part in the project-trust security boundary. It is STRUCTURED
|
|
1276
|
+
# here purely for discovery.
|
|
1277
|
+
ConfigOption(
|
|
1278
|
+
key="mcp.disabled_servers",
|
|
1279
|
+
group="MCP",
|
|
1280
|
+
summary="MCP server names disabled by the user from the server viewer.",
|
|
1281
|
+
kind=OptionKind.STRUCTURED,
|
|
1282
|
+
toml_keys=("mcp", "disabled_servers"),
|
|
1283
|
+
),
|
|
1284
|
+
# --- Updates --------------------------------------------------------
|
|
1285
|
+
ConfigOption(
|
|
1286
|
+
key="update.auto_update",
|
|
1287
|
+
group="Updates",
|
|
1288
|
+
summary="Enable automatic app updates.",
|
|
1289
|
+
kind=OptionKind.BOOL,
|
|
1290
|
+
default=True,
|
|
1291
|
+
env_var=_env_vars.AUTO_UPDATE,
|
|
1292
|
+
toml_keys=("update", "auto_update"),
|
|
1293
|
+
cli_flag="--set-auto-update",
|
|
1294
|
+
),
|
|
1295
|
+
ConfigOption(
|
|
1296
|
+
key="update.no_update_check",
|
|
1297
|
+
group="Updates",
|
|
1298
|
+
summary="Disable automatic update checking.",
|
|
1299
|
+
kind=OptionKind.BOOL_PRESENCE,
|
|
1300
|
+
default=False,
|
|
1301
|
+
env_var=_env_vars.NO_UPDATE_CHECK,
|
|
1302
|
+
toml_keys=("update", "check"),
|
|
1303
|
+
invert_toml_bool=True,
|
|
1304
|
+
),
|
|
1305
|
+
# --- Runtime --------------------------------------------------------
|
|
1306
|
+
ConfigOption(
|
|
1307
|
+
key="runtime.offline",
|
|
1308
|
+
group="Runtime",
|
|
1309
|
+
summary="Disable managed binary downloads and use local fallbacks.",
|
|
1310
|
+
kind=OptionKind.BOOL,
|
|
1311
|
+
default=False,
|
|
1312
|
+
env_var=_env_vars.OFFLINE,
|
|
1313
|
+
),
|
|
1314
|
+
ConfigOption(
|
|
1315
|
+
key="runtime.ripgrep_installer",
|
|
1316
|
+
group="Runtime",
|
|
1317
|
+
summary="Select ripgrep provisioning mode ('managed' or 'system').",
|
|
1318
|
+
kind=OptionKind.STR,
|
|
1319
|
+
default="managed",
|
|
1320
|
+
env_var=_env_vars.RIPGREP_INSTALLER,
|
|
1321
|
+
),
|
|
1322
|
+
# --- Startup --------------------------------------------------------
|
|
1323
|
+
ConfigOption(
|
|
1324
|
+
key="startup.mode",
|
|
1325
|
+
group="Startup",
|
|
1326
|
+
summary="Default approval mode at launch ('manual' or 'dangerously-auto').",
|
|
1327
|
+
kind=OptionKind.STARTUP_MODE_DELEGATE,
|
|
1328
|
+
default="manual",
|
|
1329
|
+
toml_keys=("startup", "mode"),
|
|
1330
|
+
cli_flag="--auto-approve",
|
|
1331
|
+
),
|
|
1332
|
+
# --- Debug / Development -------------------------------------------
|
|
1333
|
+
ConfigOption(
|
|
1334
|
+
key="debug.enabled",
|
|
1335
|
+
group="Debug",
|
|
1336
|
+
summary="Enable verbose debug logging and preserve the server log.",
|
|
1337
|
+
kind=OptionKind.BOOL,
|
|
1338
|
+
default=False,
|
|
1339
|
+
env_var=_env_vars.DEBUG,
|
|
1340
|
+
),
|
|
1341
|
+
ConfigOption(
|
|
1342
|
+
key="debug.file",
|
|
1343
|
+
group="Debug",
|
|
1344
|
+
summary="Path for the debug log file.",
|
|
1345
|
+
kind=OptionKind.STR,
|
|
1346
|
+
default="/tmp/deepagents_debug.log", # noqa: S108 # documents the app default, not a write target
|
|
1347
|
+
env_var=_env_vars.DEBUG_FILE,
|
|
1348
|
+
),
|
|
1349
|
+
ConfigOption(
|
|
1350
|
+
key="debug.log_level",
|
|
1351
|
+
group="Debug",
|
|
1352
|
+
summary=(
|
|
1353
|
+
"Minimum runtime log level (DEBUG, INFO, WARNING, ERROR, or CRITICAL); "
|
|
1354
|
+
"defaults to DEBUG in debug mode and INFO otherwise."
|
|
1355
|
+
),
|
|
1356
|
+
kind=OptionKind.LOG_LEVEL_DELEGATE,
|
|
1357
|
+
env_var=_env_vars.LOG_LEVEL,
|
|
1358
|
+
),
|
|
1359
|
+
ConfigOption(
|
|
1360
|
+
key="debug.onboarding",
|
|
1361
|
+
group="Debug",
|
|
1362
|
+
summary="Force the onboarding flow to open on every interactive startup.",
|
|
1363
|
+
kind=OptionKind.BOOL,
|
|
1364
|
+
default=False,
|
|
1365
|
+
env_var=_env_vars.DEBUG_ONBOARDING,
|
|
1366
|
+
),
|
|
1367
|
+
ConfigOption(
|
|
1368
|
+
key="debug.notifications",
|
|
1369
|
+
group="Debug",
|
|
1370
|
+
summary="Inject sample missing-dependency notifications at launch.",
|
|
1371
|
+
kind=OptionKind.BOOL_PRESENCE,
|
|
1372
|
+
default=False,
|
|
1373
|
+
env_var=_env_vars.DEBUG_NOTIFICATIONS,
|
|
1374
|
+
),
|
|
1375
|
+
ConfigOption(
|
|
1376
|
+
key="debug.update",
|
|
1377
|
+
group="Debug",
|
|
1378
|
+
summary="Inject a sample update notification and open the update modal.",
|
|
1379
|
+
kind=OptionKind.BOOL_PRESENCE,
|
|
1380
|
+
default=False,
|
|
1381
|
+
env_var=_env_vars.DEBUG_UPDATE,
|
|
1382
|
+
),
|
|
1383
|
+
ConfigOption(
|
|
1384
|
+
key="debug.mcp_project_trust",
|
|
1385
|
+
group="Debug",
|
|
1386
|
+
summary="Force the project MCP approval prompt for manual UI testing.",
|
|
1387
|
+
kind=OptionKind.BOOL,
|
|
1388
|
+
default=False,
|
|
1389
|
+
env_var=_env_vars.DEBUG_MCP_PROJECT_TRUST,
|
|
1390
|
+
),
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
# Env-var constants in `_env_vars` that are not standalone options: prefixes
|
|
1395
|
+
# and aggregates the manifest does not enumerate, plus internal/transient
|
|
1396
|
+
# signaling flags the app sets for itself rather than reading as user config.
|
|
1397
|
+
NON_OPTION_ENV_VARS: frozenset[str] = frozenset(
|
|
1398
|
+
{
|
|
1399
|
+
_env_vars.SERVER_ENV_PREFIX,
|
|
1400
|
+
# Set then popped during the self-update restart handshake (main.py);
|
|
1401
|
+
# never user-configured.
|
|
1402
|
+
_env_vars.RESTARTED_AFTER_UPDATE,
|
|
1403
|
+
# Advanced overrides for the update-check JSON API mirror URLs. Resolved
|
|
1404
|
+
# in `_version.py` at import time, not surfaced as user config options.
|
|
1405
|
+
_env_vars.PYPI_URL,
|
|
1406
|
+
_env_vars.SDK_PYPI_URL,
|
|
1407
|
+
# Env equivalents of the STRUCTURED `[mcp]` lists. They are read by the
|
|
1408
|
+
# dedicated `model_config.load_mcp_server_trust_lists` loader (which the
|
|
1409
|
+
# `mcp.*` STRUCTURED options describe for discovery), not by the scalar
|
|
1410
|
+
# resolver, so they intentionally have no scalar `env_var` ConfigOption.
|
|
1411
|
+
_env_vars.ENABLED_PROJECT_MCP_SERVERS,
|
|
1412
|
+
_env_vars.DISABLED_PROJECT_MCP_SERVERS,
|
|
1413
|
+
}
|
|
1414
|
+
)
|
|
1415
|
+
"""`_env_vars` constants intentionally excluded from the option catalog."""
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
@lru_cache(maxsize=1)
|
|
1419
|
+
def get_config_options() -> tuple[ConfigOption, ...]:
|
|
1420
|
+
"""Return every option, credentials-first then by domain group.
|
|
1421
|
+
|
|
1422
|
+
Cached: provider credentials are generated once from `PROVIDER_API_KEY_ENV`
|
|
1423
|
+
on first call (which lazily imports `model_config`). The cache assumes that
|
|
1424
|
+
registry is an immutable module constant; a test that monkeypatches it must
|
|
1425
|
+
call `get_config_options.cache_clear()` (and `_options_by_key.cache_clear()`).
|
|
1426
|
+
"""
|
|
1427
|
+
return _credential_options() + _STATIC_OPTIONS
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def get_option(key: str) -> ConfigOption | None:
|
|
1431
|
+
"""Return the manifest entry for `key`, or `None` when unknown."""
|
|
1432
|
+
return _options_by_key().get(key)
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def option_keys() -> tuple[str, ...]:
|
|
1436
|
+
"""Return every manifest key in definition order."""
|
|
1437
|
+
return tuple(opt.key for opt in get_config_options())
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
@lru_cache(maxsize=1)
|
|
1441
|
+
def _options_by_key() -> dict[str, ConfigOption]:
|
|
1442
|
+
return {opt.key: opt for opt in get_config_options()}
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def iter_groups(options: Iterable[ConfigOption]) -> list[str]:
|
|
1446
|
+
"""Return group names from `options` in first-seen order."""
|
|
1447
|
+
groups: list[str] = []
|
|
1448
|
+
for opt in options:
|
|
1449
|
+
if opt.group not in groups:
|
|
1450
|
+
groups.append(opt.group)
|
|
1451
|
+
return groups
|