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,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>`.
@@ -0,0 +1,26 @@
1
+ dex_agents_cli_core/__init__.py,sha256=y9fRB2uhmWxsDSwJYqBFJnGXxH0QEyRX_blsauELXo4,2825
2
+ dex_agents_cli_core/chat.py,sha256=GWYz9a6SeUIe_zSgs9KncCSBBqC-_yB2liEyQPKKBtY,21499
3
+ dex_agents_cli_core/cli.py,sha256=b0KSSbQa64RZqnu46L3NlBbPZFpCK84aDwlCV6YD_nQ,25635
4
+ dex_agents_cli_core/config.py,sha256=6wgZGPjC6FRN7p3FjPBMeS5Ciqg1y6JWACnPWhvKsdM,23326
5
+ dex_agents_cli_core/config_file.py,sha256=50gDl8xt9v4apta-muIadk83SU8JgzG4bgJaoU3xfjc,7045
6
+ dex_agents_cli_core/connectors.py,sha256=pEpaNWJPQet2b43MSdx6nv9YIzDtdBeYpEFL_7w-ARU,20750
7
+ dex_agents_cli_core/dex_server.py,sha256=_5Ncvndu-DcC5TxZt--6toV5HYrrRJWE7MTcL72Jpuo,12719
8
+ dex_agents_cli_core/dexcli_install.py,sha256=68z1-0zZlzox1juuJ6sHMHbu3Bjo3uftv3ac-1v_1fQ,12402
9
+ dex_agents_cli_core/environment.py,sha256=xVjoARPZS_jeyyE7_vln12KUxU0u3x0oh4RcKfVZkgA,5197
10
+ dex_agents_cli_core/harness.py,sha256=INtB3AviKcSkuAwd05mGXC_2ynNkmT_oegsgHgPxQoo,17742
11
+ dex_agents_cli_core/local_tools/__init__.py,sha256=MFAatBbp7bw-e_U_8qPwMHUEnPboiQlrpOwzqtis2sA,770
12
+ dex_agents_cli_core/local_tools/browser.py,sha256=cJnFZ5tiLisLlkrPvvF_fg3WoNCkV026uXpWZWA5Rwk,9154
13
+ dex_agents_cli_core/local_tools/files.py,sha256=GjkwefbhrMSL7D7gtcfPcoBNnhJihusfxoMi7KgB8VM,21722
14
+ dex_agents_cli_core/local_tools/shell.py,sha256=Cgp-IxpxhYTLfHP-iCquXX6lWUX-2KI23Z7MSHeR0Sk,2883
15
+ dex_agents_cli_core/local_tools/source.py,sha256=1jGZsEPoXQnm1N3pjYl2kBTl0LgIg7KiFIZ4iGyl_fc,17798
16
+ dex_agents_cli_core/local_tools/web.py,sha256=tRjQZhfPwg-0u4g6TPINaPpyjTpfgY4uRIepiQW3IuY,3158
17
+ dex_agents_cli_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ dex_agents_cli_core/schemas/__init__.py,sha256=VvWkhHpxEr3WT3TiS_8uaVoaDw-0DRp9qJj1x5s_svc,363
19
+ dex_agents_cli_core/schemas/config.schema.json,sha256=OKVn8-0vLHd1c6Y9W47kZnFFIJNPkTbTHfIbdEigqU0,14001
20
+ dex_agents_cli_core/schemas/session-settings.schema.json,sha256=vLNqVhbYdVLlGUKl3M2CV_fmrKKLP9MLtO1Ls6Nd3_w,6620
21
+ dex_agents_cli_core/tool_policy.py,sha256=UK-2FMK-xFdEDYc_wyTqmaOV3Z207pUPuksJVAcY12s,6561
22
+ dex_agents_cli_core/turns.py,sha256=M4euPcnuo0AzwZA0ee-sGmGK_KI5FO2QG8x-nhJDYQA,8538
23
+ dex_agents_cli_core-0.5.0.dist-info/licenses/LICENSE,sha256=0AJCV4ky-vrSnT08IIdYpyiFPPoF2UrtKsBAvuvt1G4,4220
24
+ dex_agents_cli_core-0.5.0.dist-info/WHEEL,sha256=q9Wah2_CgP24mRNSlTyb0F8Yrbz55SzVl8eHLkZ_wvE,80
25
+ dex_agents_cli_core-0.5.0.dist-info/METADATA,sha256=nVyXRC9a0X6nwgOHS77kCATQxhLiDTdqHQs72VU7RVI,27272
26
+ dex_agents_cli_core-0.5.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.