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,315 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Dex Agents CLI configuration",
4
+ "description": "The configuration file of a Dex Agents CLI command, ~/.dex/agents/<harness>.yaml by default. It is YAML 1.2; this schema checks the parsed value. Every harness command in Python and TypeScript reads the same common fields and its own harness section. Do not put secrets in this file: provider keys come from the environment, and connector credentials stay in the connections that Dex Web authorizes. The schema cannot enforce this inside the open harness sections. Omit a field to take its default; null is never a valid value. The settings of one session may change model and system_prompt and may only narrow the rest; see session-settings.schema.json.",
5
+ "$comment": "Patterns end with (?![\\s\\S]) rather than $, because Python's re lets $ match before a trailing newline and ECMAScript does not. The not schema repeats properties so that Ajv's strictRequired mode accepts it.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "model": {
10
+ "$ref": "#/$defs/model",
11
+ "description": "The model the agent calls, in the harness's own format, such as claude-opus-5-5 or a <provider>/<model> ID. Omitted: the harness command's default model. A harness command without a default model refuses to run until this is set."
12
+ },
13
+ "system_prompt": {
14
+ "type": "string",
15
+ "description": "The agent's instructions. Omitted, together with system_prompt_file: the harness's default instructions. Mutually exclusive with system_prompt_file."
16
+ },
17
+ "system_prompt_file": {
18
+ "$ref": "#/$defs/single_line_text",
19
+ "description": "A UTF-8 text file whose content is the agent's instructions, read when the command starts. A leading ~ means the home directory; a relative path is relative to the directory of this configuration file. Mutually exclusive with system_prompt."
20
+ },
21
+ "dex": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "description": "The Dex server the Worker connects to.",
25
+ "default": {
26
+ "server": "auto"
27
+ },
28
+ "properties": {
29
+ "server": {
30
+ "anyOf": [
31
+ {
32
+ "const": "auto"
33
+ },
34
+ {
35
+ "type": "string",
36
+ "pattern": "^https?://(?:(?:[A-Za-z0-9-]+\\.)*[A-Za-z][A-Za-z0-9-]*|[0-9]{1,3}(?:\\.[0-9]{1,3}){3}|\\[[0-9A-Fa-f:.]+\\])(?::[0-9]{1,5})?(?:/[!-~]*)?(?![\\s\\S])"
37
+ }
38
+ ],
39
+ "default": "auto",
40
+ "description": "auto: reuse the running dexcli dev server of this machine, or start one. Otherwise the http or https URL of a Dex server, without user info. Its host is a DNS name whose last label starts with a letter, a dotted IPv4 address, or a bracketed IPv6 address. After the schema, the CLI core also checks that each IPv4 part is 0 or a decimal from 1 to 255 without a leading zero, that an IPv6 address is valid, that no DNS label starts with xn--, and that the port is at most 65535."
41
+ }
42
+ }
43
+ },
44
+ "connectors": {
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "description": "The connector tools the agent may call.",
48
+ "default": {
49
+ "mcp": "auto"
50
+ },
51
+ "properties": {
52
+ "mcp": {
53
+ "anyOf": [
54
+ {
55
+ "const": "auto"
56
+ },
57
+ {
58
+ "type": "string",
59
+ "pattern": "^https?://(?:127\\.0\\.0\\.1|localhost|\\[::1\\])(?::[0-9]{1,5})?(?:/[!-~]*)?(?![\\s\\S])"
60
+ }
61
+ ],
62
+ "default": "auto",
63
+ "description": "The connectors MCP server. auto: the server that dexcli dev provides. Otherwise its http or https URL on a loopback host: 127.0.0.1, localhost, or [::1], without user info. After the schema, the CLI core also checks that the port is at most 65535."
64
+ },
65
+ "enabled": {
66
+ "$ref": "#/$defs/connector_entries",
67
+ "description": "The connections whose operations the agent may call. Omitted: every authorized connection, with every operation its connector offers to agents. An empty list: no connector tools. An entry names a connector, which covers all of its connections, or connector/connection. A single-key map {\"connector/connection\": {\"tools\": [...]}} or {\"connector\": {\"tools\": [...]}} also limits the operations. Two entries must not cover the same connection."
68
+ }
69
+ }
70
+ },
71
+ "approval": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "description": "Approval beyond what each tool already requires.",
75
+ "default": {
76
+ "always": []
77
+ },
78
+ "properties": {
79
+ "always": {
80
+ "$ref": "#/$defs/tool_names",
81
+ "default": [],
82
+ "description": "Tool names, as the agent sees them, whose every call waits for approval, in addition to the tools whose connector or local tool policy asks for it."
83
+ }
84
+ }
85
+ },
86
+ "workspace": {
87
+ "$ref": "#/$defs/single_line_text",
88
+ "default": "~/dex-agent-workspace",
89
+ "description": "The only directory the local file tools may read and write, and the working directory of shell commands. A leading ~ means the home directory; a relative path is relative to the directory of this configuration file."
90
+ },
91
+ "local_tools": {
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "description": "Tools that act on this machine. Reads run without approval; writes, browser clicks, form fills, and submits wait for approval; each shell command waits for approval unless it matches the allow list.",
95
+ "default": {
96
+ "files": true,
97
+ "shell": {
98
+ "enabled": true,
99
+ "allow": []
100
+ },
101
+ "web_fetch": true,
102
+ "browser": true
103
+ },
104
+ "properties": {
105
+ "files": {
106
+ "type": "boolean",
107
+ "default": true,
108
+ "description": "Read, write, list, and search files inside workspace."
109
+ },
110
+ "shell": {
111
+ "type": "object",
112
+ "additionalProperties": false,
113
+ "description": "Run shell commands in workspace.",
114
+ "default": {
115
+ "enabled": true,
116
+ "allow": []
117
+ },
118
+ "properties": {
119
+ "enabled": {
120
+ "type": "boolean",
121
+ "default": true,
122
+ "description": "Whether the agent may run shell commands at all."
123
+ },
124
+ "allow": {
125
+ "$ref": "#/$defs/shell_commands",
126
+ "default": [],
127
+ "description": "Commands that run without approval. A command matches only when its whole text equals an entry exactly; every other command waits for approval."
128
+ }
129
+ }
130
+ },
131
+ "web_fetch": {
132
+ "type": "boolean",
133
+ "default": true,
134
+ "description": "Fetch web pages."
135
+ },
136
+ "browser": {
137
+ "type": "boolean",
138
+ "default": true,
139
+ "description": "Drive a local browser. The browser is downloaded the first time the agent uses it."
140
+ }
141
+ }
142
+ },
143
+ "media": {
144
+ "type": "object",
145
+ "additionalProperties": false,
146
+ "description": "Where the images of a session are stored: the images attached with /image and the images that connector tools return. Session state keeps only a reference to each image; the bytes stay in this store, one copy per distinct image, named by its SHA-256. Every Worker of a session must reach the same store. Sessions cannot change it: session settings ignore a media key with a warning. Credentials never go in this file: an s3 store reads them from the environment or the standard AWS chain.",
147
+ "default": {
148
+ "store": "auto"
149
+ },
150
+ "properties": {
151
+ "store": {
152
+ "anyOf": [
153
+ {
154
+ "const": "auto"
155
+ },
156
+ {
157
+ "type": "string",
158
+ "pattern": "^s3://[a-z0-9][a-z0-9.-]{1,61}[a-z0-9](?:/[!-~]*)?(?![\\s\\S])"
159
+ }
160
+ ],
161
+ "default": "auto",
162
+ "description": "auto: a directory on this machine, ~/.dex/agents/media, which keeps its own copy of each image, never a link to the original file. Otherwise s3://<bucket> or s3://<bucket>/<prefix> for an S3 bucket, which Workers on other machines can reach too. The bucket name has 3 to 63 lowercase letters, digits, dots, or hyphens and starts and ends with a letter or digit; the prefix is printable ASCII without spaces. An s3 store needs the S3 support of the agents library: the dex-agents[s3] extra in Python, the @aws-sdk/client-s3 package in TypeScript."
163
+ },
164
+ "endpoint": {
165
+ "type": "string",
166
+ "pattern": "^https?://(?:(?:[A-Za-z0-9-]+\\.)*[A-Za-z][A-Za-z0-9-]*|[0-9]{1,3}(?:\\.[0-9]{1,3}){3}|\\[[0-9A-Fa-f:.]+\\])(?::[0-9]{1,5})?(?:/[!-~]*)?(?![\\s\\S])",
167
+ "description": "The http or https URL of an S3-compatible service, such as http://127.0.0.1:9000; omitted, AWS S3. Only valid with an s3 store. With an endpoint, the bucket is addressed in the URL path, as most S3-compatible services need. The host rules and the CLI core checks are those of dex.server."
168
+ }
169
+ },
170
+ "not": {
171
+ "type": "object",
172
+ "properties": {
173
+ "store": {
174
+ "const": "auto"
175
+ },
176
+ "endpoint": true
177
+ },
178
+ "required": [
179
+ "endpoint"
180
+ ]
181
+ }
182
+ },
183
+ "claude": {
184
+ "$ref": "#/$defs/harness_options",
185
+ "description": "Options of the Claude Agent SDK command. Omitted: its defaults."
186
+ },
187
+ "openai": {
188
+ "$ref": "#/$defs/harness_options",
189
+ "description": "Options of the OpenAI Agents SDK command. Omitted: its defaults."
190
+ },
191
+ "pydantic_ai": {
192
+ "$ref": "#/$defs/harness_options",
193
+ "description": "Options of the Pydantic AI command. Omitted: its defaults."
194
+ },
195
+ "deepagents": {
196
+ "$ref": "#/$defs/harness_options",
197
+ "description": "Options of the Deep Agents command. Omitted: its defaults."
198
+ },
199
+ "google_adk": {
200
+ "$ref": "#/$defs/harness_options",
201
+ "description": "Options of the Google ADK command. Omitted: its defaults."
202
+ },
203
+ "vercel_ai": {
204
+ "$ref": "#/$defs/harness_options",
205
+ "description": "Options of the Vercel AI SDK command. Omitted: its defaults."
206
+ },
207
+ "mastra": {
208
+ "$ref": "#/$defs/harness_options",
209
+ "description": "Options of the Mastra command. Omitted: its defaults."
210
+ },
211
+ "pi": {
212
+ "$ref": "#/$defs/harness_options",
213
+ "description": "Options of the Pi command. Omitted: its defaults."
214
+ },
215
+ "builtin": {
216
+ "$ref": "#/$defs/harness_options",
217
+ "description": "Options of the built-in agent loop command. Omitted: its defaults."
218
+ }
219
+ },
220
+ "not": {
221
+ "type": "object",
222
+ "properties": {
223
+ "system_prompt": true,
224
+ "system_prompt_file": true
225
+ },
226
+ "required": [
227
+ "system_prompt",
228
+ "system_prompt_file"
229
+ ]
230
+ },
231
+ "$defs": {
232
+ "model": {
233
+ "type": "string",
234
+ "minLength": 1,
235
+ "description": "A model ID in the harness's own format, such as claude-opus-5-5 or openai/gpt-5-mini. Any non-empty string; the harness checks it."
236
+ },
237
+ "single_line_text": {
238
+ "type": "string",
239
+ "pattern": "^[^\\x00-\\x1f\\x7f]+(?![\\s\\S])",
240
+ "description": "Non-empty text without control characters."
241
+ },
242
+ "tool_name": {
243
+ "type": "string",
244
+ "pattern": "^[A-Za-z0-9_.-]{1,128}(?![\\s\\S])",
245
+ "description": "A tool name: 1 to 128 ASCII letters, digits, underscores, hyphens, or dots."
246
+ },
247
+ "tool_names": {
248
+ "type": "array",
249
+ "uniqueItems": true,
250
+ "items": {
251
+ "$ref": "#/$defs/tool_name"
252
+ }
253
+ },
254
+ "operation_name": {
255
+ "type": "string",
256
+ "pattern": "^[A-Za-z][A-Za-z0-9_]{0,127}(?![\\s\\S])",
257
+ "description": "A connector operation: its snake-case name, such as get_message, or its operationId, such as getMessage. Both spellings name the same operation: an operationId is compared after the connectors library's lowerCamelToSnake conversion, which keeps acronym runs together, so listStoryIDs is list_story_ids and getHTTPResponse is get_http_response."
258
+ },
259
+ "connector_key": {
260
+ "type": "string",
261
+ "pattern": "^[a-z][a-z0-9-]{1,62}(?:/[^/\\x00-\\x20\\x7f-\\x9f](?:[^/\\x00-\\x1f\\x7f-\\x9f]{0,254}[^/\\x00-\\x20\\x7f-\\x9f])?)?(?![\\s\\S])",
262
+ "description": "A connector ID, or connector/connection. A connection name has at most 256 characters, no slash, no control character, and no leading or trailing space, as Dex Web requires."
263
+ },
264
+ "connector_entry": {
265
+ "anyOf": [
266
+ {
267
+ "$ref": "#/$defs/connector_key"
268
+ },
269
+ {
270
+ "type": "object",
271
+ "minProperties": 1,
272
+ "maxProperties": 1,
273
+ "propertyNames": {
274
+ "$ref": "#/$defs/connector_key"
275
+ },
276
+ "additionalProperties": {
277
+ "type": "object",
278
+ "additionalProperties": false,
279
+ "required": [
280
+ "tools"
281
+ ],
282
+ "properties": {
283
+ "tools": {
284
+ "type": "array",
285
+ "uniqueItems": true,
286
+ "minItems": 1,
287
+ "items": {
288
+ "$ref": "#/$defs/operation_name"
289
+ },
290
+ "description": "The connector operations the agent may call, by snake-case name or operationId. Two entries must not name the same operation."
291
+ }
292
+ }
293
+ }
294
+ }
295
+ ]
296
+ },
297
+ "connector_entries": {
298
+ "type": "array",
299
+ "items": {
300
+ "$ref": "#/$defs/connector_entry"
301
+ }
302
+ },
303
+ "shell_commands": {
304
+ "type": "array",
305
+ "uniqueItems": true,
306
+ "items": {
307
+ "$ref": "#/$defs/single_line_text"
308
+ }
309
+ },
310
+ "harness_options": {
311
+ "type": "object",
312
+ "description": "Harness options. Each harness command defines its own fields and ignores the sections of other harnesses."
313
+ }
314
+ }
315
+ }
@@ -0,0 +1,153 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Dex Agents CLI session settings",
4
+ "description": "The settings of one session that a Dex Agents CLI command runs: the JSON object that start_session and update_session_settings store, at most 16 KiB. They override the configuration file from the next turn on. model and system_prompt replace the file's values. connectors.enabled may only narrow the file's connections and tools, approval.always only adds tools that need approval, and local_tools may only switch tools off or shrink the shell allow list. An omitted field keeps the file's value; there are no defaults here. Other top-level keys belong to other readers of the settings: they are ignored and reported as warnings, so they never fail a turn. Unknown keys inside a known field are errors. Settings that fail this schema or try to widen the file are rejected as a whole, and the turn fails with the error. A session without settings uses the file as it is. Do not put secrets in settings: they are stored in the session.",
5
+ "$comment": "The $defs shared with config.schema.json must stay identical to it; the tests in agents/cli/tests check them.",
6
+ "type": "object",
7
+ "properties": {
8
+ "model": {
9
+ "$ref": "#/$defs/model",
10
+ "description": "Replaces the file's model."
11
+ },
12
+ "system_prompt": {
13
+ "type": "string",
14
+ "description": "Replaces the file's system_prompt or system_prompt_file."
15
+ },
16
+ "connectors": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "properties": {
20
+ "enabled": {
21
+ "$ref": "#/$defs/connector_entries",
22
+ "description": "Replaces the file's list, which bounds it. Each entry must fall inside one entry of the file: the same connector, the same connection unless the file's entry names none, and a subset of the file's operations unless the file's entry lists none. When the file omits connectors.enabled, any list narrows it. An empty list: no connector tools."
23
+ }
24
+ },
25
+ "description": "Narrows the file's connectors. connectors.mcp is fixed by the file."
26
+ },
27
+ "approval": {
28
+ "type": "object",
29
+ "additionalProperties": false,
30
+ "properties": {
31
+ "always": {
32
+ "$ref": "#/$defs/tool_names",
33
+ "description": "More tools whose every call waits for approval, added to the file's approval.always."
34
+ }
35
+ },
36
+ "description": "Tightens the file's approval."
37
+ },
38
+ "local_tools": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "description": "Each field may only switch off what the file switches on, or repeat the file's value.",
42
+ "properties": {
43
+ "files": {
44
+ "type": "boolean",
45
+ "description": "false switches the file tools off; true only repeats the file's true."
46
+ },
47
+ "shell": {
48
+ "type": "object",
49
+ "additionalProperties": false,
50
+ "properties": {
51
+ "enabled": {
52
+ "type": "boolean",
53
+ "description": "false switches shell commands off; true only repeats the file's true."
54
+ },
55
+ "allow": {
56
+ "$ref": "#/$defs/shell_commands",
57
+ "description": "Replaces the file's allow list. Every command must be in the file's allow list, compared as exact strings."
58
+ }
59
+ },
60
+ "description": "Narrows the file's shell tool."
61
+ },
62
+ "web_fetch": {
63
+ "type": "boolean",
64
+ "description": "false switches web fetching off; true only repeats the file's true."
65
+ },
66
+ "browser": {
67
+ "type": "boolean",
68
+ "description": "false switches the browser off; true only repeats the file's true."
69
+ }
70
+ }
71
+ }
72
+ },
73
+ "$defs": {
74
+ "model": {
75
+ "type": "string",
76
+ "minLength": 1,
77
+ "description": "A model ID in the harness's own format, such as claude-opus-5-5 or openai/gpt-5-mini. Any non-empty string; the harness checks it."
78
+ },
79
+ "single_line_text": {
80
+ "type": "string",
81
+ "pattern": "^[^\\x00-\\x1f\\x7f]+(?![\\s\\S])",
82
+ "description": "Non-empty text without control characters."
83
+ },
84
+ "tool_name": {
85
+ "type": "string",
86
+ "pattern": "^[A-Za-z0-9_.-]{1,128}(?![\\s\\S])",
87
+ "description": "A tool name: 1 to 128 ASCII letters, digits, underscores, hyphens, or dots."
88
+ },
89
+ "tool_names": {
90
+ "type": "array",
91
+ "uniqueItems": true,
92
+ "items": {
93
+ "$ref": "#/$defs/tool_name"
94
+ }
95
+ },
96
+ "operation_name": {
97
+ "type": "string",
98
+ "pattern": "^[A-Za-z][A-Za-z0-9_]{0,127}(?![\\s\\S])",
99
+ "description": "A connector operation: its snake-case name, such as get_message, or its operationId, such as getMessage. Both spellings name the same operation: an operationId is compared after the connectors library's lowerCamelToSnake conversion, which keeps acronym runs together, so listStoryIDs is list_story_ids and getHTTPResponse is get_http_response."
100
+ },
101
+ "connector_key": {
102
+ "type": "string",
103
+ "pattern": "^[a-z][a-z0-9-]{1,62}(?:/[^/\\x00-\\x20\\x7f-\\x9f](?:[^/\\x00-\\x1f\\x7f-\\x9f]{0,254}[^/\\x00-\\x20\\x7f-\\x9f])?)?(?![\\s\\S])",
104
+ "description": "A connector ID, or connector/connection. A connection name has at most 256 characters, no slash, no control character, and no leading or trailing space, as Dex Web requires."
105
+ },
106
+ "connector_entry": {
107
+ "anyOf": [
108
+ {
109
+ "$ref": "#/$defs/connector_key"
110
+ },
111
+ {
112
+ "type": "object",
113
+ "minProperties": 1,
114
+ "maxProperties": 1,
115
+ "propertyNames": {
116
+ "$ref": "#/$defs/connector_key"
117
+ },
118
+ "additionalProperties": {
119
+ "type": "object",
120
+ "additionalProperties": false,
121
+ "required": [
122
+ "tools"
123
+ ],
124
+ "properties": {
125
+ "tools": {
126
+ "type": "array",
127
+ "uniqueItems": true,
128
+ "minItems": 1,
129
+ "items": {
130
+ "$ref": "#/$defs/operation_name"
131
+ },
132
+ "description": "The connector operations the agent may call, by snake-case name or operationId. Two entries must not name the same operation."
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ]
138
+ },
139
+ "connector_entries": {
140
+ "type": "array",
141
+ "items": {
142
+ "$ref": "#/$defs/connector_entry"
143
+ }
144
+ },
145
+ "shell_commands": {
146
+ "type": "array",
147
+ "uniqueItems": true,
148
+ "items": {
149
+ "$ref": "#/$defs/single_line_text"
150
+ }
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,179 @@
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
+ """Per-turn tool policy and narrowing, for the CLI core's tools and a harness's own replacements."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+
15
+ from dex_agents import (
16
+ AgentTurnContext,
17
+ DeferredToolCall,
18
+ DeferredToolExecution,
19
+ ToolEffect,
20
+ ToolExecutionError,
21
+ ToolPolicy,
22
+ )
23
+ from dex_agents_cli_core.config import EffectiveConfig, is_shell_command_allowed
24
+ from dex_agents_cli_core.harness import LocalToolGroup
25
+
26
+ # From least to most cautious: a policy may move a tool right, never left.
27
+ _EFFECT_CAUTION = {
28
+ ToolEffect.READ: 0,
29
+ ToolEffect.IDEMPOTENT_WRITE: 1,
30
+ ToolEffect.NON_IDEMPOTENT_WRITE: 2,
31
+ ToolEffect.UNCLASSIFIED: 3,
32
+ }
33
+ _APPROVED_BY_DEFAULT = frozenset({ToolEffect.NON_IDEMPOTENT_WRITE, ToolEffect.UNCLASSIFIED})
34
+
35
+
36
+ def resolve_turn_tool_policy(
37
+ turn: AgentTurnContext,
38
+ tool_name: str,
39
+ effect: ToolEffect,
40
+ requires_approval: bool,
41
+ turn_config: EffectiveConfig,
42
+ ) -> ToolPolicy:
43
+ """Return one tool call's policy from the tool's own, an explicit session policy, and the turn.
44
+
45
+ The tool's effect is a floor. An explicit ``ToolPolicy`` for the tool in
46
+ ``AgentSessionOptions.tool_policies`` may make the effect more cautious,
47
+ such as ``read`` to ``non_idempotent_write``, but never less, so a write
48
+ is never retried as a read; the ranking is ``read``,
49
+ ``idempotent_write``, ``non_idempotent_write``, then ``unclassified``.
50
+ Approval is a floor too: the call waits when the tool asks for it, when
51
+ the explicit policy asks for it, or, when the explicit policy leaves it
52
+ unset, when the combined effect needs approval by default. An explicit
53
+ ``requires_approval=False`` never removes the tool's own approval. A tool
54
+ in the turn's ``approval.always`` always waits.
55
+
56
+ A harness that replaces a local tool group, such as with Claude Code's own
57
+ tools, uses it to give those tools the CLI core's policy.
58
+
59
+ Example:
60
+ >>> turn_config = context.current_turn()
61
+ >>> policy = resolve_turn_tool_policy(
62
+ ... turn, "Bash", ToolEffect.NON_IDEMPOTENT_WRITE,
63
+ ... is_shell_command_approval_required(turn_config, command), turn_config)
64
+
65
+ Args:
66
+ turn: The ``RunModel`` context, whose explicit policies may tighten the tool.
67
+ tool_name: The tool name as the agent sees it.
68
+ effect: The tool's own effect.
69
+ requires_approval: Whether the tool itself asks for approval.
70
+ turn_config: The turn's configuration, for ``approval.always``.
71
+
72
+ Returns:
73
+ The policy for ``DeferredToolCall.policy``.
74
+ """
75
+ is_always_approved = tool_name in turn_config.approval.always
76
+ explicit = turn.tool_policies.get(tool_name)
77
+ if explicit is None:
78
+ return ToolPolicy(effect, requires_approval=requires_approval or is_always_approved)
79
+ if _EFFECT_CAUTION[explicit.effect] > _EFFECT_CAUTION[effect]:
80
+ effect = explicit.effect
81
+ if explicit.requires_approval is None:
82
+ is_explicitly_approved = effect in _APPROVED_BY_DEFAULT
83
+ else:
84
+ is_explicitly_approved = explicit.requires_approval
85
+ return ToolPolicy(
86
+ effect,
87
+ requires_approval=requires_approval or is_explicitly_approved or is_always_approved,
88
+ )
89
+
90
+
91
+ def is_local_tool_group_enabled(turn: EffectiveConfig, group: LocalToolGroup) -> bool:
92
+ """Report whether a turn switches a local tool group on.
93
+
94
+ A harness that replaces a group offers its own tools only while this is ``True``.
95
+
96
+ Example:
97
+ >>> if not is_local_tool_group_enabled(context.current_turn(), LocalToolGroup.SHELL):
98
+ ... return deny("shell commands are switched off for this session")
99
+
100
+ Args:
101
+ turn: The turn's configuration.
102
+ group: The local tool group.
103
+
104
+ Returns:
105
+ The group's ``local_tools`` switch for the turn.
106
+ """
107
+ local_tools = turn.local_tools
108
+ match group:
109
+ case LocalToolGroup.FILES:
110
+ return local_tools.files
111
+ case LocalToolGroup.SHELL:
112
+ return local_tools.shell.enabled
113
+ case LocalToolGroup.WEB_FETCH:
114
+ return local_tools.web_fetch
115
+ case LocalToolGroup.BROWSER:
116
+ return local_tools.browser
117
+
118
+
119
+ def is_shell_command_approval_required(turn: EffectiveConfig, command: str) -> bool:
120
+ """Report whether a shell command waits for approval in a turn.
121
+
122
+ Args:
123
+ turn: The turn's configuration.
124
+ command: The command text.
125
+
126
+ Returns:
127
+ ``True`` unless the turn's ``local_tools.shell.allow`` has the command's exact text.
128
+ """
129
+ return not is_shell_command_allowed(command, turn.local_tools.shell.allow)
130
+
131
+
132
+ #: Key of the reference a source stores for a call of a tool its turn switched off.
133
+ DISABLED_TOOL_REFERENCE_KEY = "disabled_tool"
134
+
135
+
136
+ def disabled_tool_call(tool_call_id: str, tool_name: str, arguments: str) -> DeferredToolCall:
137
+ """Return a call that its tool Step refuses: the turn switched the tool off.
138
+
139
+ A runner can still name a tool the turn does not offer, such as from a
140
+ listing it kept. The refusal is stored with the call, so the tool Step
141
+ refuses it on any Worker without running anything.
142
+
143
+ Args:
144
+ tool_call_id: The model's tool call ID.
145
+ tool_name: The tool name.
146
+ arguments: The arguments as JSON text.
147
+
148
+ Returns:
149
+ A read call without approval whose reference marks it switched off.
150
+ """
151
+ return DeferredToolCall(
152
+ tool_call_id=tool_call_id,
153
+ tool_name=tool_name,
154
+ arguments=arguments,
155
+ policy=ToolPolicy(ToolEffect.READ, requires_approval=False),
156
+ tool_source_reference=json.dumps({DISABLED_TOOL_REFERENCE_KEY: tool_name}),
157
+ )
158
+
159
+
160
+ def raise_if_disabled(call: DeferredToolExecution) -> None:
161
+ """Refuse a call that ``disabled_tool_call`` built.
162
+
163
+ Args:
164
+ call: The tool Step's call.
165
+
166
+ Raises:
167
+ ToolExecutionError: If the call's turn switched its tool off.
168
+ """
169
+ reference = call.tool_call.tool_source_reference
170
+ if reference is None:
171
+ return
172
+ try:
173
+ parsed = json.loads(reference)
174
+ except json.JSONDecodeError:
175
+ return
176
+ if isinstance(parsed, dict) and DISABLED_TOOL_REFERENCE_KEY in parsed:
177
+ raise ToolExecutionError(
178
+ f"tool {call.tool_call.tool_name} is switched off for this session"
179
+ )