agentpool-cli 0.1.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.
- agentpool/__init__.py +3 -0
- agentpool/agent_io.py +134 -0
- agentpool/artifacts.py +151 -0
- agentpool/cli.py +1199 -0
- agentpool/config.py +373 -0
- agentpool/docs/agentpool-skill.md +85 -0
- agentpool/docs/onboarding.md +169 -0
- agentpool/event_detection.py +150 -0
- agentpool/fixtures/__init__.py +1 -0
- agentpool/fixtures/fake_agents/__init__.py +1 -0
- agentpool/fixtures/fake_agents/fake_approval_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_common.py +44 -0
- agentpool/fixtures/fake_agents/fake_completed_agent.py +13 -0
- agentpool/fixtures/fake_agents/fake_idle_agent.py +16 -0
- agentpool/fixtures/fake_agents/fake_limit_agent.py +14 -0
- agentpool/fixtures/fake_agents/fake_patch_agent.py +17 -0
- agentpool/fixtures/fake_agents/fake_question_agent.py +16 -0
- agentpool/git_worktree.py +144 -0
- agentpool/mcp/__init__.py +1 -0
- agentpool/mcp/resources.py +64 -0
- agentpool/mcp/tools.py +259 -0
- agentpool/mcp_server.py +487 -0
- agentpool/models.py +310 -0
- agentpool/onboarding.py +1279 -0
- agentpool/policy.py +63 -0
- agentpool/provider_model_catalog.json +997 -0
- agentpool/providers/__init__.py +3 -0
- agentpool/providers/base.py +411 -0
- agentpool/providers/registry.py +139 -0
- agentpool/redaction.py +30 -0
- agentpool/runtimes/__init__.py +3 -0
- agentpool/runtimes/base.py +36 -0
- agentpool/runtimes/tmux.py +133 -0
- agentpool/session_manager.py +1061 -0
- agentpool/stats/__init__.py +6 -0
- agentpool/stats/card.py +74 -0
- agentpool/stats/compute.py +496 -0
- agentpool/stats/queries.py +138 -0
- agentpool/stats/render.py +103 -0
- agentpool/stats/window.py +85 -0
- agentpool/store.py +478 -0
- agentpool/usage/__init__.py +1 -0
- agentpool/usage/_common.py +223 -0
- agentpool/usage/ccusage.py +130 -0
- agentpool/usage/claude.py +23 -0
- agentpool/usage/codex.py +210 -0
- agentpool/usage/codexbar.py +186 -0
- agentpool/usage/combine.py +71 -0
- agentpool/usage/copilot.py +146 -0
- agentpool/usage/devin.py +265 -0
- agentpool/usage/parsers.py +41 -0
- agentpool/usage/probes.py +52 -0
- agentpool/usage/provider_parsers.py +276 -0
- agentpool/usage/summary.py +166 -0
- agentpool/utils.py +59 -0
- agentpool_cli-0.1.0.dist-info/METADATA +292 -0
- agentpool_cli-0.1.0.dist-info/RECORD +60 -0
- agentpool_cli-0.1.0.dist-info/WHEEL +4 -0
- agentpool_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agentpool_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from agentpool.config import ProviderConfig
|
|
10
|
+
from agentpool.models import (
|
|
11
|
+
AuthStatus,
|
|
12
|
+
Capability,
|
|
13
|
+
CapacitySnapshot,
|
|
14
|
+
Confidence,
|
|
15
|
+
ModelDescriptor,
|
|
16
|
+
ProviderDescriptor,
|
|
17
|
+
RuntimeKind,
|
|
18
|
+
SpawnWorkerRequest,
|
|
19
|
+
UsageStatus,
|
|
20
|
+
)
|
|
21
|
+
from agentpool.utils import run_capture
|
|
22
|
+
from agentpool.usage.probes import (
|
|
23
|
+
claude_code_usage_snapshot,
|
|
24
|
+
codex_cli_usage_snapshot,
|
|
25
|
+
copilot_cli_usage_snapshot,
|
|
26
|
+
devin_cli_usage_snapshot,
|
|
27
|
+
unknown,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProviderEventPatterns:
|
|
32
|
+
question: list[str] = []
|
|
33
|
+
approval: list[str] = []
|
|
34
|
+
error: list[str] = []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ProviderAdapter(Protocol):
|
|
38
|
+
id: str
|
|
39
|
+
display_name: str
|
|
40
|
+
harness: str
|
|
41
|
+
config: ProviderConfig
|
|
42
|
+
|
|
43
|
+
def detect(self, config: ProviderConfig) -> ProviderDescriptor:
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def auth_status(self) -> AuthStatus:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def inventory_usage_snapshot(self) -> CapacitySnapshot:
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
def build_launch_command(self, request: SpawnWorkerRequest, workdir: Path) -> list[str]:
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def build_initial_prompt(self, request: SpawnWorkerRequest, session_id: str, workdir: Path) -> str:
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def event_patterns(self) -> ProviderEventPatterns:
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
def capabilities(self) -> list[Capability]:
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def submit_keys(self) -> list[str] | None:
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CommandProviderAdapter:
|
|
72
|
+
vendor: str | None = None
|
|
73
|
+
|
|
74
|
+
def __init__(self, provider_id: str, display_name: str, harness: str, config: ProviderConfig):
|
|
75
|
+
self.id = provider_id
|
|
76
|
+
self.display_name = display_name
|
|
77
|
+
self.harness = harness
|
|
78
|
+
self.config = config
|
|
79
|
+
|
|
80
|
+
def detect(self, config: ProviderConfig | None = None) -> ProviderDescriptor:
|
|
81
|
+
cfg = config or self.config
|
|
82
|
+
binary_path = self._binary_path(cfg)
|
|
83
|
+
installed = bool(binary_path)
|
|
84
|
+
return ProviderDescriptor(
|
|
85
|
+
id=self.id,
|
|
86
|
+
display_name=self.display_name,
|
|
87
|
+
vendor=self.vendor,
|
|
88
|
+
harness=self.harness,
|
|
89
|
+
installed=installed,
|
|
90
|
+
binary_path=binary_path,
|
|
91
|
+
version=self._version(binary_path),
|
|
92
|
+
auth=self.auth_status() if installed else AuthStatus(
|
|
93
|
+
status="unavailable",
|
|
94
|
+
confidence=Confidence.UNKNOWN,
|
|
95
|
+
reason="No binary candidate found.",
|
|
96
|
+
),
|
|
97
|
+
models=[
|
|
98
|
+
ModelDescriptor(
|
|
99
|
+
id=str(model["id"]),
|
|
100
|
+
display_name=model.get("display_name"),
|
|
101
|
+
source=model.get("source", "config"),
|
|
102
|
+
confidence=Confidence(model.get("confidence", Confidence.UNKNOWN.value)),
|
|
103
|
+
aliases=model.get("aliases", []),
|
|
104
|
+
metadata=model.get("metadata", {}),
|
|
105
|
+
)
|
|
106
|
+
for model in cfg.models
|
|
107
|
+
],
|
|
108
|
+
runtimes=[RuntimeKind.TMUX] if installed else [],
|
|
109
|
+
capabilities=self.capabilities() if installed else [],
|
|
110
|
+
usage=self.inventory_usage_snapshot() if installed else CapacitySnapshot(
|
|
111
|
+
provider_id=self.id,
|
|
112
|
+
status=UsageStatus.UNAVAILABLE,
|
|
113
|
+
confidence=Confidence.UNKNOWN,
|
|
114
|
+
warnings=["Provider binary is not installed."],
|
|
115
|
+
),
|
|
116
|
+
warnings=[] if installed else ["Provider binary is not installed."],
|
|
117
|
+
metadata=cfg.metadata,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def auth_status(self) -> AuthStatus:
|
|
121
|
+
return AuthStatus(
|
|
122
|
+
status="unknown",
|
|
123
|
+
confidence=Confidence.UNKNOWN,
|
|
124
|
+
reason="No safe auth probe implemented for this provider.",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
128
|
+
return CapacitySnapshot(
|
|
129
|
+
provider_id=self.id,
|
|
130
|
+
status=UsageStatus.UNKNOWN,
|
|
131
|
+
confidence=Confidence.UNKNOWN,
|
|
132
|
+
warnings=["No safe usage probe available for this provider."],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def inventory_usage_snapshot(self) -> CapacitySnapshot:
|
|
136
|
+
return self.usage_snapshot()
|
|
137
|
+
|
|
138
|
+
def build_launch_command(self, request: SpawnWorkerRequest, workdir: Path) -> list[str]:
|
|
139
|
+
if self.config.command:
|
|
140
|
+
command = [str(Path(part).expanduser()) if part.startswith("~/") else part for part in self.config.command]
|
|
141
|
+
else:
|
|
142
|
+
binary = self._binary_path(self.config)
|
|
143
|
+
if not binary:
|
|
144
|
+
raise FileNotFoundError(f"No binary found for {self.id}")
|
|
145
|
+
command = [binary]
|
|
146
|
+
if request.model and Capability.SUPPORTS_MODEL_ARG in self.capabilities():
|
|
147
|
+
model_arg = str(self.config.metadata.get("model_arg") or "--model")
|
|
148
|
+
command.extend([model_arg, request.model])
|
|
149
|
+
return command
|
|
150
|
+
|
|
151
|
+
def build_initial_prompt(self, request: SpawnWorkerRequest, session_id: str, workdir: Path) -> str:
|
|
152
|
+
return f"""You are running as a delegated worker session under AgentPool.
|
|
153
|
+
|
|
154
|
+
Role: {request.role}
|
|
155
|
+
Isolation: {request.isolation}
|
|
156
|
+
Repo: {workdir}
|
|
157
|
+
Session: {session_id}
|
|
158
|
+
|
|
159
|
+
Instructions:
|
|
160
|
+
- Follow the task from the primary agent.
|
|
161
|
+
- If you are blocked, ask a concise question and wait.
|
|
162
|
+
- If you need approval for a risky action, ask before proceeding.
|
|
163
|
+
- Do not modify files unless the task explicitly allows edits.
|
|
164
|
+
- If isolation is read_only, inspect only and do not edit files.
|
|
165
|
+
- Keep notes of files inspected, commands run, and findings.
|
|
166
|
+
- When finished, print:
|
|
167
|
+
|
|
168
|
+
AGENTPOOL_RESULT_START
|
|
169
|
+
Summary:
|
|
170
|
+
Findings:
|
|
171
|
+
Files inspected:
|
|
172
|
+
Files changed:
|
|
173
|
+
Commands run:
|
|
174
|
+
Tests run:
|
|
175
|
+
Blockers:
|
|
176
|
+
Confidence:
|
|
177
|
+
AGENTPOOL_RESULT_END
|
|
178
|
+
|
|
179
|
+
Task:
|
|
180
|
+
{request.task}
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def event_patterns(self) -> ProviderEventPatterns:
|
|
184
|
+
return ProviderEventPatterns()
|
|
185
|
+
|
|
186
|
+
def capabilities(self) -> list[Capability]:
|
|
187
|
+
return [
|
|
188
|
+
Capability.LIVE_STEERING,
|
|
189
|
+
Capability.READ_ONLY_ADVISORY,
|
|
190
|
+
Capability.CAN_RUN_TESTS,
|
|
191
|
+
Capability.SUPPORTS_INTERACTIVE_MODE,
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
def submit_keys(self) -> list[str] | None:
|
|
195
|
+
keys = self.config.metadata.get("submit_keys")
|
|
196
|
+
return list(keys) if isinstance(keys, list) else None
|
|
197
|
+
|
|
198
|
+
def _binary_path(self, config: ProviderConfig) -> str | None:
|
|
199
|
+
if config.command:
|
|
200
|
+
first = config.command[0]
|
|
201
|
+
return first if Path(first).exists() else shutil.which(first)
|
|
202
|
+
for candidate in config.binary_candidates:
|
|
203
|
+
if Path(candidate).exists():
|
|
204
|
+
return str(Path(candidate))
|
|
205
|
+
found = shutil.which(candidate)
|
|
206
|
+
if found:
|
|
207
|
+
return found
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def _version(self, binary_path: str | None) -> str | None:
|
|
211
|
+
if not binary_path:
|
|
212
|
+
return None
|
|
213
|
+
proc = run_capture([binary_path, "--version"], timeout=1)
|
|
214
|
+
if proc.returncode == 0:
|
|
215
|
+
return (proc.stdout or proc.stderr).strip().splitlines()[0][:200]
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class FakeProviderAdapter(CommandProviderAdapter):
|
|
220
|
+
vendor = "AgentPool"
|
|
221
|
+
|
|
222
|
+
def auth_status(self) -> AuthStatus:
|
|
223
|
+
return AuthStatus(status="authenticated", confidence=Confidence.LOCAL_CONFIG, reason="Fake local fixture.")
|
|
224
|
+
|
|
225
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
226
|
+
return CapacitySnapshot(provider_id=self.id, status=UsageStatus.AVAILABLE, confidence=Confidence.LOCAL_CONFIG)
|
|
227
|
+
|
|
228
|
+
def capabilities(self) -> list[Capability]:
|
|
229
|
+
return super().capabilities() + [
|
|
230
|
+
Capability.WORKTREE_EDITS,
|
|
231
|
+
Capability.SUPPORTS_APPROVAL_DETECTION,
|
|
232
|
+
Capability.SUPPORTS_ONE_SHOT_MODE,
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ClaudeCodeAdapter(CommandProviderAdapter):
|
|
237
|
+
vendor = "Anthropic"
|
|
238
|
+
|
|
239
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
240
|
+
return claude_code_usage_snapshot(self.id, self._binary_path(self.config))
|
|
241
|
+
|
|
242
|
+
def inventory_usage_snapshot(self) -> CapacitySnapshot:
|
|
243
|
+
return unknown(
|
|
244
|
+
self.id,
|
|
245
|
+
"Explicit `agentpool usage --provider claude-code` runs a temporary Claude CLI /usage probe.",
|
|
246
|
+
source="claude_pty_usage",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def capabilities(self) -> list[Capability]:
|
|
250
|
+
return super().capabilities() + [Capability.SUPPORTS_MODEL_ARG, Capability.SUPPORTS_USAGE_PROBE]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class CodexCliAdapter(CommandProviderAdapter):
|
|
254
|
+
vendor = "OpenAI"
|
|
255
|
+
|
|
256
|
+
def build_launch_command(self, request: SpawnWorkerRequest, workdir: Path) -> list[str]:
|
|
257
|
+
command = super().build_launch_command(request, workdir)
|
|
258
|
+
if request.reasoning_effort:
|
|
259
|
+
command.extend(["-c", f"model_reasoning_effort={json.dumps(request.reasoning_effort)}"])
|
|
260
|
+
if request.service_tier:
|
|
261
|
+
command.extend(["-c", f"service_tier={json.dumps(request.service_tier)}"])
|
|
262
|
+
return command
|
|
263
|
+
|
|
264
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
265
|
+
return codex_cli_usage_snapshot(self.id, self._binary_path(self.config))
|
|
266
|
+
|
|
267
|
+
def inventory_usage_snapshot(self) -> CapacitySnapshot:
|
|
268
|
+
return unknown(
|
|
269
|
+
self.id,
|
|
270
|
+
"Explicit `agentpool usage --provider codex-cli` runs the Codex app-server usage probe.",
|
|
271
|
+
source="codex_app_server",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def capabilities(self) -> list[Capability]:
|
|
275
|
+
return super().capabilities() + [Capability.SUPPORTS_MODEL_ARG, Capability.SUPPORTS_USAGE_PROBE]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class OpenCodeAdapter(CommandProviderAdapter):
|
|
279
|
+
vendor = "OpenCode"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class CursorCliAdapter(CommandProviderAdapter):
|
|
283
|
+
vendor = "Cursor"
|
|
284
|
+
|
|
285
|
+
def auth_status(self) -> AuthStatus:
|
|
286
|
+
binary = self._binary_path(self.config)
|
|
287
|
+
if not binary:
|
|
288
|
+
return AuthStatus(
|
|
289
|
+
status="unavailable",
|
|
290
|
+
confidence=Confidence.UNKNOWN,
|
|
291
|
+
reason="No Cursor Agent binary found.",
|
|
292
|
+
)
|
|
293
|
+
proc = run_capture([binary, "status", "--format", "json"], timeout=5)
|
|
294
|
+
text = "\n".join(part for part in [proc.stdout, proc.stderr] if part)
|
|
295
|
+
try:
|
|
296
|
+
payload = json.loads(text)
|
|
297
|
+
except json.JSONDecodeError:
|
|
298
|
+
if proc.returncode == 0 and "logged in" in text.lower():
|
|
299
|
+
return AuthStatus(status="authenticated", confidence=Confidence.LOCAL_CLI, reason=text.strip()[:300])
|
|
300
|
+
return AuthStatus(status="unknown", confidence=Confidence.UNKNOWN, reason=text.strip()[:300] or None)
|
|
301
|
+
if payload.get("isAuthenticated") or payload.get("status") == "authenticated":
|
|
302
|
+
reason = payload.get("message")
|
|
303
|
+
return AuthStatus(
|
|
304
|
+
status="authenticated",
|
|
305
|
+
confidence=Confidence.LOCAL_CLI,
|
|
306
|
+
reason=str(reason) if reason else None,
|
|
307
|
+
)
|
|
308
|
+
return AuthStatus(
|
|
309
|
+
status="unauthenticated",
|
|
310
|
+
confidence=Confidence.LOCAL_CLI,
|
|
311
|
+
reason=str(payload.get("message") or "Cursor Agent CLI is not authenticated."),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def build_launch_command(self, request: SpawnWorkerRequest, workdir: Path) -> list[str]:
|
|
315
|
+
command = super().build_launch_command(request, workdir)
|
|
316
|
+
if request.isolation == "read_only":
|
|
317
|
+
read_only_mode = str(self.config.metadata.get("read_only_mode_arg") or "ask")
|
|
318
|
+
command.extend(["--mode", read_only_mode])
|
|
319
|
+
command.extend(["--workspace", str(workdir)])
|
|
320
|
+
return command
|
|
321
|
+
|
|
322
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
323
|
+
return unknown(
|
|
324
|
+
self.id,
|
|
325
|
+
"Cursor Agent CLI exposes usage via interactive /usage, but no stable non-interactive native usage probe is confirmed.",
|
|
326
|
+
source="cursor_cli_interactive_usage_only",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def inventory_usage_snapshot(self) -> CapacitySnapshot:
|
|
330
|
+
return unknown(
|
|
331
|
+
self.id,
|
|
332
|
+
"Use `agentpool usage --provider cursor-cli --backend codexbar` for the optional CodexBar Cursor usage path.",
|
|
333
|
+
source="cursor_cli_usage_unknown",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def capabilities(self) -> list[Capability]:
|
|
337
|
+
return super().capabilities() + [Capability.SUPPORTS_MODEL_ARG, Capability.SUPPORTS_USAGE_PROBE]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class CopilotCliAdapter(CommandProviderAdapter):
|
|
341
|
+
vendor = "GitHub"
|
|
342
|
+
|
|
343
|
+
def build_launch_command(self, request: SpawnWorkerRequest, workdir: Path) -> list[str]:
|
|
344
|
+
command = [str(Path(part).expanduser()) if part.startswith("~/") else part for part in self.config.command or []]
|
|
345
|
+
if not command:
|
|
346
|
+
binary = self._binary_path(self.config)
|
|
347
|
+
if not binary:
|
|
348
|
+
raise FileNotFoundError(f"No binary found for {self.id}")
|
|
349
|
+
command = [binary]
|
|
350
|
+
copilot_args: list[str] = []
|
|
351
|
+
if request.model:
|
|
352
|
+
model_arg = str(self.config.metadata.get("model_arg") or "--model")
|
|
353
|
+
copilot_args.extend([model_arg, request.model])
|
|
354
|
+
if request.isolation == "read_only":
|
|
355
|
+
read_only_mode = str(self.config.metadata.get("read_only_mode_arg") or "plan")
|
|
356
|
+
copilot_args.extend(["--mode", read_only_mode])
|
|
357
|
+
separator = self.config.metadata.get("forward_separator")
|
|
358
|
+
if copilot_args and command[:2] == ["gh", "copilot"] and separator:
|
|
359
|
+
command.append(str(separator))
|
|
360
|
+
command.extend(copilot_args)
|
|
361
|
+
return command
|
|
362
|
+
|
|
363
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
364
|
+
return copilot_cli_usage_snapshot(self.id, self._binary_path(self.config))
|
|
365
|
+
|
|
366
|
+
def inventory_usage_snapshot(self) -> CapacitySnapshot:
|
|
367
|
+
return unknown(
|
|
368
|
+
self.id,
|
|
369
|
+
"Explicit `agentpool usage --provider copilot-cli` uses an ambient GitHub token.",
|
|
370
|
+
source="github_copilot_internal_api",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def capabilities(self) -> list[Capability]:
|
|
374
|
+
return super().capabilities() + [Capability.SUPPORTS_MODEL_ARG, Capability.SUPPORTS_USAGE_PROBE]
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class FactoryDroidAdapter(CommandProviderAdapter):
|
|
378
|
+
vendor = "Factory"
|
|
379
|
+
|
|
380
|
+
def build_launch_command(self, request: SpawnWorkerRequest, workdir: Path) -> list[str]:
|
|
381
|
+
command = super().build_launch_command(request, workdir)
|
|
382
|
+
if request.model:
|
|
383
|
+
command.extend(["--settings", str(_droid_runtime_settings_path(request.model))])
|
|
384
|
+
return command
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class DevinCliAdapter(CommandProviderAdapter):
|
|
388
|
+
vendor = "Devin"
|
|
389
|
+
|
|
390
|
+
def usage_snapshot(self) -> CapacitySnapshot:
|
|
391
|
+
return devin_cli_usage_snapshot(self.id, self._binary_path(self.config))
|
|
392
|
+
|
|
393
|
+
def inventory_usage_snapshot(self) -> CapacitySnapshot:
|
|
394
|
+
return unknown(
|
|
395
|
+
self.id,
|
|
396
|
+
"Explicit `agentpool usage --provider devin-cli` runs the Devin plan-status usage probe.",
|
|
397
|
+
source="devin_plan_status_api",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
def capabilities(self) -> list[Capability]:
|
|
401
|
+
return super().capabilities() + [Capability.SUPPORTS_MODEL_ARG, Capability.SUPPORTS_USAGE_PROBE]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _droid_runtime_settings_path(model: str) -> Path:
|
|
405
|
+
safe_model = re.sub(r"[^A-Za-z0-9_.-]+", "_", model)
|
|
406
|
+
root = Path("~/.agentpool/runtime-settings").expanduser()
|
|
407
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
408
|
+
path = root / f"droid-{safe_model}.json"
|
|
409
|
+
payload = {"sessionDefaultSettings": {"model": model}}
|
|
410
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
411
|
+
return path
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
|
|
5
|
+
from agentpool.config import AgentPoolConfig, ProviderConfig
|
|
6
|
+
from agentpool.models import CapacitySnapshot, Confidence, ProviderDescriptor, ToolError, UsageStatus
|
|
7
|
+
from agentpool.providers.base import (
|
|
8
|
+
ClaudeCodeAdapter,
|
|
9
|
+
CodexCliAdapter,
|
|
10
|
+
CommandProviderAdapter,
|
|
11
|
+
CopilotCliAdapter,
|
|
12
|
+
CursorCliAdapter,
|
|
13
|
+
DevinCliAdapter,
|
|
14
|
+
FactoryDroidAdapter,
|
|
15
|
+
FakeProviderAdapter,
|
|
16
|
+
OpenCodeAdapter,
|
|
17
|
+
ProviderAdapter,
|
|
18
|
+
)
|
|
19
|
+
from agentpool.usage.probes import ccusage_usage_snapshot, codexbar_usage_snapshot, combine_usage_snapshots
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
ADAPTERS = {
|
|
23
|
+
"claude-code": ClaudeCodeAdapter,
|
|
24
|
+
"codex-cli": CodexCliAdapter,
|
|
25
|
+
"cursor-cli": CursorCliAdapter,
|
|
26
|
+
"opencode": OpenCodeAdapter,
|
|
27
|
+
"copilot-cli": CopilotCliAdapter,
|
|
28
|
+
"droid-cli": FactoryDroidAdapter,
|
|
29
|
+
"devin-cli": DevinCliAdapter,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
DISPLAY_NAMES = {
|
|
33
|
+
"claude-code": "Claude Code",
|
|
34
|
+
"codex-cli": "Codex CLI",
|
|
35
|
+
"cursor-cli": "Cursor Agent CLI",
|
|
36
|
+
"opencode": "OpenCode",
|
|
37
|
+
"copilot-cli": "GitHub Copilot CLI",
|
|
38
|
+
"droid-cli": "Droid CLI",
|
|
39
|
+
"devin-cli": "Devin CLI",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProviderRegistry:
|
|
44
|
+
def __init__(self, adapters: dict[str, ProviderAdapter]):
|
|
45
|
+
self.adapters = adapters
|
|
46
|
+
|
|
47
|
+
def get(self, provider_id: str) -> ProviderAdapter:
|
|
48
|
+
if provider_id == "auto":
|
|
49
|
+
raise ToolError(
|
|
50
|
+
"POLICY_BLOCKED",
|
|
51
|
+
"provider=auto is not supported in AgentPool v0.1.",
|
|
52
|
+
{"provider_id": provider_id},
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
return self.adapters[provider_id]
|
|
56
|
+
except KeyError as exc:
|
|
57
|
+
raise ToolError(
|
|
58
|
+
"PROVIDER_NOT_FOUND",
|
|
59
|
+
f"Provider {provider_id} is not configured.",
|
|
60
|
+
{"provider_id": provider_id},
|
|
61
|
+
) from exc
|
|
62
|
+
|
|
63
|
+
def descriptors(self, include_usage: bool = True) -> list[ProviderDescriptor]:
|
|
64
|
+
descriptors = []
|
|
65
|
+
for adapter in self.adapters.values():
|
|
66
|
+
descriptor = adapter.detect()
|
|
67
|
+
if not include_usage:
|
|
68
|
+
descriptor.usage = None
|
|
69
|
+
descriptors.append(descriptor)
|
|
70
|
+
return descriptors
|
|
71
|
+
|
|
72
|
+
def usage(self, provider_id: str | None = None, backend: str = "native") -> list[CapacitySnapshot]:
|
|
73
|
+
if backend not in {"native", "codexbar", "ccusage", "combined"}:
|
|
74
|
+
raise ToolError(
|
|
75
|
+
"INVALID_USAGE_BACKEND",
|
|
76
|
+
"Usage backend must be one of: native, codexbar, ccusage, combined.",
|
|
77
|
+
{"backend": backend},
|
|
78
|
+
)
|
|
79
|
+
adapters = [self.get(provider_id)] if provider_id else list(self.adapters.values())
|
|
80
|
+
if len(adapters) <= 1:
|
|
81
|
+
return [_usage_for_adapter(adapters[0], backend)] if adapters else []
|
|
82
|
+
with ThreadPoolExecutor(max_workers=min(8, len(adapters))) as executor:
|
|
83
|
+
return list(executor.map(lambda adapter: _usage_for_adapter(adapter, backend), adapters))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _usage_for_adapter(adapter: ProviderAdapter, backend: str) -> CapacitySnapshot:
|
|
87
|
+
descriptor = adapter.detect()
|
|
88
|
+
if not descriptor.installed:
|
|
89
|
+
return CapacitySnapshot(
|
|
90
|
+
provider_id=adapter.id,
|
|
91
|
+
status=UsageStatus.UNAVAILABLE,
|
|
92
|
+
confidence=Confidence.UNKNOWN,
|
|
93
|
+
warnings=["Provider binary is not installed."],
|
|
94
|
+
)
|
|
95
|
+
if backend == "codexbar":
|
|
96
|
+
return codexbar_usage_snapshot(adapter.id)
|
|
97
|
+
if backend == "ccusage":
|
|
98
|
+
return ccusage_usage_snapshot(adapter.id)
|
|
99
|
+
if backend == "combined":
|
|
100
|
+
native = adapter.usage_snapshot()
|
|
101
|
+
codexbar = codexbar_usage_snapshot(adapter.id)
|
|
102
|
+
ccusage = ccusage_usage_snapshot(adapter.id) if adapter.id == "claude-code" else None
|
|
103
|
+
return combine_usage_snapshots(native, codexbar, ccusage=ccusage)
|
|
104
|
+
return adapter.usage_snapshot()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def build_registry(config: AgentPoolConfig) -> ProviderRegistry:
|
|
108
|
+
adapters: dict[str, ProviderAdapter] = {}
|
|
109
|
+
for provider_id, provider_config in config.providers.items():
|
|
110
|
+
if not provider_config.enabled:
|
|
111
|
+
continue
|
|
112
|
+
if provider_id.startswith("fake-"):
|
|
113
|
+
adapters[provider_id] = FakeProviderAdapter(
|
|
114
|
+
provider_id, provider_id.replace("-", " ").title(), provider_id, provider_config
|
|
115
|
+
)
|
|
116
|
+
continue
|
|
117
|
+
cls = ADAPTERS.get(provider_id, CommandProviderAdapter)
|
|
118
|
+
adapters[provider_id] = cls(
|
|
119
|
+
provider_id,
|
|
120
|
+
DISPLAY_NAMES.get(provider_id, provider_id.replace("-", " ").title()),
|
|
121
|
+
provider_id,
|
|
122
|
+
provider_config,
|
|
123
|
+
)
|
|
124
|
+
for custom in config.custom_providers:
|
|
125
|
+
provider_id = custom["id"]
|
|
126
|
+
provider_config = ProviderConfig(
|
|
127
|
+
enabled=custom.get("enabled", True),
|
|
128
|
+
binary_candidates=custom.get("binary_candidates", []),
|
|
129
|
+
command=custom.get("command"),
|
|
130
|
+
models=custom.get("models", []),
|
|
131
|
+
metadata={**custom.get("metadata", {}), "custom": True},
|
|
132
|
+
)
|
|
133
|
+
adapters[provider_id] = CommandProviderAdapter(
|
|
134
|
+
provider_id,
|
|
135
|
+
custom.get("display_name", provider_id),
|
|
136
|
+
custom.get("harness", provider_id),
|
|
137
|
+
provider_config,
|
|
138
|
+
)
|
|
139
|
+
return ProviderRegistry(adapters)
|
agentpool/redaction.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
SECRET_PATTERNS = [
|
|
7
|
+
re.compile(r"(?i)(authorization:\s*[A-Za-z][A-Za-z0-9._+-]*\s+)[^\s]+"),
|
|
8
|
+
re.compile(r"(?i)((?:api|access|refresh|secret|private|session|auth)?_?(?:token|key|secret|password)=)[^\s]+"),
|
|
9
|
+
re.compile(r"(?i)((?:api|access|refresh|secret|private|session|auth)?[-_ ]?(?:token|key|secret|password):\s*)[^\s]+"),
|
|
10
|
+
re.compile(r"(?i)((?:postgres|postgresql|mysql|mongodb|redis)://[^:\s]+:)[^@\s]+(@)"),
|
|
11
|
+
re.compile(r"(?i)(sk-[A-Za-z0-9_\-]{16,})"),
|
|
12
|
+
re.compile(r"\b(AKIA|ASIA)[A-Z0-9]{16}\b"),
|
|
13
|
+
re.compile(r"\bgh[opusr]_[A-Za-z0-9_]{20,}\b"),
|
|
14
|
+
re.compile(r"\bAIza[0-9A-Za-z_\-]{30,}\b"),
|
|
15
|
+
re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{20,}\b"),
|
|
16
|
+
re.compile(r"\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b"),
|
|
17
|
+
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----", re.S),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def redact_text(text: str) -> str:
|
|
22
|
+
redacted = text
|
|
23
|
+
for pattern in SECRET_PATTERNS:
|
|
24
|
+
if pattern.groups >= 2:
|
|
25
|
+
redacted = pattern.sub(lambda match: f"{match.group(1)}[REDACTED]{match.group(2)}", redacted)
|
|
26
|
+
elif pattern.groups == 1:
|
|
27
|
+
redacted = pattern.sub(lambda match: f"{match.group(1)}[REDACTED]", redacted)
|
|
28
|
+
else:
|
|
29
|
+
redacted = pattern.sub("[REDACTED]", redacted)
|
|
30
|
+
return redacted
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from agentpool.models import RuntimeKind, TmuxSessionRef
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RuntimeAdapter(Protocol):
|
|
10
|
+
kind: RuntimeKind
|
|
11
|
+
|
|
12
|
+
def spawn(
|
|
13
|
+
self, command: list[str], cwd: Path, env: dict[str, str], session_name: str
|
|
14
|
+
) -> TmuxSessionRef:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
def capture(self, ref: TmuxSessionRef, lines: int) -> str:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def send_message(self, ref: TmuxSessionRef, text: str, submit: bool = True) -> None:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def send_keys(self, ref: TmuxSessionRef, keys: list[str]) -> None:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def interrupt(self, ref: TmuxSessionRef) -> None:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def attach_command(self, ref: TmuxSessionRef) -> str:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def terminate(self, ref: TmuxSessionRef) -> None:
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def exists(self, ref: TmuxSessionRef) -> bool:
|
|
36
|
+
...
|