dex-agents-cli-core 0.5.0__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.
@@ -0,0 +1,210 @@
1
+ # Copyright (c) 2026 Super Durable, Inc.
2
+ #
3
+ # Licensed under the Sustainable Use License 1.0.
4
+ # You may not use this file except in compliance with the License.
5
+ # See the LICENSE file in the repository root.
6
+ #
7
+ # SPDX-License-Identifier: LicenseRef-Sustainable-Use-1.0
8
+
9
+ """Find, load, and create a command's configuration file."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import io
14
+ from collections.abc import Callable
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from ruamel.yaml import YAML
20
+
21
+ from dex_agents_cli_core.config import (
22
+ DEFAULT_WORKSPACE,
23
+ ConfigError,
24
+ EffectiveConfig,
25
+ parse_config_yaml,
26
+ resolve_config,
27
+ )
28
+ from dex_agents_cli_core.environment import CommandEnvironment
29
+ from dex_agents_cli_core.harness import AgentHarness
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class LoadedConfig:
34
+ """A command's effective configuration and where it came from.
35
+
36
+ Attributes:
37
+ config: The effective configuration.
38
+ path: The file, or ``None`` when the command runs on defaults.
39
+ directory: The directory relative paths in the file are relative to.
40
+ """
41
+
42
+ config: EffectiveConfig
43
+ path: Path | None
44
+ directory: Path
45
+
46
+
47
+ class ConfigFileError(Exception):
48
+ """Report a configuration file that cannot be read or is invalid."""
49
+
50
+
51
+ def default_config_path(environment: CommandEnvironment, harness: AgentHarness) -> Path:
52
+ """Return ``~/.dex/agents/<name>.yaml``.
53
+
54
+ Args:
55
+ environment: The machine settings.
56
+ harness: The command's harness.
57
+
58
+ Returns:
59
+ The default path.
60
+ """
61
+ return environment.agents_directory / f"{harness.name}.yaml"
62
+
63
+
64
+ def load_config_file(path: Path, harness: AgentHarness) -> LoadedConfig:
65
+ """Read and resolve a configuration file.
66
+
67
+ Args:
68
+ path: The file.
69
+ harness: The command's harness.
70
+
71
+ Returns:
72
+ The effective configuration.
73
+
74
+ Raises:
75
+ ConfigFileError: If the file cannot be read or fails a rule, with the
76
+ error code and JSON Pointer.
77
+ """
78
+ try:
79
+ text = path.read_text(encoding="utf-8")
80
+ except OSError as error:
81
+ raise ConfigFileError(f"cannot read {path}: {error.strerror}") from error
82
+ try:
83
+ config = resolve_config(
84
+ parse_config_yaml(text), harness.config_section, harness.default_model
85
+ )
86
+ except ConfigError as error:
87
+ raise ConfigFileError(f"{path}: {error}") from error
88
+ return LoadedConfig(config=config, path=path, directory=path.parent)
89
+
90
+
91
+ def default_config_values(harness: AgentHarness) -> dict[str, Any]:
92
+ """Return the values ``init --yes`` writes: the defaults, stated in full.
93
+
94
+ Args:
95
+ harness: The command's harness.
96
+
97
+ Returns:
98
+ The configuration file's values.
99
+ """
100
+ values: dict[str, Any] = {}
101
+ if harness.default_model is not None:
102
+ values["model"] = harness.default_model
103
+ values.update(
104
+ {
105
+ "dex": {"server": "auto"},
106
+ "connectors": {"mcp": "auto"},
107
+ "workspace": DEFAULT_WORKSPACE,
108
+ "local_tools": {
109
+ "files": True,
110
+ "shell": {"enabled": True, "allow": []},
111
+ "web_fetch": True,
112
+ "browser": True,
113
+ },
114
+ "media": {"store": "auto"},
115
+ }
116
+ )
117
+ return values
118
+
119
+
120
+ def ask_config_values(harness: AgentHarness, ask: Callable[[str, str], str]) -> dict[str, Any]:
121
+ """Ask the init questions and return the configuration file's values.
122
+
123
+ Args:
124
+ harness: The command's harness.
125
+ ask: Shows a question with its default and returns the answer, the
126
+ default when the answer is empty.
127
+
128
+ Returns:
129
+ The values to write.
130
+ """
131
+ values = default_config_values(harness)
132
+ model = ask("Model", harness.default_model or "").strip()
133
+ if model:
134
+ values["model"] = model
135
+ prompt = ask("System prompt (empty for the harness default)", "").strip()
136
+ if prompt:
137
+ values["system_prompt"] = prompt
138
+ connectors = ask("Connectors to enable, comma-separated (empty for all authorized)", "")
139
+ entries = [entry.strip() for entry in connectors.split(",") if entry.strip()]
140
+ if entries:
141
+ values["connectors"]["enabled"] = entries
142
+ values["workspace"] = ask("Workspace for local files and commands", DEFAULT_WORKSPACE).strip()
143
+ local_tools = values["local_tools"]
144
+ local_tools["files"] = _is_yes(ask("Allow file tools in the workspace? (y/n)", "y"))
145
+ local_tools["shell"]["enabled"] = _is_yes(ask("Allow shell commands? (y/n)", "y"))
146
+ if local_tools["shell"]["enabled"]:
147
+ allow = ask("Commands that run without approval, comma-separated", "")
148
+ local_tools["shell"]["allow"] = [part.strip() for part in allow.split(",") if part.strip()]
149
+ local_tools["web_fetch"] = _is_yes(ask("Allow web fetching? (y/n)", "y"))
150
+ local_tools["browser"] = _is_yes(ask("Allow a local browser? (y/n)", "y"))
151
+ return values
152
+
153
+
154
+ def write_config_file(path: Path, values: dict[str, Any], harness: AgentHarness) -> LoadedConfig:
155
+ """Check values against the configuration rules and write them as YAML.
156
+
157
+ Args:
158
+ path: The file to create or replace.
159
+ values: The configuration file's values.
160
+ harness: The command's harness.
161
+
162
+ Returns:
163
+ The effective configuration of the written file.
164
+
165
+ Raises:
166
+ ConfigFileError: If the values fail a rule; nothing is written.
167
+ """
168
+ try:
169
+ resolve_config(values, harness.config_section, harness.default_model)
170
+ except ConfigError as error:
171
+ raise ConfigFileError(f"the answers are not a valid configuration: {error}") from error
172
+ yaml = YAML(typ="safe", pure=True)
173
+ yaml.default_flow_style = False
174
+ buffer = io.StringIO()
175
+ yaml.dump(values, buffer)
176
+ header = (
177
+ f"# The configuration of dex-agents-{harness.name}; regenerate it with "
178
+ f"`dex-agents-{harness.name} init`.\n"
179
+ "# Provider keys stay in the environment. Schema: agents/cli/config.schema.json "
180
+ "in the Dex repository.\n"
181
+ )
182
+ path.parent.mkdir(parents=True, exist_ok=True)
183
+ path.write_text(header + buffer.getvalue(), encoding="utf-8")
184
+ return load_config_file(path, harness)
185
+
186
+
187
+ def defaults_without_file(harness: AgentHarness, directory: Path) -> LoadedConfig:
188
+ """Return the configuration of a command that runs without a file.
189
+
190
+ Args:
191
+ harness: The command's harness.
192
+ directory: The directory relative paths resolve against.
193
+
194
+ Returns:
195
+ Every default.
196
+
197
+ Raises:
198
+ ConfigFileError: If the harness has no default model.
199
+ """
200
+ try:
201
+ config = resolve_config({}, harness.config_section, harness.default_model)
202
+ except ConfigError as error:
203
+ raise ConfigFileError(
204
+ f"dex-agents-{harness.name} has no default model; run `dex-agents-{harness.name} init`"
205
+ ) from error
206
+ return LoadedConfig(config=config, path=None, directory=directory)
207
+
208
+
209
+ def _is_yes(answer: str) -> bool:
210
+ return answer.strip().lower() in ("y", "yes", "true")