dex-agents-cli-core 0.5.0__tar.gz
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-0.5.0/LICENSE +105 -0
- dex_agents_cli_core-0.5.0/PKG-INFO +479 -0
- dex_agents_cli_core-0.5.0/README.md +457 -0
- dex_agents_cli_core-0.5.0/pyproject.toml +101 -0
- dex_agents_cli_core-0.5.0/pyproject.toml.orig +76 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/__init__.py +101 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/chat.py +538 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/cli.py +671 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/config.py +650 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/config_file.py +210 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/connectors.py +531 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/dex_server.py +345 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/dexcli_install.py +314 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/environment.py +136 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/harness.py +445 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/local_tools/__init__.py +26 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/local_tools/browser.py +235 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/local_tools/files.py +512 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/local_tools/shell.py +85 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/local_tools/source.py +458 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/local_tools/web.py +98 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/py.typed +0 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/schemas/__init__.py +9 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/schemas/config.schema.json +315 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/schemas/session-settings.schema.json +153 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/tool_policy.py +179 -0
- dex_agents_cli_core-0.5.0/src/dex_agents_cli_core/turns.py +235 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Super Durable, Inc.
|
|
4
|
+
|
|
5
|
+
Portions of this software are licensed as follows:
|
|
6
|
+
|
|
7
|
+
- Legacy Materials remain under their original licenses as described in
|
|
8
|
+
`LEGACY_NOTICES.md`.
|
|
9
|
+
- Third-Party Materials remain under the licenses provided by their owners.
|
|
10
|
+
- The `docs/` and `examples/` directories are not licensed under the
|
|
11
|
+
Sustainable Use License and retain their existing licensing status.
|
|
12
|
+
- All other material owned or licensable by Super Durable, Inc. is available
|
|
13
|
+
under the Sustainable Use License below.
|
|
14
|
+
|
|
15
|
+
See `LEGACY_NOTICES.md` for the applicable legacy and third-party notices.
|
|
16
|
+
|
|
17
|
+
## Sustainable Use License
|
|
18
|
+
|
|
19
|
+
Version 1.0
|
|
20
|
+
|
|
21
|
+
### Acceptance
|
|
22
|
+
|
|
23
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
24
|
+
|
|
25
|
+
### Copyright License
|
|
26
|
+
|
|
27
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
28
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
29
|
+
available, and prepare derivative works of the software, in each case subject
|
|
30
|
+
to the limitations below.
|
|
31
|
+
|
|
32
|
+
### Limitations
|
|
33
|
+
|
|
34
|
+
You may use or modify the software only for your own internal business purposes
|
|
35
|
+
or for non-commercial or personal use. You may distribute the software or
|
|
36
|
+
provide it to others only if you do so free of charge for non-commercial
|
|
37
|
+
purposes. You may not alter, remove, or obscure any licensing, copyright, or
|
|
38
|
+
other notices of the licensor in the software. Any use of the licensor's
|
|
39
|
+
trademarks is subject to applicable law.
|
|
40
|
+
|
|
41
|
+
### Patents
|
|
42
|
+
|
|
43
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
44
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
45
|
+
sale, import and have imported the software, in each case subject to the
|
|
46
|
+
limitations and conditions in this license. This license does not cover any
|
|
47
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
48
|
+
the software.
|
|
49
|
+
|
|
50
|
+
If you or your company make any written claim that the software infringes or
|
|
51
|
+
contributes to infringement of any patent, your patent license for the software
|
|
52
|
+
granted under these terms ends immediately. If your company makes such a claim,
|
|
53
|
+
your patent license ends immediately for work on behalf of your company.
|
|
54
|
+
|
|
55
|
+
### Notices
|
|
56
|
+
|
|
57
|
+
You must ensure that anyone who gets a copy of any part of the software from
|
|
58
|
+
you also gets a copy of these terms. If you modify the software, you must
|
|
59
|
+
include in any modified copies of the software a prominent notice stating that
|
|
60
|
+
you have modified the software.
|
|
61
|
+
|
|
62
|
+
### No Other Rights
|
|
63
|
+
|
|
64
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
65
|
+
these terms.
|
|
66
|
+
|
|
67
|
+
### Termination
|
|
68
|
+
|
|
69
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
70
|
+
and your license will automatically terminate. If the licensor provides you
|
|
71
|
+
with a notice of your violation, and you cease all violation of this license no
|
|
72
|
+
later than 30 days after you receive that notice, your license will be
|
|
73
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
74
|
+
reinstatement, any additional violation of these terms will cause your license
|
|
75
|
+
to terminate automatically and permanently.
|
|
76
|
+
|
|
77
|
+
### No Liability
|
|
78
|
+
|
|
79
|
+
As far as the law allows, the software comes as is, without any warranty or
|
|
80
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
81
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
82
|
+
legal claim.
|
|
83
|
+
|
|
84
|
+
### Definitions
|
|
85
|
+
|
|
86
|
+
The "licensor" is the entity offering these terms.
|
|
87
|
+
|
|
88
|
+
The "software" is the software the licensor makes available under these terms,
|
|
89
|
+
including any portion of it.
|
|
90
|
+
|
|
91
|
+
"You" refers to the individual or entity agreeing to these terms.
|
|
92
|
+
|
|
93
|
+
"Your company" is any legal entity, sole proprietorship, or other kind of
|
|
94
|
+
organization that you work for, plus all organizations that have control over,
|
|
95
|
+
are under the control of, or are under common control with that organization.
|
|
96
|
+
Control means ownership of substantially all the assets of an entity, or the
|
|
97
|
+
power to direct its management and policies by vote, contract, or otherwise.
|
|
98
|
+
Control can be direct or indirect.
|
|
99
|
+
|
|
100
|
+
"Your license" is the license granted to you for the software under these
|
|
101
|
+
terms.
|
|
102
|
+
|
|
103
|
+
"Use" means anything you do with the software requiring your license.
|
|
104
|
+
|
|
105
|
+
"Trademark" means trademarks, service marks, and similar rights.
|
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dex-agents-cli-core
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: The shared CLI core of the Dex Agents CLI commands: configuration, dexcli dev, connectors, local tools, and terminal chat
|
|
5
|
+
Author: Super Durable
|
|
6
|
+
License-Expression: LicenseRef-Sustainable-Use-1.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Dist: dex-agents[mcp]==0.5.0
|
|
13
|
+
Requires-Dist: dex-python-sdk==1.45.0
|
|
14
|
+
Requires-Dist: jsonschema>=4.26,<5
|
|
15
|
+
Requires-Dist: playwright>=1.55,<2
|
|
16
|
+
Requires-Dist: ruamel-yaml>=0.18,<0.19
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Homepage, https://github.com/superdurable/dex/tree/main/agents/cli/python/core
|
|
19
|
+
Project-URL: Repository, https://github.com/superdurable/dex
|
|
20
|
+
Project-URL: Changelog, https://github.com/superdurable/dex/blob/main/agents/CHANGELOG.md
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# dex-agents-cli-core
|
|
24
|
+
|
|
25
|
+
`dex-agents-cli-core` (import `dex_agents_cli_core`) is the shared CLI core of the Python
|
|
26
|
+
Dex Agents CLI commands, such as `dex-agents-claude-sdk`. Each command is a
|
|
27
|
+
thin harness package. It builds the agent session Flow of one agent framework
|
|
28
|
+
and calls `dex_agents_cli_core.run_agent_cli`. The CLI core does everything else:
|
|
29
|
+
|
|
30
|
+
- reads `~/.dex/agents/<name>.yaml`, the shared format in
|
|
31
|
+
[`agents/cli`](../../README.md), and creates it on first run;
|
|
32
|
+
- reuses the machine's running `dexcli dev`, or starts one, and downloads
|
|
33
|
+
`dexcli` when it is not installed;
|
|
34
|
+
- offers connector tools from the connectors MCP server and local tools for
|
|
35
|
+
files, shell commands, web pages, and a browser;
|
|
36
|
+
- runs the Worker in the command's process and chats with one session in the
|
|
37
|
+
terminal.
|
|
38
|
+
|
|
39
|
+
The TypeScript CLI core in `agents/cli/typescript/core` has the same
|
|
40
|
+
behavior and the same concepts, with the same names in camel case.
|
|
41
|
+
|
|
42
|
+
## Running a command
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
dex-agents-<name> [run|init|connectors|sessions] [options]
|
|
46
|
+
|
|
47
|
+
run Chat with the agent in this terminal (the default).
|
|
48
|
+
init Create or replace the configuration file.
|
|
49
|
+
connectors List the connector tools and the Dex Web connections.
|
|
50
|
+
sessions List this command's sessions.
|
|
51
|
+
|
|
52
|
+
-c, --config <file> Configuration file (default ~/.dex/agents/<name>.yaml).
|
|
53
|
+
-y, --yes Write and use the defaults instead of asking.
|
|
54
|
+
-r, --resume <id> Continue a session.
|
|
55
|
+
-m, --model <id> Override the configured model.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Without a configuration file, `run` asks the `init` questions at a terminal
|
|
59
|
+
and writes the file. With `--yes` it writes the defaults without asking.
|
|
60
|
+
Without a terminal it runs on the defaults, writes nothing, and says to run
|
|
61
|
+
`init`. A file that fails the rules stops the command with the error code and
|
|
62
|
+
JSON Pointer, such as `schema at "/local_tools/shell/allow" (schema keyword type)`.
|
|
63
|
+
|
|
64
|
+
The exit code is 0 on success; 1 for a failure at run time, such as a Dex
|
|
65
|
+
server that does not start, an explicit `connectors.mcp` URL that is
|
|
66
|
+
unavailable, or a turn or session that failed during the chat; and 2 for a
|
|
67
|
+
usage or configuration error.
|
|
68
|
+
|
|
69
|
+
### Sessions and Flow types
|
|
70
|
+
|
|
71
|
+
The command registers its sessions under `<flow_type>-<suffix>`, the
|
|
72
|
+
harness's Flow type namespaced by this machine, user, configuration file, and
|
|
73
|
+
workspace. The suffix is 6 lowercase base36 characters (`0-9a-z`). Take the
|
|
74
|
+
SHA-256 of the UTF-8 text
|
|
75
|
+
`"<hostname>\n<user>\n<config identity>\n<workspace>"`: the user is the
|
|
76
|
+
numeric user ID, or the user name where there is none; the config identity is
|
|
77
|
+
the configuration file's real path, or `defaults` without a file; and the
|
|
78
|
+
workspace is its real path. Read the digest's first 8 bytes as an unsigned
|
|
79
|
+
big-endian integer, reduce it modulo 36**6, write it in base36, and left-pad
|
|
80
|
+
it with `0` to 6 characters. Tool settings do not count.
|
|
81
|
+
`namespace_session_flow_type` computes it, `session_flow_type` gathers the
|
|
82
|
+
values of this process, and every CLI core passes the cases in
|
|
83
|
+
[`agents/conformance/cli_session_flow_types.json`](../../../conformance/cli_session_flow_types.json).
|
|
84
|
+
|
|
85
|
+
Each new session records these values in its start input,
|
|
86
|
+
`AgentSessionStartInput.start_environment`, so a run shows where it was
|
|
87
|
+
started: `host_name`, `user_id`, `config_identity`, and `workspace` are the
|
|
88
|
+
hashed values, and `user_name` is the user's name, left out when the system
|
|
89
|
+
cannot tell it. They are not session settings and cannot change. Two commands that differ in any of these never take each other's
|
|
90
|
+
tool Steps, so an approved write runs only with the workspace, tools, and
|
|
91
|
+
connections it was approved under. `sessions` lists, and `--resume` continues,
|
|
92
|
+
only sessions of the same Flow type; resuming another one exits with code 2
|
|
93
|
+
and says to use the user, configuration file, and workspace that started it.
|
|
94
|
+
|
|
95
|
+
### The terminal chat
|
|
96
|
+
|
|
97
|
+
Each line is a user message. Model text streams as it arrives, and each tool
|
|
98
|
+
call prints a `[tool]` line when it starts and ends. A call that waits for
|
|
99
|
+
approval prints `[approval] <tool> <arguments>`; answer `y`, or `n` with an
|
|
100
|
+
optional reason. A question from the agent prints `[question]`; answer with the
|
|
101
|
+
text or a choice number, or an empty line to decline.
|
|
102
|
+
|
|
103
|
+
`/image <path>` attaches an image file to the next message. A leading `~` is
|
|
104
|
+
the home directory, and a relative path is relative to the current directory.
|
|
105
|
+
The command refuses a missing or special file, a file over `MAX_IMAGE_BYTES`
|
|
106
|
+
(3,750,000 bytes, checked before it reads the file), and bytes that are not
|
|
107
|
+
PNG, JPEG, GIF, or WebP; it prints `[image not attached]` with the reason. A
|
|
108
|
+
valid image is copied into the media store, which prints `[image attached]
|
|
109
|
+
chart.png (image/png, 48 KB)`, and goes with the next message. A message
|
|
110
|
+
carries at most `MAX_IMAGES_PER_MESSAGE` (20) images. When the harness's runner
|
|
111
|
+
does not implement `build_user_message_with_images`, the message is not sent:
|
|
112
|
+
the command prints `[not sent] this agent does not take images` and removes
|
|
113
|
+
the attached images.
|
|
114
|
+
|
|
115
|
+
The media store comes from `media.store`, built once when the command starts:
|
|
116
|
+
`auto` is a `LocalDirectoryMediaStore` at `~/.dex/agents/media`, and
|
|
117
|
+
`s3://<bucket>[/<prefix>]` is an `S3MediaStore`, with path-style addressing
|
|
118
|
+
when `media.endpoint` is set. Its credentials come from the environment or the
|
|
119
|
+
standard AWS chain. The S3 store needs the `dex-agents[s3]` extra in the
|
|
120
|
+
command's environment; without it, or with a store the core refuses, the
|
|
121
|
+
command exits with code 2. The connectors MCP source uploads the images of
|
|
122
|
+
connector tool results to the same store.
|
|
123
|
+
|
|
124
|
+
The first Ctrl-C during a turn cancels it (`cancel_turn`), and a second Ctrl-C
|
|
125
|
+
exits. Ctrl-C while idle, Ctrl-D, and `/exit` also exit. Exiting never ends the
|
|
126
|
+
session: it stays in Dex, and the command prints the `--resume` line that
|
|
127
|
+
continues it. New sessions are named `<name>-YYYYMMDD-HHMMSS-<6 hex digits>`.
|
|
128
|
+
|
|
129
|
+
## The Dex server
|
|
130
|
+
|
|
131
|
+
`dex.server: auto` looks at the configured ports, 127.0.0.1:8801 for Dex and
|
|
132
|
+
8802 for Dex Web by default:
|
|
133
|
+
|
|
134
|
+
1. Nothing answers on the Dex Web port: the command starts
|
|
135
|
+
`dexcli dev -open=false -dex-port <port> -web-port <port>` in the
|
|
136
|
+
background in its own session, with its output in
|
|
137
|
+
`~/.dex/agents/dexcli-dev.log`. It reads the ready output (`Dex Server:`,
|
|
138
|
+
`Dex Web:`, `Connectors MCP:`), records the process and ports in
|
|
139
|
+
`~/.dex/agents/dexcli-dev.json`, and prints how to stop it. It keeps running
|
|
140
|
+
after the command exits, so later commands and `--resume` reuse it.
|
|
141
|
+
2. Dex Web answers `/readyz` with its readiness JSON (`status: "ready"` with
|
|
142
|
+
`definitionRevision`, `source`, and `definitionCount`), and within five
|
|
143
|
+
seconds the Dex port answers as FlowService, reporting an unknown Flow as not
|
|
144
|
+
found: the command reuses it. It checks this even for the `dexcli dev` it
|
|
145
|
+
started itself. While Dex Web reports that Dex is starting, it waits up to
|
|
146
|
+
a minute.
|
|
147
|
+
3. Anything else answers: the command stops with exit code 1 instead of
|
|
148
|
+
trusting it, because that service would stand in for Dex and the connectors
|
|
149
|
+
MCP server.
|
|
150
|
+
|
|
151
|
+
The readiness JSON is checked by its values: `status` is `"ready"`,
|
|
152
|
+
`definitionRevision` and `source` are strings, and `definitionCount` is an
|
|
153
|
+
integer. These checks tell a `dexcli dev` from an unrelated service, but they
|
|
154
|
+
do not prove who owns the ports: another local user's process that binds them
|
|
155
|
+
first and answers the same way is not detected. Run on a machine you do not
|
|
156
|
+
share, or set `dex.server` and `connectors.mcp` to URLs you trust.
|
|
157
|
+
|
|
158
|
+
The command finds `dexcli` in this order: `DEXCLI`; the release it downloaded,
|
|
159
|
+
`~/.dex/bin/dexcli-<version>/dexcli`, after checking both executables against
|
|
160
|
+
the SHA-256 recorded when it was installed; the first `dexcli` on `PATH` whose
|
|
161
|
+
`--version` is at least the pinned version. Otherwise it downloads the pinned
|
|
162
|
+
release `cli-v<version>` for the platform from GitHub and checks the archive
|
|
163
|
+
against the release's `checksums.txt`. Every archive entry must be a relative
|
|
164
|
+
path without `..` and a regular file or a directory, or nothing is installed.
|
|
165
|
+
Entry names must not contain a backslash or a NUL either. Only `dexcli` and
|
|
166
|
+
`dex-server-zd` are extracted, into a private staging directory with
|
|
167
|
+
`install.json` (`version`, `archive`, `archiveSha256`, and the `sha256` of
|
|
168
|
+
each executable), which then becomes `~/.dex/bin/dexcli-<version>` in one
|
|
169
|
+
rename. A downloaded copy that changed later stops the command; delete its
|
|
170
|
+
directory to download it again. A cached executable must also still be
|
|
171
|
+
executable, and a `dexcli` on `PATH` counts only when `--version` exits with
|
|
172
|
+
code 0. When the command downloads, `DEX_AGENTS_DEXCLI_DOWNLOAD_URL`, and every
|
|
173
|
+
redirect of the checksums and the archive, must be an https URL, or http on
|
|
174
|
+
`127.0.0.1`, `localhost`, or `[::1]`.
|
|
175
|
+
|
|
176
|
+
Another `dex.server` value is a server URL; the command connects to it and
|
|
177
|
+
starts nothing.
|
|
178
|
+
|
|
179
|
+
| Environment variable | Meaning |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `DEXCLI` | The `dexcli` executable to run. |
|
|
182
|
+
| `DEX_AGENTS_DEX_PORT`, `DEX_AGENTS_WEB_PORT` | The ports of the `dexcli dev` that `auto` reuses or starts. |
|
|
183
|
+
| `DEX_AGENTS_DEXCLI_DOWNLOAD_URL` | The base URL of `dexcli` release downloads, such as a mirror. |
|
|
184
|
+
|
|
185
|
+
## Connector tools
|
|
186
|
+
|
|
187
|
+
`connectors.mcp: auto` uses the connectors MCP server that `dexcli dev` serves
|
|
188
|
+
at `<Dex Web URL>/mcp`. When the command started `dexcli dev`, it takes the URL
|
|
189
|
+
from the `Connectors MCP:` line of its output. With an explicit `dex.server`,
|
|
190
|
+
`auto` has no server, so connectors are off with a hint to set the URL.
|
|
191
|
+
|
|
192
|
+
`ConnectorsMcpToolSource` offers the server's tools through `McpToolSource`,
|
|
193
|
+
so each call is a Dex tool Step with the server's effect class, approval,
|
|
194
|
+
idempotency key, and tasks. The agent sees each tool by its MCP name, such as
|
|
195
|
+
`slack_post_thread_reply`, without a server prefix, and `approval.always`
|
|
196
|
+
names it the same way.
|
|
197
|
+
|
|
198
|
+
`connectors.enabled` filters by connector, connection, and operation. The
|
|
199
|
+
CLI core reads them from the top-level `_meta` keys of each tool, which
|
|
200
|
+
`CONNECTOR_TOOL_META_KEYS` holds: `io.superdurable.connectors/connector`,
|
|
201
|
+
`io.superdurable.connectors/connection`, and
|
|
202
|
+
`io.superdurable.connectors/operation` (the operationId) or
|
|
203
|
+
`io.superdurable.connectors/operationName` (its snake-case name); a `tools`
|
|
204
|
+
entry matches either. The CLI core never parses tool names. While
|
|
205
|
+
`connectors.enabled` is set, a tool without the connector and connection keys
|
|
206
|
+
is hidden, and an entry that lists operations matches only tools with both
|
|
207
|
+
`operation` and `operationName`.
|
|
208
|
+
|
|
209
|
+
Each call is checked against its turn again when it is built: a tool whose
|
|
210
|
+
connection or operation the turn does not enable, such as from a listing a
|
|
211
|
+
runner kept, becomes a call that its tool Step refuses with
|
|
212
|
+
`tool <name> is switched off for this session`. The local tools do the same
|
|
213
|
+
for a switched-off group.
|
|
214
|
+
|
|
215
|
+
The tool Step authorizes the call once more, on the Worker that runs it, with
|
|
216
|
+
that Worker's own configuration file narrowed by the session settings recorded
|
|
217
|
+
for the call's turn. A call may run on another Worker than the one that built
|
|
218
|
+
it, such as a command started later with the same file after the file was
|
|
219
|
+
edited. The tool's group, connection, and operation must be enabled there too,
|
|
220
|
+
and a shell command that was built without approval must still be in that
|
|
221
|
+
allow list; otherwise the call fails and nothing runs.
|
|
222
|
+
|
|
223
|
+
A connector tool name can move to another connection between the model call
|
|
224
|
+
and the tool Step, such as when a second connection of the connector is
|
|
225
|
+
authorized. So before it sends a call, the connector source lists the server
|
|
226
|
+
again and requires the tool's current connector, connection, and operation to
|
|
227
|
+
equal the ones the call was made for; otherwise the call fails and nothing is
|
|
228
|
+
sent. It also sends them in the `tools/call` request's `_meta` as
|
|
229
|
+
`io.superdurable.connectors/expectedConnector`, `expectedConnection`, and
|
|
230
|
+
`expectedOperation`. The client-side check only narrows the window: a name can
|
|
231
|
+
still move between the check and the call. The connectors MCP server enforces
|
|
232
|
+
the expected identity in the same step that runs the operation from its release
|
|
233
|
+
that supports these keys; with an older server, the check is the only guard.
|
|
234
|
+
The connection and operation authorization runs on every attempt. Only the
|
|
235
|
+
listing check is skipped, and only when the core says the attempt resumes a
|
|
236
|
+
task an earlier attempt started and can never send `tools/call` again, so the
|
|
237
|
+
task is recovered even while the server cannot be listed. An attempt that may
|
|
238
|
+
send again lists the server first and fails when it cannot.
|
|
239
|
+
|
|
240
|
+
At startup, before the harness builds its Flow, the command reads the
|
|
241
|
+
server's tool catalog. When the server answers, it then lists the tools the
|
|
242
|
+
configuration file enables, which is one MCP `tools/list` and one more catalog
|
|
243
|
+
read, and caches them for the harness:
|
|
244
|
+
|
|
245
|
+
- An `auto` server that does not answer, such as one answering 503 while it
|
|
246
|
+
starts or an older `dexcli` without `/mcp`, gives a warning with the reason.
|
|
247
|
+
The command runs without connector tools and lists the server again in every
|
|
248
|
+
model call, printing `Connector tools are unavailable for now` and
|
|
249
|
+
`Connector tools are available again` when that changes. An explicit `connectors.mcp` URL
|
|
250
|
+
that does not answer stops the command with exit code 1.
|
|
251
|
+
- A harness whose runner fixes its tools when it is built, such as the OpenAI
|
|
252
|
+
Agents SDK command, declares `is_tool_set_fixed_at_build = True`. It offers
|
|
253
|
+
only the connector tools of the startup listing; session settings still
|
|
254
|
+
narrow them each turn. When the server does not answer at startup, the
|
|
255
|
+
warning says the run has no connector tools and to restart the command once
|
|
256
|
+
the server answers. A connection authorized while the command runs also
|
|
257
|
+
needs a restart.
|
|
258
|
+
- An entry of `connectors.enabled` with no tool on the server has no
|
|
259
|
+
authorized connection. The command prints a hint with the Dex Web
|
|
260
|
+
Connectors page, `<Dex Web URL>/v1/connectors`, and opens it only in an
|
|
261
|
+
interactive terminal.
|
|
262
|
+
|
|
263
|
+
## Local tools
|
|
264
|
+
|
|
265
|
+
The local tool source offers these tools in their own Dex tool Steps. Paths
|
|
266
|
+
are relative to `workspace`, or absolute inside it.
|
|
267
|
+
|
|
268
|
+
The file tools resolve a path's symbolic links one hop at a time, reading each
|
|
269
|
+
link relative to its directory's descriptor, and every hop must stay inside the
|
|
270
|
+
workspace, so a link such as `node_modules/.bin/tool` that points inside works
|
|
271
|
+
and one that points outside fails the call. `..` may not climb above the
|
|
272
|
+
workspace. An absolute path may spell the workspace in another case on a
|
|
273
|
+
case-insensitive disk. The resolved path is then opened from the workspace one
|
|
274
|
+
directory at a time, each component relative to its parent with `O_NOFOLLOW`,
|
|
275
|
+
so a directory swapped for a link while a tool runs fails the call. Each opened file or directory is checked against
|
|
276
|
+
the workspace again by the path the kernel reports for it, which also holds on
|
|
277
|
+
a case-insensitive disk. A file with more than one hard link is refused,
|
|
278
|
+
because the other link may be outside; `search_files` skips such files and
|
|
279
|
+
checks every file it reads the same way. `write_file` and `edit_file` create a
|
|
280
|
+
temporary file with `O_CREAT|O_EXCL|O_NOFOLLOW` in the checked directory and
|
|
281
|
+
rename it over the name, all through the directory's descriptor, so the write
|
|
282
|
+
lands in the directory that was checked. They refuse a symbolic link at the
|
|
283
|
+
name, check the directory again right before the rename, and afterwards check
|
|
284
|
+
that the name holds exactly the file they wrote. `edit_file` changes files of
|
|
285
|
+
at most 4 MiB (4194304 bytes) and refuses larger ones. Right before the rename
|
|
286
|
+
it checks that the name is the same file, with the same size, modification
|
|
287
|
+
time, and content, as when it read it; otherwise it writes nothing and says to
|
|
288
|
+
read the file again and retry the edit.
|
|
289
|
+
|
|
290
|
+
These checks have limits:
|
|
291
|
+
|
|
292
|
+
- Another process that moves a directory out of the workspace after the last
|
|
293
|
+
check and before the rename can make a write land outside. The tool then
|
|
294
|
+
reports the error, but the file is written. Only OS-level isolation, such as
|
|
295
|
+
a container, closes this; writes always wait for approval.
|
|
296
|
+
- They cover the file tools only. An approved shell command runs with your
|
|
297
|
+
permissions and can change anything, including the workspace and files
|
|
298
|
+
outside it.
|
|
299
|
+
|
|
300
|
+
| Group | Tool | Effect | Approval |
|
|
301
|
+
| --- | --- | --- | --- |
|
|
302
|
+
| `files` | `read_file`, `list_directory`, `search_files` | read | No |
|
|
303
|
+
| `files` | `write_file` | idempotent write | Every call |
|
|
304
|
+
| `files` | `edit_file` | non-idempotent write | Every call |
|
|
305
|
+
| `shell` | `run_shell_command` | non-idempotent write | Unless the whole command text is in `local_tools.shell.allow` |
|
|
306
|
+
| `web_fetch` | `fetch_web_page` | read | No |
|
|
307
|
+
| `browser` | `browser_navigate`, `browser_snapshot` | read | No |
|
|
308
|
+
| `browser` | `browser_click`, `browser_fill`, `browser_submit_form` | non-idempotent write | Every call |
|
|
309
|
+
|
|
310
|
+
- A shell command runs with `/bin/sh -c` in the workspace, in its own process
|
|
311
|
+
group, so Ctrl-C never reaches it. It times out after 120 seconds by default
|
|
312
|
+
and 600 at most. It is a single-attempt write: a Worker that dies during it
|
|
313
|
+
records `outcome_unknown` and never runs it again.
|
|
314
|
+
- The browser is headless: Playwright's own Chromium when it is installed,
|
|
315
|
+
else the system Chrome, else Chromium that Playwright downloads the first
|
|
316
|
+
time a browser tool runs. `browser_navigate` opens only `http` and `https`
|
|
317
|
+
URLs, so the browser never reads local files. Each session has one page in
|
|
318
|
+
the Worker process. Every tool returns the page's URL, title, and
|
|
319
|
+
accessibility snapshot as text;
|
|
320
|
+
the browser tools take no screenshots. After a Worker restart a
|
|
321
|
+
session has no page until the agent navigates again.
|
|
322
|
+
- `approval.always` adds approval to any tool by name.
|
|
323
|
+
- Each tool's effect and approval are a floor. An explicit `ToolPolicy` in
|
|
324
|
+
the session options may make a tool more cautious, such as a `read` that
|
|
325
|
+
must be approved, but never less: a write stays a write, and
|
|
326
|
+
`requires_approval=False` never removes a tool's own approval.
|
|
327
|
+
|
|
328
|
+
## Session settings
|
|
329
|
+
|
|
330
|
+
Every model call resolves its turn's session settings with the shared rules
|
|
331
|
+
in [`agents/cli`](../../README.md#session-settings) before the harness runs.
|
|
332
|
+
Settings that fail the schema or widen the file fail the model call with
|
|
333
|
+
`ConfigError`, whose message is `<code> at "<pointer>"`. The session retries
|
|
334
|
+
it and then fails the turn with that error. Ignored top-level keys print
|
|
335
|
+
`Session settings keys ignored: <pointers>` to standard error, once per
|
|
336
|
+
distinct settings. While the harness's runner runs, `current_turn_config()`
|
|
337
|
+
returns the turn's configuration:
|
|
338
|
+
|
|
339
|
+
- the CLI core's tool sources offer only the connectors, operations, and local
|
|
340
|
+
tool groups the turn enables, and outside a model call they offer nothing;
|
|
341
|
+
- a shell command's allow list and `approval.always` come from the turn;
|
|
342
|
+
- the harness reads the turn's model and instructions with
|
|
343
|
+
`HarnessContext.resolve_turn`.
|
|
344
|
+
|
|
345
|
+
## Writing a harness command
|
|
346
|
+
|
|
347
|
+
A harness package implements `AgentHarness` and calls `run_agent_cli` from its
|
|
348
|
+
console script:
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
import dex_agents_cli_core # first: before anything that imports gRPC
|
|
352
|
+
|
|
353
|
+
from dex_agents_cli_core import AgentHarness, HarnessContext, LocalToolGroup, run_agent_cli
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class MyHarness:
|
|
357
|
+
name = "my-framework" # dex-agents-my-framework, ~/.dex/agents/my-framework.yaml
|
|
358
|
+
config_section = "builtin" # the configuration file's harness section
|
|
359
|
+
flow_type = "DexAgentsMyFrameworkCliSession" # keep it stable: sessions resume by it
|
|
360
|
+
default_model = "openai/gpt-5-mini" # None: the file must set model
|
|
361
|
+
replaced_local_tools: frozenset[LocalToolGroup] = frozenset()
|
|
362
|
+
|
|
363
|
+
def create_session_flow(self, context: HarnessContext) -> AgentSessionFlow:
|
|
364
|
+
...
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def run() -> None:
|
|
368
|
+
raise SystemExit(run_agent_cli(MyHarness()))
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Import `dex_agents_cli_core` before any module that imports gRPC, such as `dex` or
|
|
372
|
+
a `dex_agents` adapter. The package turns off gRPC's fork handlers, which only
|
|
373
|
+
works before gRPC loads, so they do not print into every shell command.
|
|
374
|
+
|
|
375
|
+
### `AgentHarness`
|
|
376
|
+
|
|
377
|
+
| Member | Meaning |
|
|
378
|
+
| --- | --- |
|
|
379
|
+
| `name: str` | Command `dex-agents-<name>` and file `~/.dex/agents/<name>.yaml`. |
|
|
380
|
+
| `config_section: str` | The harness section of the file, such as `claude`. |
|
|
381
|
+
| `flow_type: str` | The Flow type of the sessions. |
|
|
382
|
+
| `default_model: str \| None` | The model when the file sets none. |
|
|
383
|
+
| `replaced_local_tools: frozenset[LocalToolGroup]` | Local tool groups the harness provides itself. |
|
|
384
|
+
| `create_session_flow(context: HarnessContext) -> AgentSessionFlow` | Builds the Flow once per command run. |
|
|
385
|
+
|
|
386
|
+
`LocalToolGroup` is `FILES`, `SHELL`, `WEB_FETCH`, or `BROWSER`. The CLI core
|
|
387
|
+
leaves a replaced group out of `context.tool_sources`. The harness then
|
|
388
|
+
switches its own tools on from `context.config.local_tools` and gives them the
|
|
389
|
+
same policy: reads run at once, writes and shell commands wait for approval
|
|
390
|
+
unless the shell allow list has the exact command, and the working directory
|
|
391
|
+
is `context.workspace`. The Claude Agent SDK command, for example, replaces
|
|
392
|
+
`FILES`, `SHELL`, and `WEB_FETCH` with Claude Code's Read, Grep, Glob, Write,
|
|
393
|
+
Edit, Bash, and WebFetch, and keeps the CLI core's browser tools. Another
|
|
394
|
+
harness may replace only `FILES` and `SHELL`.
|
|
395
|
+
|
|
396
|
+
### `HarnessContext`
|
|
397
|
+
|
|
398
|
+
| Member | Meaning |
|
|
399
|
+
| --- | --- |
|
|
400
|
+
| `flow_type: str` | The harness's Flow type. |
|
|
401
|
+
| `config: EffectiveConfig` | The file's effective configuration, with `--model` applied. It bounds every turn. |
|
|
402
|
+
| `workspace: Path` | The absolute workspace, which exists. |
|
|
403
|
+
| `instructions: str \| None` | `system_prompt`, else the text of `system_prompt_file`, else `None` for the harness default. |
|
|
404
|
+
| `harness_options: Mapping[str, Any]` | The harness's own section, `{}` by default. |
|
|
405
|
+
| `tool_sources: Sequence[ToolSource]` | The connector and local tool sources, which apply each turn's settings themselves. Offer all of them. |
|
|
406
|
+
| `media_store: MediaStore \| None` | The media store of `media.store`, holding the images of `/image` and of connector tool results. Pass it to an adapter that takes one, such as `media_store=context.media_store`. The CLI core always sets it; it is `None` only in a context a test builds without one. |
|
|
407
|
+
| `resolve_turn(session_settings) -> ResolvedTurn` | A turn's narrowed configuration with its `model` and `instructions`. Raises `ConfigError` for rejected settings. |
|
|
408
|
+
| `resolve_tool_step_permissions(call=None) -> ResolvedTurn` | The permissions a replacement tool's own Step applies: this Worker's file narrowed by the session settings recorded for the call's turn. Pass the `DeferredToolExecution` from a runner, or nothing from a built-in loop function tool. Raises `RuntimeError` without a call outside a tool Step and a model call. |
|
|
409
|
+
| `current_turn() -> ResolvedTurn` | The turn whose model call or tool Step is running, or the file's configuration outside both. In a tool Step it is the configuration of the turn that made the call, from the settings the session recorded for that turn, so a replacement tool sees the turn's switches, shell allow list, and `approval.always`. |
|
|
410
|
+
|
|
411
|
+
The returned Flow must use `context.flow_type`, which is namespaced as
|
|
412
|
+
[Sessions and Flow types](#sessions-and-flow-types) describes. The CLI core wraps its runner so
|
|
413
|
+
that each model call resolves the turn first, keeping the runner's optional
|
|
414
|
+
protocols, such as `build_user_message_with_images`, and registers the result.
|
|
415
|
+
|
|
416
|
+
A harness that replaces a local tool group gives its own tools the CLI core's
|
|
417
|
+
rules with these helpers:
|
|
418
|
+
|
|
419
|
+
| Helper | Use |
|
|
420
|
+
| --- | --- |
|
|
421
|
+
| `is_local_tool_group_enabled(turn, group) -> bool` | Offer the group's tools only while the turn switches it on. |
|
|
422
|
+
| `is_shell_command_approval_required(turn, command) -> bool` | Approval is required unless the turn's allow list has the command's whole text. |
|
|
423
|
+
| `resolve_turn_tool_policy(turn_context, tool_name, effect, requires_approval, turn) -> ToolPolicy` | The call's policy: the tool's effect and approval as a floor, raised by an explicit session policy, and `approval.always`. |
|
|
424
|
+
|
|
425
|
+
A runner-based adapter passes the sources to its flow builder. Where its hook
|
|
426
|
+
receives no session settings, it reads the running turn with
|
|
427
|
+
`current_turn_config()`, which every model call sets:
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
def create_session_flow(self, context: HarnessContext) -> AgentSessionFlow:
|
|
431
|
+
def options_factory(options_context: ClaudeAgentOptionsContext) -> ClaudeAgentOptions:
|
|
432
|
+
turn = current_turn_config()
|
|
433
|
+
assert turn is not None, "the CLI core sets the turn around every model call"
|
|
434
|
+
return ClaudeAgentOptions(
|
|
435
|
+
model=turn.model, system_prompt=turn.instructions, cwd=str(context.workspace)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return claude_agent_session_flow(
|
|
439
|
+
options_factory, flow_type=context.flow_type, tool_sources=context.tool_sources
|
|
440
|
+
)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
A built-in loop harness puts the sources in `AgentDefinition.tool_sources` and
|
|
444
|
+
resolves the model and instructions from the session settings it is given:
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
def create_session_flow(self, context: HarnessContext) -> AgentSessionFlow:
|
|
448
|
+
def resolve_model(settings: Mapping[str, Any]) -> ModelClient:
|
|
449
|
+
return LiteLLMModelClient(context.resolve_turn(settings).model)
|
|
450
|
+
|
|
451
|
+
definition = AgentDefinition(
|
|
452
|
+
name="assistant",
|
|
453
|
+
instructions=lambda instruction: context.resolve_turn(
|
|
454
|
+
instruction.session_settings
|
|
455
|
+
).instructions or "You are a helpful assistant.",
|
|
456
|
+
model=resolve_model({}),
|
|
457
|
+
resolve_model=resolve_model,
|
|
458
|
+
tool_sources=list(context.tool_sources),
|
|
459
|
+
)
|
|
460
|
+
return builtin_agent_session_flow(definition, flow_type=context.flow_type)
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
## Development
|
|
464
|
+
|
|
465
|
+
Develop with uv 0.12.2. The package resolves `dex-agents` from
|
|
466
|
+
`../../../python/core` by path. `ci.sh` installs the locked dependencies,
|
|
467
|
+
checks lint and types, and runs the tests against a private `dexcli dev`:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
cd agents/cli/python/core
|
|
471
|
+
DEXCLI=/path/to/dexcli ./ci.sh
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
The tests run the commands as processes, in a pseudo-terminal where a test
|
|
475
|
+
needs one, with a scripted built-in loop harness (`tests/scripted_harness.py`), a
|
|
476
|
+
fake connectors MCP server, and a private home directory each. They also run
|
|
477
|
+
every case of `agents/conformance/cli_config.json`. The browser test downloads
|
|
478
|
+
Playwright's headless Chromium when it is missing and skips only when it
|
|
479
|
+
cannot start. Agents CI runs `ci.sh` for every `agents/cli/<language>/<package>`.
|