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.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. 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