delegate-agent-cli 0.11.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.
- delegate_agent/__init__.py +5 -0
- delegate_agent/archived_logs.py +44 -0
- delegate_agent/argv_builders.py +423 -0
- delegate_agent/argv_utils.py +36 -0
- delegate_agent/capability_commands.py +99 -0
- delegate_agent/cli.py +987 -0
- delegate_agent/cli_parser.py +1627 -0
- delegate_agent/command_errors.py +29 -0
- delegate_agent/command_help.py +1264 -0
- delegate_agent/config.py +1117 -0
- delegate_agent/config_commands.py +143 -0
- delegate_agent/constants.py +50 -0
- delegate_agent/describe_payload.py +1018 -0
- delegate_agent/errors.py +32 -0
- delegate_agent/git_utils.py +180 -0
- delegate_agent/harness_events.py +719 -0
- delegate_agent/inspection_commands.py +104 -0
- delegate_agent/isolation.py +316 -0
- delegate_agent/json_types.py +18 -0
- delegate_agent/log_output.py +78 -0
- delegate_agent/private_io.py +109 -0
- delegate_agent/profile_commands.py +42 -0
- delegate_agent/profile_guard.py +116 -0
- delegate_agent/profiles.py +389 -0
- delegate_agent/prompt_instructions.py +39 -0
- delegate_agent/prompt_transport.py +12 -0
- delegate_agent/reasoning.py +916 -0
- delegate_agent/redaction.py +193 -0
- delegate_agent/rendering.py +573 -0
- delegate_agent/request_build.py +1681 -0
- delegate_agent/request_models.py +191 -0
- delegate_agent/retention.py +321 -0
- delegate_agent/run_metadata.py +78 -0
- delegate_agent/run_output_commands.py +645 -0
- delegate_agent/run_registry.py +747 -0
- delegate_agent/run_status.py +300 -0
- delegate_agent/runner.py +1830 -0
- delegate_agent/safe_workspace.py +821 -0
- delegate_agent/snapshot_view.py +229 -0
- delegate_agent/wait_cancel_commands.py +559 -0
- delegate_agent/worktree_commands.py +218 -0
- delegate_agent/worktree_execution.py +654 -0
- delegate_agent/worktree_gc.py +529 -0
- delegate_agent/worktree_mgmt.py +782 -0
- delegate_agent/worktree_records.py +232 -0
- delegate_agent/worktree_remove.py +547 -0
- delegate_agent/worktree_summary.py +242 -0
- delegate_agent/wsl.py +65 -0
- delegate_agent_cli-0.11.0.dist-info/METADATA +325 -0
- delegate_agent_cli-0.11.0.dist-info/RECORD +54 -0
- delegate_agent_cli-0.11.0.dist-info/WHEEL +5 -0
- delegate_agent_cli-0.11.0.dist-info/entry_points.txt +2 -0
- delegate_agent_cli-0.11.0.dist-info/licenses/LICENSE +21 -0
- delegate_agent_cli-0.11.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tarfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from delegate_agent.json_types import JsonObject, is_non_negative_int
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def archive_dir(registry_root: Path) -> Path:
|
|
10
|
+
return registry_root / "archive"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def archive_path(registry_root: Path, run_id: str) -> Path:
|
|
14
|
+
return archive_dir(registry_root) / f"{run_id}.tar.gz"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def state_log_byte_sizes(state: JsonObject | None) -> tuple[int, int] | None:
|
|
18
|
+
if not state:
|
|
19
|
+
return None
|
|
20
|
+
stdout_bytes = state.get("stdoutBytes")
|
|
21
|
+
stderr_bytes = state.get("stderrBytes")
|
|
22
|
+
if is_non_negative_int(stdout_bytes) and is_non_negative_int(stderr_bytes):
|
|
23
|
+
return stdout_bytes, stderr_bytes
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def archive_log_byte_sizes(
|
|
28
|
+
archive_file: Path,
|
|
29
|
+
*,
|
|
30
|
+
stdout_log: str,
|
|
31
|
+
stderr_log: str,
|
|
32
|
+
) -> tuple[int, int]:
|
|
33
|
+
try:
|
|
34
|
+
with tarfile.open(archive_file, "r:gz") as archive:
|
|
35
|
+
stdout_bytes = 0
|
|
36
|
+
stderr_bytes = 0
|
|
37
|
+
for member in archive.getmembers():
|
|
38
|
+
if member.name == stdout_log:
|
|
39
|
+
stdout_bytes = member.size
|
|
40
|
+
elif member.name == stderr_log:
|
|
41
|
+
stderr_bytes = member.size
|
|
42
|
+
return stdout_bytes, stderr_bytes
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
return 0, 0
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Per-engine command-line construction.
|
|
2
|
+
|
|
3
|
+
Given a resolved config section, mode, workspace, model, and prompt, build the
|
|
4
|
+
exact argv each harness CLI (cursor / droid / kimi / claude / codex) expects.
|
|
5
|
+
The builders stay deliberately explicit rather than table-driven: prompt
|
|
6
|
+
transport, sandbox flags, MCP handling, and reasoning wiring differ materially
|
|
7
|
+
per engine, and the safe-mode flag sets here are security-load-bearing — codex
|
|
8
|
+
never emits its bypass flags in safe mode, cursor/droid/kimi/claude apply their
|
|
9
|
+
read-only flag sets. Preserve those branches exactly.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from delegate_agent import reasoning
|
|
15
|
+
from delegate_agent.constants import MODE_CALL, MODE_SAFE, MODE_WORK, validate_mode
|
|
16
|
+
from delegate_agent.errors import DelegateError
|
|
17
|
+
from delegate_agent.json_types import JsonObject
|
|
18
|
+
from delegate_agent.prompt_instructions import SKILL_REVIEW_PREFIX
|
|
19
|
+
from delegate_agent.prompt_transport import (
|
|
20
|
+
CURSOR_PROMPT_REDACTION,
|
|
21
|
+
DROID_PROMPT_FILE_ARG_PLACEHOLDER,
|
|
22
|
+
PROMPT_FILE_ARG_PLACEHOLDER,
|
|
23
|
+
PROMPT_TRANSPORT_ARGV,
|
|
24
|
+
PROMPT_TRANSPORT_FILE,
|
|
25
|
+
PROMPT_TRANSPORT_STDIN,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Each engine's safe-review prefix is one shared body behind a per-engine label.
|
|
29
|
+
# Cursor uses "Delegate review mode"; the rest use "Delegate <Engine> safe mode".
|
|
30
|
+
_SAFE_REVIEW_BODY = (
|
|
31
|
+
"(read-only review/investigation): "
|
|
32
|
+
"Inspect, map, and reason about the workspace. "
|
|
33
|
+
"You may propose patches or commands in text, but do not edit, create, delete, "
|
|
34
|
+
"format, commit, or otherwise mutate files or repo state. "
|
|
35
|
+
"If a write is blocked, do not retry or work around it; continue with a "
|
|
36
|
+
"text-only report.\n\n"
|
|
37
|
+
)
|
|
38
|
+
_SAFE_REVIEW_LABEL_BY_ENGINE = {
|
|
39
|
+
"cursor": "Delegate review mode",
|
|
40
|
+
"codex": "Delegate Codex safe mode",
|
|
41
|
+
"claude": "Delegate Claude safe mode",
|
|
42
|
+
"grok": "Delegate Grok safe mode",
|
|
43
|
+
"droid": "Delegate Droid safe mode",
|
|
44
|
+
"kimi": "Delegate Kimi safe mode",
|
|
45
|
+
}
|
|
46
|
+
SAFE_REVIEW_PREFIX_BY_ENGINE: dict[str, str] = {
|
|
47
|
+
engine: f"{label} {_SAFE_REVIEW_BODY}" for engine, label in _SAFE_REVIEW_LABEL_BY_ENGINE.items()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
CLAUDE_SAFE_TOOLS = "Read,Grep,Glob,Bash"
|
|
51
|
+
|
|
52
|
+
CLAUDE_SAFE_ALLOWED_TOOLS = (
|
|
53
|
+
"Bash(git diff:*),Bash(git status:*),Bash(git show:*),Bash(git log:*),"
|
|
54
|
+
"Bash(rg:*),Bash(grep:*),Bash(ls:*)"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def redacted_prompt_argv(argv: list[str], replacement: str = CURSOR_PROMPT_REDACTION) -> list[str]:
|
|
59
|
+
if not argv:
|
|
60
|
+
return []
|
|
61
|
+
redacted = list(argv)
|
|
62
|
+
redacted[-1] = replacement
|
|
63
|
+
return redacted
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _prefix_safe_prompt(prompt: str, engine: str) -> str:
|
|
67
|
+
"""Idempotently prepend the engine's safe-review prefix."""
|
|
68
|
+
safe_prefix = SAFE_REVIEW_PREFIX_BY_ENGINE[engine]
|
|
69
|
+
if prompt.startswith(safe_prefix):
|
|
70
|
+
return prompt
|
|
71
|
+
return f"{safe_prefix}{prompt}"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def prefix_cursor_safe_prompt(prompt: str) -> str:
|
|
75
|
+
return _prefix_safe_prompt(prompt, "cursor")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def prefix_kimi_safe_prompt(prompt: str) -> str:
|
|
79
|
+
return _prefix_safe_prompt(prompt, "kimi")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def prefix_droid_safe_prompt(prompt: str) -> str:
|
|
83
|
+
safe_prefix = SAFE_REVIEW_PREFIX_BY_ENGINE["droid"]
|
|
84
|
+
if prompt.startswith(safe_prefix):
|
|
85
|
+
return prompt
|
|
86
|
+
skill_prefix = SKILL_REVIEW_PREFIX
|
|
87
|
+
if prompt.startswith(skill_prefix):
|
|
88
|
+
insert_at = len(skill_prefix)
|
|
89
|
+
if prompt[insert_at:].startswith(safe_prefix):
|
|
90
|
+
return prompt
|
|
91
|
+
return prompt[:insert_at] + safe_prefix + prompt[insert_at:]
|
|
92
|
+
return f"{safe_prefix}{prompt}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _harness_bypass_enabled(config: JsonObject, mode: str, engine: str) -> bool:
|
|
96
|
+
"""Return true only for explicit harness-scoped bypass policy.
|
|
97
|
+
|
|
98
|
+
Delegate's historical policy profiles were Codex-oriented. Requiring the
|
|
99
|
+
bypass to live under policy.harness.<engine>.work prevents a global
|
|
100
|
+
external-sandbox profile from silently broadening native harness permissions.
|
|
101
|
+
"""
|
|
102
|
+
if mode != MODE_WORK:
|
|
103
|
+
return False
|
|
104
|
+
policy = config.get("policy")
|
|
105
|
+
if not isinstance(policy, dict):
|
|
106
|
+
return False
|
|
107
|
+
harnesses = policy.get("harness")
|
|
108
|
+
if not isinstance(harnesses, dict):
|
|
109
|
+
return False
|
|
110
|
+
engine_policy = harnesses.get(engine)
|
|
111
|
+
if not isinstance(engine_policy, dict):
|
|
112
|
+
return False
|
|
113
|
+
work_policy = engine_policy.get(MODE_WORK)
|
|
114
|
+
return isinstance(work_policy, dict) and work_policy.get("bypassApprovalsAndSandbox") is True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _claude_harness_bypass_enabled(config: JsonObject, mode: str) -> bool:
|
|
118
|
+
return _harness_bypass_enabled(config, mode, "claude")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _grok_harness_bypass_enabled(config: JsonObject, mode: str) -> bool:
|
|
122
|
+
return _harness_bypass_enabled(config, mode, "grok")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_cursor_argv(
|
|
126
|
+
prefix: list[str],
|
|
127
|
+
mode: str,
|
|
128
|
+
workspace: str,
|
|
129
|
+
model: str,
|
|
130
|
+
prompt: str,
|
|
131
|
+
*,
|
|
132
|
+
stream_capture: bool = True,
|
|
133
|
+
call_read_only: bool = False,
|
|
134
|
+
) -> list[str]:
|
|
135
|
+
argv = [*prefix, "--workspace", workspace, "-p", "--trust"]
|
|
136
|
+
if mode == MODE_WORK:
|
|
137
|
+
argv.extend(["--approve-mcps", "--force"])
|
|
138
|
+
elif mode == MODE_SAFE:
|
|
139
|
+
prompt = prefix_cursor_safe_prompt(prompt)
|
|
140
|
+
elif mode == MODE_CALL:
|
|
141
|
+
# Call defaults to work-level capability ("work minus a repo"); --read-only
|
|
142
|
+
# drops the write flags for the stateless judge/completion contract.
|
|
143
|
+
if not call_read_only:
|
|
144
|
+
argv.extend(["--approve-mcps", "--force"])
|
|
145
|
+
else:
|
|
146
|
+
validate_mode(mode)
|
|
147
|
+
if stream_capture:
|
|
148
|
+
argv.extend(["--model", model, "--print", "--output-format", "stream-json", prompt])
|
|
149
|
+
else:
|
|
150
|
+
argv.extend(["--model", model, "--output-format", "text", prompt])
|
|
151
|
+
return argv
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_droid_argv(
|
|
155
|
+
binary: str,
|
|
156
|
+
mode: str,
|
|
157
|
+
workspace: str,
|
|
158
|
+
model: str,
|
|
159
|
+
prompt: str,
|
|
160
|
+
*,
|
|
161
|
+
stream_capture: bool = True,
|
|
162
|
+
reasoning_capability: reasoning.ReasoningCapability | None = None,
|
|
163
|
+
prompt_transport: str = PROMPT_TRANSPORT_ARGV,
|
|
164
|
+
call_read_only: bool = False,
|
|
165
|
+
) -> list[str]:
|
|
166
|
+
argv = [binary, "exec", "--cwd", workspace]
|
|
167
|
+
if mode == MODE_WORK:
|
|
168
|
+
argv.append("--skip-permissions-unsafe")
|
|
169
|
+
elif mode == MODE_SAFE:
|
|
170
|
+
prompt = prefix_droid_safe_prompt(prompt)
|
|
171
|
+
elif mode == MODE_CALL:
|
|
172
|
+
if not call_read_only:
|
|
173
|
+
argv.append("--skip-permissions-unsafe")
|
|
174
|
+
else:
|
|
175
|
+
validate_mode(mode)
|
|
176
|
+
if reasoning_capability is not None:
|
|
177
|
+
argv.extend(["--reasoning-effort", reasoning_capability.effort])
|
|
178
|
+
if stream_capture:
|
|
179
|
+
argv.extend(["--model", model, "--output-format", "stream-json"])
|
|
180
|
+
else:
|
|
181
|
+
argv.extend(["--model", model])
|
|
182
|
+
if prompt_transport == PROMPT_TRANSPORT_ARGV:
|
|
183
|
+
argv.append(prompt)
|
|
184
|
+
elif prompt_transport == PROMPT_TRANSPORT_FILE:
|
|
185
|
+
argv.extend(["--file", DROID_PROMPT_FILE_ARG_PLACEHOLDER])
|
|
186
|
+
elif prompt_transport != PROMPT_TRANSPORT_STDIN:
|
|
187
|
+
raise DelegateError(
|
|
188
|
+
"invalid_prompt_transport",
|
|
189
|
+
f"Unsupported Droid prompt transport: {prompt_transport}",
|
|
190
|
+
)
|
|
191
|
+
return argv
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def build_kimi_argv(
|
|
195
|
+
kimi: JsonObject,
|
|
196
|
+
mode: str,
|
|
197
|
+
workspace: str,
|
|
198
|
+
model: str | None,
|
|
199
|
+
prompt: str,
|
|
200
|
+
*,
|
|
201
|
+
stream_capture: bool = True,
|
|
202
|
+
) -> list[str]:
|
|
203
|
+
argv = [str(kimi["binary"])]
|
|
204
|
+
if mode == MODE_SAFE:
|
|
205
|
+
prompt = prefix_kimi_safe_prompt(prompt)
|
|
206
|
+
elif mode not in (MODE_WORK, MODE_CALL):
|
|
207
|
+
validate_mode(mode)
|
|
208
|
+
if model:
|
|
209
|
+
argv.extend(["--model", model])
|
|
210
|
+
if stream_capture:
|
|
211
|
+
argv.extend(["--output-format", "stream-json"])
|
|
212
|
+
argv.extend(["--prompt", prompt])
|
|
213
|
+
return argv
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def build_claude_argv(
|
|
217
|
+
claude: JsonObject,
|
|
218
|
+
mode: str,
|
|
219
|
+
model: str | None,
|
|
220
|
+
policy: JsonObject,
|
|
221
|
+
*,
|
|
222
|
+
stream_capture: bool = True,
|
|
223
|
+
reasoning_effort: str | None = None,
|
|
224
|
+
allow_bypass_permissions: bool = False,
|
|
225
|
+
call_read_only: bool = False,
|
|
226
|
+
) -> list[str]:
|
|
227
|
+
argv = [
|
|
228
|
+
str(claude["binary"]),
|
|
229
|
+
"-p",
|
|
230
|
+
"--output-format",
|
|
231
|
+
"stream-json" if stream_capture else "text",
|
|
232
|
+
"--input-format",
|
|
233
|
+
"text",
|
|
234
|
+
]
|
|
235
|
+
if mode == MODE_SAFE:
|
|
236
|
+
argv.extend(
|
|
237
|
+
[
|
|
238
|
+
"--permission-mode",
|
|
239
|
+
"plan",
|
|
240
|
+
"--tools",
|
|
241
|
+
CLAUDE_SAFE_TOOLS,
|
|
242
|
+
"--allowedTools",
|
|
243
|
+
CLAUDE_SAFE_ALLOWED_TOOLS,
|
|
244
|
+
"--strict-mcp-config",
|
|
245
|
+
]
|
|
246
|
+
)
|
|
247
|
+
elif mode == MODE_WORK:
|
|
248
|
+
permission_mode = (
|
|
249
|
+
"bypassPermissions"
|
|
250
|
+
if allow_bypass_permissions and policy.get("bypassApprovalsAndSandbox") is True
|
|
251
|
+
else str(claude.get("workPermissionMode", "auto"))
|
|
252
|
+
)
|
|
253
|
+
argv.extend(["--permission-mode", permission_mode])
|
|
254
|
+
elif mode == MODE_CALL:
|
|
255
|
+
if call_read_only:
|
|
256
|
+
argv.extend(
|
|
257
|
+
[
|
|
258
|
+
"--permission-mode",
|
|
259
|
+
"plan",
|
|
260
|
+
"--tools",
|
|
261
|
+
CLAUDE_SAFE_TOOLS,
|
|
262
|
+
"--allowedTools",
|
|
263
|
+
CLAUDE_SAFE_ALLOWED_TOOLS,
|
|
264
|
+
"--strict-mcp-config",
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
argv.extend(["--permission-mode", str(claude.get("workPermissionMode", "auto"))])
|
|
269
|
+
else:
|
|
270
|
+
validate_mode(mode)
|
|
271
|
+
if claude.get("noSessionPersistence", True) is True:
|
|
272
|
+
argv.append("--no-session-persistence")
|
|
273
|
+
if claude.get("bare", False) is True:
|
|
274
|
+
argv.append("--bare")
|
|
275
|
+
if model:
|
|
276
|
+
argv.extend(["--model", model])
|
|
277
|
+
if reasoning_effort is not None:
|
|
278
|
+
argv.extend(["--effort", reasoning_effort])
|
|
279
|
+
return argv
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def build_grok_argv(
|
|
283
|
+
grok: JsonObject,
|
|
284
|
+
mode: str,
|
|
285
|
+
workspace: str,
|
|
286
|
+
model: str | None,
|
|
287
|
+
policy: JsonObject,
|
|
288
|
+
*,
|
|
289
|
+
stream_capture: bool = True,
|
|
290
|
+
reasoning_effort: str | None = None,
|
|
291
|
+
allow_bypass_permissions: bool = False,
|
|
292
|
+
prompt_transport: str = PROMPT_TRANSPORT_FILE,
|
|
293
|
+
call_read_only: bool = False,
|
|
294
|
+
) -> list[str]:
|
|
295
|
+
argv = [str(grok["binary"]), "--cwd", workspace]
|
|
296
|
+
if stream_capture:
|
|
297
|
+
argv.extend(["--output-format", "streaming-json"])
|
|
298
|
+
else:
|
|
299
|
+
argv.extend(["--output-format", "plain"])
|
|
300
|
+
if mode == MODE_SAFE:
|
|
301
|
+
argv.extend(
|
|
302
|
+
[
|
|
303
|
+
"--permission-mode",
|
|
304
|
+
str(grok.get("safePermissionMode", "dontAsk")),
|
|
305
|
+
"--sandbox",
|
|
306
|
+
str(grok.get("safeSandbox", "read-only")),
|
|
307
|
+
]
|
|
308
|
+
)
|
|
309
|
+
elif mode == MODE_WORK:
|
|
310
|
+
if allow_bypass_permissions and policy.get("bypassApprovalsAndSandbox") is True:
|
|
311
|
+
argv.extend(["--permission-mode", "bypassPermissions", "--always-approve"])
|
|
312
|
+
else:
|
|
313
|
+
argv.extend(["--permission-mode", str(grok.get("workPermissionMode", "auto"))])
|
|
314
|
+
work_sandbox = grok.get("workSandbox")
|
|
315
|
+
if isinstance(work_sandbox, str) and work_sandbox:
|
|
316
|
+
argv.extend(["--sandbox", work_sandbox])
|
|
317
|
+
elif mode == MODE_CALL:
|
|
318
|
+
if call_read_only:
|
|
319
|
+
argv.extend(
|
|
320
|
+
[
|
|
321
|
+
"--permission-mode",
|
|
322
|
+
str(grok.get("safePermissionMode", "dontAsk")),
|
|
323
|
+
"--sandbox",
|
|
324
|
+
str(grok.get("safeSandbox", "read-only")),
|
|
325
|
+
]
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
argv.extend(["--permission-mode", str(grok.get("workPermissionMode", "auto"))])
|
|
329
|
+
work_sandbox = grok.get("workSandbox")
|
|
330
|
+
if isinstance(work_sandbox, str) and work_sandbox:
|
|
331
|
+
argv.extend(["--sandbox", work_sandbox])
|
|
332
|
+
else:
|
|
333
|
+
validate_mode(mode)
|
|
334
|
+
if policy.get("webSearch") is not True and grok.get("disableWebSearch", True) is True:
|
|
335
|
+
argv.append("--disable-web-search")
|
|
336
|
+
if grok.get("noSubagents", False) is True:
|
|
337
|
+
argv.append("--no-subagents")
|
|
338
|
+
if model:
|
|
339
|
+
argv.extend(["--model", model])
|
|
340
|
+
if reasoning_effort is not None:
|
|
341
|
+
argv.extend(["--effort", reasoning_effort])
|
|
342
|
+
if prompt_transport != PROMPT_TRANSPORT_FILE:
|
|
343
|
+
raise DelegateError(
|
|
344
|
+
"invalid_prompt_transport",
|
|
345
|
+
f"Unsupported Grok prompt transport: {prompt_transport}",
|
|
346
|
+
)
|
|
347
|
+
argv.extend(["--prompt-file", PROMPT_FILE_ARG_PLACEHOLDER])
|
|
348
|
+
return argv
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def build_codex_argv(
|
|
352
|
+
codex: JsonObject,
|
|
353
|
+
mode: str,
|
|
354
|
+
workspace: str,
|
|
355
|
+
model: str | None,
|
|
356
|
+
prompt: str,
|
|
357
|
+
policy: JsonObject,
|
|
358
|
+
*,
|
|
359
|
+
workspace_kind: str,
|
|
360
|
+
stream_capture: bool = True,
|
|
361
|
+
reasoning_capability: reasoning.ReasoningCapability | None = None,
|
|
362
|
+
prompt_transport: str = PROMPT_TRANSPORT_ARGV,
|
|
363
|
+
output_schema: str | None = None,
|
|
364
|
+
call_read_only: bool = False,
|
|
365
|
+
) -> list[str]:
|
|
366
|
+
binary = str(codex["binary"])
|
|
367
|
+
argv = [binary]
|
|
368
|
+
# Safe mode is read-only by contract: never emit the dangerous bypass flags,
|
|
369
|
+
# even if a policy block somehow carries them. Config validation rejects such
|
|
370
|
+
# configs up front, but enforce the invariant structurally here too.
|
|
371
|
+
# Work-level call ("work minus a repo") gets the workSandbox but never the
|
|
372
|
+
# bypass flags — those stay bound to real work mode.
|
|
373
|
+
elevated = mode == MODE_WORK
|
|
374
|
+
call_write = mode == MODE_CALL and not call_read_only
|
|
375
|
+
write_sandbox = mode == MODE_WORK or call_write
|
|
376
|
+
bypass_sandbox = elevated and policy.get("bypassApprovalsAndSandbox") is True
|
|
377
|
+
bypass_hook_trust = elevated and policy.get("bypassHookTrust") is True
|
|
378
|
+
if policy.get("webSearch") is True:
|
|
379
|
+
argv.append("--search")
|
|
380
|
+
if not bypass_sandbox:
|
|
381
|
+
argv.extend(["--ask-for-approval", "never"])
|
|
382
|
+
if codex.get("profile"):
|
|
383
|
+
argv.extend(["--profile", str(codex["profile"])])
|
|
384
|
+
if model:
|
|
385
|
+
argv.extend(["--model", model])
|
|
386
|
+
if reasoning_capability is not None:
|
|
387
|
+
argv.extend(
|
|
388
|
+
[
|
|
389
|
+
"-c",
|
|
390
|
+
f'model_reasoning_effort="{reasoning_capability.effort}"',
|
|
391
|
+
]
|
|
392
|
+
)
|
|
393
|
+
argv.append("exec")
|
|
394
|
+
argv.extend(["--cd", workspace])
|
|
395
|
+
if output_schema is not None:
|
|
396
|
+
argv.extend(["--output-schema", output_schema])
|
|
397
|
+
if codex.get("ignoreUserConfig") is True:
|
|
398
|
+
argv.append("--ignore-user-config")
|
|
399
|
+
if workspace_kind != "git":
|
|
400
|
+
argv.append("--skip-git-repo-check")
|
|
401
|
+
if bypass_sandbox:
|
|
402
|
+
argv.append("--dangerously-bypass-approvals-and-sandbox")
|
|
403
|
+
else:
|
|
404
|
+
sandbox = codex["workSandbox"] if write_sandbox else "read-only"
|
|
405
|
+
argv.extend(["--sandbox", str(sandbox)])
|
|
406
|
+
if write_sandbox and sandbox == "workspace-write" and policy.get("networkAccess") is True:
|
|
407
|
+
argv.extend(["-c", "sandbox_workspace_write.network_access=true"])
|
|
408
|
+
if bypass_hook_trust:
|
|
409
|
+
argv.append("--dangerously-bypass-hook-trust")
|
|
410
|
+
if stream_capture:
|
|
411
|
+
argv.extend(["--color", "never", "--json"])
|
|
412
|
+
if codex.get("ephemeral", True) is True:
|
|
413
|
+
argv.append("--ephemeral")
|
|
414
|
+
if prompt_transport == PROMPT_TRANSPORT_ARGV:
|
|
415
|
+
argv.append(prompt)
|
|
416
|
+
elif prompt_transport == PROMPT_TRANSPORT_STDIN:
|
|
417
|
+
argv.append("-")
|
|
418
|
+
else:
|
|
419
|
+
raise DelegateError(
|
|
420
|
+
"invalid_prompt_transport",
|
|
421
|
+
f"Unsupported Codex prompt transport: {prompt_transport}",
|
|
422
|
+
)
|
|
423
|
+
return argv
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from delegate_agent.request_models import Request
|
|
7
|
+
|
|
8
|
+
WORKSPACE_FLAG_BY_ENGINE: dict[str, str | None] = {
|
|
9
|
+
"cursor": "--workspace",
|
|
10
|
+
"codex": "--cd",
|
|
11
|
+
"droid": "--cwd",
|
|
12
|
+
"kimi": None,
|
|
13
|
+
"claude": None,
|
|
14
|
+
"grok": "--cwd",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def replace_argv_after_flag(argv: list[str], flag: str, value: str) -> list[str]:
|
|
19
|
+
updated = list(argv)
|
|
20
|
+
for index, token in enumerate(updated):
|
|
21
|
+
if token == flag and index + 1 < len(updated):
|
|
22
|
+
updated[index + 1] = value
|
|
23
|
+
return updated
|
|
24
|
+
return updated
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def replace_workspace_arg_in_argv(engine: str, argv: list[str], value: str) -> list[str]:
|
|
28
|
+
flag = WORKSPACE_FLAG_BY_ENGINE.get(engine)
|
|
29
|
+
if flag is None:
|
|
30
|
+
return list(argv)
|
|
31
|
+
return replace_argv_after_flag(argv, flag, value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def public_argv(request: Request) -> list[str]:
|
|
35
|
+
"""Return the display-safe argv for a request (redacted form when present)."""
|
|
36
|
+
return list(request.display_argv if request.display_argv is not None else request.argv)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TextIO
|
|
5
|
+
|
|
6
|
+
from delegate_agent import command_errors, profiles, reasoning
|
|
7
|
+
from delegate_agent import rendering as delegate_rendering
|
|
8
|
+
from delegate_agent.config import harness_binary
|
|
9
|
+
from delegate_agent.json_types import JsonObject
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class CapabilitiesCommand:
|
|
14
|
+
refresh: bool = False
|
|
15
|
+
json_mode: bool = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CapabilitiesError(command_errors.CommandError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def capabilities_payload(config: JsonObject, config_source: str, workspace: str) -> JsonObject:
|
|
23
|
+
cache = reasoning.load_reasoning_capability_cache(workspace)
|
|
24
|
+
return {
|
|
25
|
+
"ok": True,
|
|
26
|
+
"configSource": config_source,
|
|
27
|
+
"cachePath": str(reasoning.reasoning_capability_cache_path(workspace)),
|
|
28
|
+
"reasoning": reasoning.build_reasoning_capabilities_payload(config, cache),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _refresh_payload(
|
|
33
|
+
config: JsonObject,
|
|
34
|
+
workspace: str,
|
|
35
|
+
*,
|
|
36
|
+
auth_profile_override: str | None = None,
|
|
37
|
+
) -> JsonObject:
|
|
38
|
+
profile = profiles.resolve_active_profile(
|
|
39
|
+
config,
|
|
40
|
+
profiles.child_environment(),
|
|
41
|
+
cli_override=auth_profile_override,
|
|
42
|
+
)
|
|
43
|
+
try:
|
|
44
|
+
existing_cache = reasoning.load_reasoning_capability_cache(workspace)
|
|
45
|
+
refreshed_cache = reasoning.refresh_reasoning_capabilities(
|
|
46
|
+
cwd=workspace,
|
|
47
|
+
codex_binary=harness_binary(config, "codex"),
|
|
48
|
+
env=profiles.child_environment(overrides=profile.env),
|
|
49
|
+
)
|
|
50
|
+
cache = reasoning.merge_reasoning_capability_cache(existing_cache, refreshed_cache)
|
|
51
|
+
cache_path = reasoning.write_reasoning_capability_cache(workspace, cache)
|
|
52
|
+
except reasoning.ReasoningCapabilityError as exc:
|
|
53
|
+
raise CapabilitiesError(exc.error, exc.message) from exc
|
|
54
|
+
return {
|
|
55
|
+
"ok": True,
|
|
56
|
+
"refreshed": True,
|
|
57
|
+
"cachePath": str(cache_path),
|
|
58
|
+
"reasoning": reasoning.build_reasoning_capabilities_payload(config, cache),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def emit(
|
|
63
|
+
command: CapabilitiesCommand,
|
|
64
|
+
*,
|
|
65
|
+
config: JsonObject,
|
|
66
|
+
config_source: str,
|
|
67
|
+
workspace: str,
|
|
68
|
+
auth_profile_override: str | None = None,
|
|
69
|
+
stdout: TextIO,
|
|
70
|
+
) -> int:
|
|
71
|
+
payload = (
|
|
72
|
+
_refresh_payload(config, workspace, auth_profile_override=auth_profile_override)
|
|
73
|
+
if command.refresh
|
|
74
|
+
else capabilities_payload(config, config_source, workspace)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if command.json_mode:
|
|
78
|
+
delegate_rendering.print_json(payload, stdout)
|
|
79
|
+
else:
|
|
80
|
+
print(f"reasoning capabilities: {payload['cachePath']}", file=stdout)
|
|
81
|
+
harnesses = payload["reasoning"]["harnesses"]
|
|
82
|
+
if not isinstance(harnesses, dict):
|
|
83
|
+
return 0
|
|
84
|
+
for harness, harness_payload in harnesses.items():
|
|
85
|
+
if not isinstance(harness_payload, dict):
|
|
86
|
+
continue
|
|
87
|
+
supported = harness_payload.get("supported")
|
|
88
|
+
warning = harness_payload.get("warning")
|
|
89
|
+
if isinstance(warning, str):
|
|
90
|
+
print(f"{harness}: {warning}", file=stdout)
|
|
91
|
+
continue
|
|
92
|
+
if isinstance(supported, list):
|
|
93
|
+
print(f"{harness}: {len(supported)} static effort level(s)", file=stdout)
|
|
94
|
+
continue
|
|
95
|
+
models = harness_payload.get("models")
|
|
96
|
+
if not isinstance(models, dict):
|
|
97
|
+
continue
|
|
98
|
+
print(f"{harness}: {len(models)} model(s)", file=stdout)
|
|
99
|
+
return 0
|