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.
Files changed (60) hide show
  1. agentpool/__init__.py +3 -0
  2. agentpool/agent_io.py +134 -0
  3. agentpool/artifacts.py +151 -0
  4. agentpool/cli.py +1199 -0
  5. agentpool/config.py +373 -0
  6. agentpool/docs/agentpool-skill.md +85 -0
  7. agentpool/docs/onboarding.md +169 -0
  8. agentpool/event_detection.py +150 -0
  9. agentpool/fixtures/__init__.py +1 -0
  10. agentpool/fixtures/fake_agents/__init__.py +1 -0
  11. agentpool/fixtures/fake_agents/fake_approval_agent.py +16 -0
  12. agentpool/fixtures/fake_agents/fake_common.py +44 -0
  13. agentpool/fixtures/fake_agents/fake_completed_agent.py +13 -0
  14. agentpool/fixtures/fake_agents/fake_idle_agent.py +16 -0
  15. agentpool/fixtures/fake_agents/fake_limit_agent.py +14 -0
  16. agentpool/fixtures/fake_agents/fake_patch_agent.py +17 -0
  17. agentpool/fixtures/fake_agents/fake_question_agent.py +16 -0
  18. agentpool/git_worktree.py +144 -0
  19. agentpool/mcp/__init__.py +1 -0
  20. agentpool/mcp/resources.py +64 -0
  21. agentpool/mcp/tools.py +259 -0
  22. agentpool/mcp_server.py +487 -0
  23. agentpool/models.py +310 -0
  24. agentpool/onboarding.py +1279 -0
  25. agentpool/policy.py +63 -0
  26. agentpool/provider_model_catalog.json +997 -0
  27. agentpool/providers/__init__.py +3 -0
  28. agentpool/providers/base.py +411 -0
  29. agentpool/providers/registry.py +139 -0
  30. agentpool/redaction.py +30 -0
  31. agentpool/runtimes/__init__.py +3 -0
  32. agentpool/runtimes/base.py +36 -0
  33. agentpool/runtimes/tmux.py +133 -0
  34. agentpool/session_manager.py +1061 -0
  35. agentpool/stats/__init__.py +6 -0
  36. agentpool/stats/card.py +74 -0
  37. agentpool/stats/compute.py +496 -0
  38. agentpool/stats/queries.py +138 -0
  39. agentpool/stats/render.py +103 -0
  40. agentpool/stats/window.py +85 -0
  41. agentpool/store.py +478 -0
  42. agentpool/usage/__init__.py +1 -0
  43. agentpool/usage/_common.py +223 -0
  44. agentpool/usage/ccusage.py +130 -0
  45. agentpool/usage/claude.py +23 -0
  46. agentpool/usage/codex.py +210 -0
  47. agentpool/usage/codexbar.py +186 -0
  48. agentpool/usage/combine.py +71 -0
  49. agentpool/usage/copilot.py +146 -0
  50. agentpool/usage/devin.py +265 -0
  51. agentpool/usage/parsers.py +41 -0
  52. agentpool/usage/probes.py +52 -0
  53. agentpool/usage/provider_parsers.py +276 -0
  54. agentpool/usage/summary.py +166 -0
  55. agentpool/utils.py +59 -0
  56. agentpool_cli-0.1.0.dist-info/METADATA +292 -0
  57. agentpool_cli-0.1.0.dist-info/RECORD +60 -0
  58. agentpool_cli-0.1.0.dist-info/WHEEL +4 -0
  59. agentpool_cli-0.1.0.dist-info/entry_points.txt +2 -0
  60. agentpool_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ from agentpool.providers.registry import ProviderRegistry, build_registry
2
+
3
+ __all__ = ["ProviderRegistry", "build_registry"]
@@ -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,3 @@
1
+ from agentpool.runtimes.tmux import TmuxRuntime
2
+
3
+ __all__ = ["TmuxRuntime"]
@@ -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
+ ...