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.
- dex_agents_cli_core/__init__.py +101 -0
- dex_agents_cli_core/chat.py +538 -0
- dex_agents_cli_core/cli.py +671 -0
- dex_agents_cli_core/config.py +650 -0
- dex_agents_cli_core/config_file.py +210 -0
- dex_agents_cli_core/connectors.py +531 -0
- dex_agents_cli_core/dex_server.py +345 -0
- dex_agents_cli_core/dexcli_install.py +314 -0
- dex_agents_cli_core/environment.py +136 -0
- dex_agents_cli_core/harness.py +445 -0
- dex_agents_cli_core/local_tools/__init__.py +26 -0
- dex_agents_cli_core/local_tools/browser.py +235 -0
- dex_agents_cli_core/local_tools/files.py +512 -0
- dex_agents_cli_core/local_tools/shell.py +85 -0
- dex_agents_cli_core/local_tools/source.py +458 -0
- dex_agents_cli_core/local_tools/web.py +98 -0
- dex_agents_cli_core/py.typed +0 -0
- dex_agents_cli_core/schemas/__init__.py +9 -0
- dex_agents_cli_core/schemas/config.schema.json +315 -0
- dex_agents_cli_core/schemas/session-settings.schema.json +153 -0
- dex_agents_cli_core/tool_policy.py +179 -0
- dex_agents_cli_core/turns.py +235 -0
- dex_agents_cli_core-0.5.0.dist-info/METADATA +479 -0
- dex_agents_cli_core-0.5.0.dist-info/RECORD +26 -0
- dex_agents_cli_core-0.5.0.dist-info/WHEEL +4 -0
- dex_agents_cli_core-0.5.0.dist-info/licenses/LICENSE +105 -0
|
@@ -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")
|