pilot-workers 0.2.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 (66) hide show
  1. pilot_workers/__init__.py +1 -0
  2. pilot_workers/__main__.py +3 -0
  3. pilot_workers/cli/__init__.py +0 -0
  4. pilot_workers/cli/dispatch.py +577 -0
  5. pilot_workers/cli/install.py +259 -0
  6. pilot_workers/cli/main.py +115 -0
  7. pilot_workers/cli/run.py +191 -0
  8. pilot_workers/credentials.py +102 -0
  9. pilot_workers/data/permissions/README.md +47 -0
  10. pilot_workers/data/permissions/relaxed.yaml +10 -0
  11. pilot_workers/data/permissions/strict.yaml +9 -0
  12. pilot_workers/data/providers/README.md +24 -0
  13. pilot_workers/data/providers/ds.yaml +11 -0
  14. pilot_workers/data/providers/glm.yaml +11 -0
  15. pilot_workers/data/providers/kimi-k3.yaml +11 -0
  16. pilot_workers/data/templates/code.md +28 -0
  17. pilot_workers/data/templates/explore.md +17 -0
  18. pilot_workers/data/templates/review.md +18 -0
  19. pilot_workers/data/templates/test.md +17 -0
  20. pilot_workers/fmt_events.py +218 -0
  21. pilot_workers/integrations/README.md +25 -0
  22. pilot_workers/integrations/claude-host/agents/ds-coder.md +66 -0
  23. pilot_workers/integrations/claude-host/agents/ds-explorer.md +56 -0
  24. pilot_workers/integrations/claude-host/agents/ds-reviewer.md +50 -0
  25. pilot_workers/integrations/claude-host/agents/ds-tester.md +46 -0
  26. pilot_workers/integrations/claude-host/agents/glm-coder.md +66 -0
  27. pilot_workers/integrations/claude-host/agents/glm-explorer.md +56 -0
  28. pilot_workers/integrations/claude-host/agents/glm-reviewer.md +50 -0
  29. pilot_workers/integrations/claude-host/agents/glm-tester.md +46 -0
  30. pilot_workers/integrations/claude-host/agents/kimi-coder.md +66 -0
  31. pilot_workers/integrations/claude-host/agents/kimi-explorer.md +56 -0
  32. pilot_workers/integrations/claude-host/agents/kimi-reviewer.md +50 -0
  33. pilot_workers/integrations/claude-host/agents/kimi-tester.md +50 -0
  34. pilot_workers/integrations/claude-host/commands/glm/code.md +48 -0
  35. pilot_workers/integrations/claude-host/commands/glm/explore.md +38 -0
  36. pilot_workers/integrations/claude-host/commands/glm/review.md +38 -0
  37. pilot_workers/integrations/claude-host/commands/glm/test.md +30 -0
  38. pilot_workers/integrations/claude-host/commands/kimi/code.md +47 -0
  39. pilot_workers/integrations/claude-host/commands/kimi/explore.md +38 -0
  40. pilot_workers/integrations/claude-host/commands/kimi/review.md +38 -0
  41. pilot_workers/integrations/claude-host/commands/kimi/test.md +32 -0
  42. pilot_workers/integrations/codex-host/ds/SKILL.md +15 -0
  43. pilot_workers/integrations/codex-host/ds/openai.yaml +4 -0
  44. pilot_workers/integrations/codex-host/glm/SKILL.md +15 -0
  45. pilot_workers/integrations/codex-host/glm/openai.yaml +4 -0
  46. pilot_workers/integrations/codex-host/kimi/SKILL.md +15 -0
  47. pilot_workers/integrations/codex-host/kimi/openai.yaml +4 -0
  48. pilot_workers/maintain.py +170 -0
  49. pilot_workers/policy.py +295 -0
  50. pilot_workers/prompts/code.md +6 -0
  51. pilot_workers/prompts/common.md +24 -0
  52. pilot_workers/prompts/explore.md +7 -0
  53. pilot_workers/prompts/review.md +5 -0
  54. pilot_workers/prompts/test.md +6 -0
  55. pilot_workers/providers.py +137 -0
  56. pilot_workers/runners/__init__.py +28 -0
  57. pilot_workers/runners/base.py +123 -0
  58. pilot_workers/runners/opencode_runner.py +377 -0
  59. pilot_workers/runtime.py +314 -0
  60. pilot_workers/scripts/install_runtime.sh +51 -0
  61. pilot_workers-0.2.0.dist-info/METADATA +84 -0
  62. pilot_workers-0.2.0.dist-info/RECORD +66 -0
  63. pilot_workers-0.2.0.dist-info/WHEEL +5 -0
  64. pilot_workers-0.2.0.dist-info/entry_points.txt +2 -0
  65. pilot_workers-0.2.0.dist-info/licenses/LICENSE +21 -0
  66. pilot_workers-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env python3
2
+ """Install integration files (agents, commands, skills) to host config directories."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import importlib.metadata
8
+ import json
9
+ import os
10
+ import shutil
11
+ import sys
12
+ import tempfile
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+
16
+ from pilot_workers import providers
17
+
18
+
19
+ INTEGRATIONS_DIR = Path(__file__).resolve().parent.parent / "integrations"
20
+
21
+ MANIFEST_SCHEMA_VERSION = 1
22
+
23
+
24
+ def _manifest_path() -> Path:
25
+ return providers.pilot_home() / "install-manifest.json"
26
+
27
+
28
+ def _package_version() -> str:
29
+ try:
30
+ return importlib.metadata.version("pilot-workers")
31
+ except importlib.metadata.PackageNotFoundError:
32
+ return "unknown"
33
+
34
+
35
+ def _load_manifest(path: Path) -> dict:
36
+ if not path.exists():
37
+ return {"schema_version": MANIFEST_SCHEMA_VERSION, "hosts": {}}
38
+ try:
39
+ return json.loads(path.read_text(encoding="utf-8"))
40
+ except json.JSONDecodeError as exc:
41
+ raise RuntimeError(f"corrupt install manifest {path}: {exc}") from exc
42
+
43
+
44
+ def _write_manifest(path: Path, data: dict) -> None:
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ temporary_name: str | None = None
47
+ try:
48
+ with tempfile.NamedTemporaryFile(
49
+ mode="w", encoding="utf-8", dir=path.parent,
50
+ prefix=".install-manifest.", suffix=".tmp", delete=False,
51
+ ) as temporary:
52
+ temporary_name = temporary.name
53
+ json.dump(data, temporary, indent=2)
54
+ temporary.write("\n")
55
+ temporary.flush()
56
+ os.fsync(temporary.fileno())
57
+ os.replace(temporary_name, path)
58
+ finally:
59
+ if temporary_name and os.path.exists(temporary_name):
60
+ os.unlink(temporary_name)
61
+
62
+
63
+ def _purge_previous(host: str, manifest: dict) -> None:
64
+ """Remove files recorded by a previous install of the same host."""
65
+ entry = manifest.get("hosts", {}).get(host)
66
+ if not entry:
67
+ return
68
+ removed = 0
69
+ for name in entry.get("files", []):
70
+ try:
71
+ os.unlink(name)
72
+ removed += 1
73
+ except OSError:
74
+ pass
75
+ # Drop directories the previous install created, if now empty
76
+ # (deepest first, so nested dirs like dispatch/references/ go before dispatch/).
77
+ # Only use created_dirs — do not try to rmdir file parents that the user may own.
78
+ candidates = {Path(d) for d in entry.get("created_dirs", [])}
79
+ for directory in sorted(candidates, key=lambda p: len(p.parts), reverse=True):
80
+ try:
81
+ directory.rmdir()
82
+ except OSError:
83
+ pass
84
+ print(f" removed {removed} stale file(s) from previous {host} install")
85
+
86
+
87
+ def install_claude(target: Path | None = None) -> dict:
88
+ """Copy Claude Code agent and command definitions."""
89
+ base = (target or Path.home() / ".claude").resolve()
90
+ src = INTEGRATIONS_DIR / "claude-host"
91
+ if not src.is_dir():
92
+ raise RuntimeError(f"integration source not found: {src}")
93
+
94
+ files: list[str] = []
95
+ created_dirs: list[str] = []
96
+
97
+ def _mkdir(path: Path) -> None:
98
+ if not path.exists():
99
+ created_dirs.append(str(path))
100
+ path.mkdir(parents=True, exist_ok=True)
101
+
102
+ agents_src = src / "agents"
103
+ commands_src = src / "commands"
104
+
105
+ if agents_src.is_dir():
106
+ agents_dst = base / "agents"
107
+ _mkdir(agents_dst)
108
+ for f in agents_src.glob("*.md"):
109
+ shutil.copy2(f, agents_dst / f.name)
110
+ files.append(str(agents_dst / f.name))
111
+ print(f" installed agent: {f.name}")
112
+
113
+ if commands_src.is_dir():
114
+ for provider_dir in commands_src.iterdir():
115
+ if not provider_dir.is_dir():
116
+ continue
117
+ dst = base / "commands" / provider_dir.name
118
+ _mkdir(dst)
119
+ for f in provider_dir.glob("*.md"):
120
+ shutil.copy2(f, dst / f.name)
121
+ files.append(str(dst / f.name))
122
+ print(f" installed command: {provider_dir.name}/{f.name}")
123
+
124
+ return {"files": files, "created_dirs": created_dirs}
125
+
126
+
127
+ def install_codex(target: Path | None = None) -> dict:
128
+ """Copy Codex skill definitions."""
129
+ codex_home = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
130
+ base = (target or codex_home / "skills").resolve()
131
+ src = INTEGRATIONS_DIR / "codex-host"
132
+ if not src.is_dir():
133
+ raise RuntimeError(f"integration source not found: {src}")
134
+
135
+ files: list[str] = []
136
+ created_dirs: list[str] = []
137
+
138
+ for skill_dir in src.iterdir():
139
+ if not skill_dir.is_dir():
140
+ continue
141
+ dst = base / skill_dir.name
142
+ dst.mkdir(parents=True, exist_ok=True)
143
+ # Copy files individually (do not rmtree — that would delete user content)
144
+ for src_file in sorted(skill_dir.rglob("*")):
145
+ if src_file.is_dir():
146
+ continue
147
+ rel = src_file.relative_to(skill_dir)
148
+ dest_file = dst / rel
149
+ dest_file.parent.mkdir(parents=True, exist_ok=True)
150
+ shutil.copy2(src_file, dest_file)
151
+ created_dirs.append(str(dst))
152
+ for f in sorted(dst.rglob("*")):
153
+ if f.is_dir():
154
+ created_dirs.append(str(f))
155
+ elif f.is_file():
156
+ files.append(str(f))
157
+ print(f" installed skill: {skill_dir.name}/")
158
+
159
+ return {"files": files, "created_dirs": created_dirs}
160
+
161
+
162
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
163
+ parser = argparse.ArgumentParser(description="Install pilot-workers integrations.")
164
+ parser.add_argument("host", choices=["claude", "codex", "all"], help="Target host.")
165
+ parser.add_argument("--target", type=Path, help="Override target directory.")
166
+ return parser.parse_args(argv)
167
+
168
+
169
+ def main(argv: list[str] | None = None) -> int:
170
+ args = parse_args(argv)
171
+ try:
172
+ manifest_path = _manifest_path()
173
+ manifest = _load_manifest(manifest_path)
174
+ manifest["schema_version"] = MANIFEST_SCHEMA_VERSION
175
+ manifest.setdefault("hosts", {})
176
+ for host, label, installer in (
177
+ ("claude", "Claude Code", install_claude),
178
+ ("codex", "Codex", install_codex),
179
+ ):
180
+ if args.host not in (host, "all"):
181
+ continue
182
+ print(f"Installing {label} integrations...")
183
+ _purge_previous(host, manifest)
184
+ result = installer(args.target)
185
+ manifest["hosts"][host] = {
186
+ "installed_at": datetime.now(timezone.utc).isoformat(),
187
+ "package_version": _package_version(),
188
+ "files": result["files"],
189
+ "created_dirs": result["created_dirs"],
190
+ }
191
+ _write_manifest(manifest_path, manifest)
192
+ print("Done.")
193
+ return 0
194
+ except (OSError, RuntimeError) as exc:
195
+ print(f"error: {exc}")
196
+ return 1
197
+
198
+
199
+ def _uninstall_host(entry: dict) -> None:
200
+ for name in entry.get("files", []):
201
+ path = Path(name)
202
+ if not path.exists():
203
+ print(f"note: already gone: {name}")
204
+ continue
205
+ path.unlink()
206
+ print(f"removed: {name}")
207
+ dirs = sorted(entry.get("created_dirs", []),
208
+ key=lambda p: len(Path(p).parts), reverse=True)
209
+ for name in dirs:
210
+ try:
211
+ os.rmdir(name)
212
+ print(f"removed: {name}")
213
+ except OSError:
214
+ print(f"note: kept non-empty directory: {name}")
215
+
216
+
217
+ def uninstall_main(argv: list[str] | None = None) -> int:
218
+ parser = argparse.ArgumentParser(description="Uninstall pilot-workers integrations.")
219
+ parser.add_argument("host", choices=["claude", "codex", "all"], help="Target host.")
220
+ args = parser.parse_args(argv)
221
+
222
+ hosts = ["claude", "codex"] if args.host == "all" else [args.host]
223
+ manifest_path = _manifest_path()
224
+ if not manifest_path.exists():
225
+ print(f"error: no install manifest found at {manifest_path}", file=sys.stderr)
226
+ return 1
227
+ try:
228
+ manifest = _load_manifest(manifest_path)
229
+ except (OSError, RuntimeError) as exc:
230
+ print(f"error: {exc}", file=sys.stderr)
231
+ return 1
232
+ entries = manifest.get("hosts", {})
233
+ present = [h for h in hosts if h in entries]
234
+ missing = [h for h in hosts if h not in entries]
235
+ if missing and not present:
236
+ print(f"error: no manifest entry for host(s): {', '.join(missing)}",
237
+ file=sys.stderr)
238
+ return 1
239
+ if missing:
240
+ for h in missing:
241
+ print(f"note: no manifest entry for {h}, skipping", file=sys.stderr)
242
+ try:
243
+ for host in present:
244
+ print(f"Uninstalling {host} integrations...")
245
+ _uninstall_host(entries.pop(host))
246
+ if entries:
247
+ manifest["hosts"] = entries
248
+ _write_manifest(manifest_path, manifest)
249
+ else:
250
+ manifest_path.unlink()
251
+ print("Done.")
252
+ return 0
253
+ except (OSError, RuntimeError) as exc:
254
+ print(f"error: {exc}", file=sys.stderr)
255
+ return 1
256
+
257
+
258
+ if __name__ == "__main__":
259
+ raise SystemExit(main())
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """Unified CLI entry: pilot-workers <subcommand> ..."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+ import subprocess
8
+ import sys
9
+
10
+
11
+ USAGE = """usage: pilot-workers <subcommand> [args]
12
+
13
+ subcommands:
14
+ run Dispatch a bounded task to an isolated LLM worker.
15
+ dispatch Deterministic outer shell around run (started + verdict JSON).
16
+ template Print the task template for a mode (code|explore|test|review).
17
+ install Install worker integration assets.
18
+ uninstall Remove worker integration assets.
19
+ credentials Configure isolated worker credentials.
20
+ maintain Worker log and worktree lifecycle tools.
21
+ runtime Manage the worker runtime (runtime install).
22
+
23
+ Use 'pilot-workers <subcommand> --help' for subcommand-specific help.
24
+ """
25
+
26
+
27
+ def _with_argv(program: str, rest: list[str], fn) -> int:
28
+ """Call a no-arg main() that parses sys.argv, with a temporary argv."""
29
+ original = sys.argv
30
+ sys.argv = [program] + rest
31
+ try:
32
+ return fn()
33
+ finally:
34
+ sys.argv = original
35
+
36
+
37
+ def main(argv: list[str] | None = None) -> int:
38
+ args = list(sys.argv[1:] if argv is None else argv)
39
+ if not args or args[0] in ("-h", "--help"):
40
+ print(USAGE, end="")
41
+ return 0
42
+
43
+ subcommand, rest = args[0], args[1:]
44
+
45
+ if subcommand == "run":
46
+ from pilot_workers.cli.run import main as run_main
47
+
48
+ return run_main(rest)
49
+
50
+ if subcommand == "dispatch":
51
+ from pilot_workers.cli.dispatch import main as dispatch_main
52
+
53
+ return dispatch_main(rest)
54
+
55
+ if subcommand == "template":
56
+ import pilot_workers
57
+
58
+ modes = ("code", "explore", "test", "review")
59
+ if len(rest) != 1 or rest[0] not in modes:
60
+ print(f"usage: pilot-workers template {{{'|'.join(modes)}}}", file=sys.stderr)
61
+ return 2
62
+ path = (
63
+ Path(pilot_workers.__file__).resolve().parent
64
+ / "data" / "templates" / f"{rest[0]}.md"
65
+ )
66
+ if not path.is_file():
67
+ print(f"error: template missing from package: {path}", file=sys.stderr)
68
+ return 1
69
+ sys.stdout.write(path.read_text(encoding="utf-8"))
70
+ return 0
71
+
72
+ if subcommand == "install":
73
+ from pilot_workers.cli.install import main as install_main
74
+
75
+ return install_main(rest)
76
+
77
+ if subcommand == "uninstall":
78
+ from pilot_workers.cli import install as install_mod
79
+
80
+ fn = getattr(install_mod, "uninstall_main", None)
81
+ if fn is None:
82
+ print("error: uninstall not available in this build", file=sys.stderr)
83
+ return 1
84
+ return fn(rest)
85
+
86
+ if subcommand == "credentials":
87
+ from pilot_workers.credentials import main as credentials_main
88
+
89
+ return _with_argv("pilot-workers credentials", rest, credentials_main)
90
+
91
+ if subcommand == "maintain":
92
+ from pilot_workers.maintain import main as maintain_main
93
+
94
+ return _with_argv("pilot-workers maintain", rest, maintain_main)
95
+
96
+ if subcommand == "runtime":
97
+ if rest != ["install"]:
98
+ print("usage: pilot-workers runtime install", file=sys.stderr)
99
+ return 2
100
+ import pilot_workers
101
+
102
+ script = (
103
+ Path(pilot_workers.__file__).resolve().parent
104
+ / "scripts"
105
+ / "install_runtime.sh"
106
+ )
107
+ return subprocess.run(["bash", str(script)]).returncode
108
+
109
+ print(f"error: unknown subcommand: {subcommand}", file=sys.stderr)
110
+ print(USAGE, end="", file=sys.stderr)
111
+ return 2
112
+
113
+
114
+ if __name__ == "__main__":
115
+ raise SystemExit(main())
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env python3
2
+ """CLI entry: pilot-workers run — dispatch a worker task."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from datetime import datetime, timezone
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import secrets
12
+ import sys
13
+
14
+ from pilot_workers import fmt_events, policy, providers, runtime
15
+ from pilot_workers.runners import get_runner
16
+
17
+
18
+ DEFAULT_TIMEOUT_S = 3600
19
+ DEFAULT_IDLE_TIMEOUT_S = 900
20
+
21
+
22
+ def load_task(args: argparse.Namespace) -> str:
23
+ if args.task is not None:
24
+ task = args.task
25
+ else:
26
+ path = Path(args.task_file).expanduser().resolve()
27
+ if not path.is_file():
28
+ raise RuntimeError(f"task file does not exist: {path}")
29
+ if path.stat().st_size > providers.MAX_TASK_BYTES:
30
+ raise RuntimeError(f"task file exceeds {providers.MAX_TASK_BYTES} bytes: {path}")
31
+ task = path.read_text(encoding="utf-8")
32
+ if not task.strip():
33
+ raise RuntimeError("task must not be empty")
34
+ if len(task.encode("utf-8")) > providers.MAX_TASK_BYTES:
35
+ raise RuntimeError(f"task exceeds {providers.MAX_TASK_BYTES} bytes")
36
+ return task.strip()
37
+
38
+
39
+ def validate_mode_arguments(args: argparse.Namespace) -> None:
40
+ if args.mode == "resume" and not args.session:
41
+ raise RuntimeError("--session is required when --mode resume is used")
42
+ if args.mode != "resume" and args.session:
43
+ raise RuntimeError("--session is only valid with --mode resume")
44
+ if args.mode == "resume" and args.worktree:
45
+ raise RuntimeError("resume the previously reported work directory; do not create a new worktree")
46
+
47
+
48
+ def dry_run_summary(provider: providers.Provider, mode: str, workdir: Path, *, permission_profile: str | None = None) -> dict:
49
+ runner = get_runner(provider.runner)
50
+ config = runner.build_config(provider, mode, permission_profile=permission_profile)
51
+ paths = providers.profile_paths(provider)
52
+ effective_profile = permission_profile or provider.permissions
53
+ bp = runner.binary_path()
54
+ return {
55
+ "type": "worker_runner.dry_run",
56
+ "provider": provider.key,
57
+ "runner": provider.runner,
58
+ "provider_id": provider.provider_id,
59
+ "endpoint": provider.base_url,
60
+ "model": provider.model,
61
+ "agent": policy.MODE_TO_AGENT[mode],
62
+ "mode": mode,
63
+ "workdir": str(workdir),
64
+ "sharing": config["share"],
65
+ "enabled_providers": config["enabled_providers"],
66
+ "permission_profile": effective_profile,
67
+ "profile": str(paths["root"]),
68
+ "credential": runtime.credential_metadata(provider, runner),
69
+ "runtime": str(bp) if bp else None,
70
+ "runtime_present": bp.is_file() if bp else False,
71
+ }
72
+
73
+
74
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
75
+ parser = argparse.ArgumentParser(description="Dispatch a bounded task to an isolated LLM worker.")
76
+ parser.add_argument("--provider", required=True, choices=sorted(providers.PROVIDERS))
77
+ parser.add_argument("--mode", required=True, choices=sorted(policy.MODE_TO_AGENT))
78
+ parser.add_argument("--workdir", required=True, help="Existing project directory.")
79
+ task_group = parser.add_mutually_exclusive_group(required=True)
80
+ task_group.add_argument("--task", help="Short task contract as a string.")
81
+ task_group.add_argument("--task-file", help="UTF-8 file containing the task contract.")
82
+ parser.add_argument("--session", help="Session ID for resume mode.")
83
+ parser.add_argument("--worktree", action="store_true", help="Create a detached worktree from HEAD.")
84
+ parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT_S)
85
+ parser.add_argument("--idle-timeout", type=int, default=DEFAULT_IDLE_TIMEOUT_S)
86
+ parser.add_argument("--permissions", help="Permission profile name (overrides provider YAML).")
87
+ parser.add_argument("--dry-run", action="store_true", help="Show routing metadata without invoking a model.")
88
+ return parser.parse_args(argv)
89
+
90
+
91
+ def main(argv: list[str] | None = None) -> int:
92
+ args = parse_args(argv)
93
+ try:
94
+ validate_mode_arguments(args)
95
+ if args.timeout < 0 or args.idle_timeout < 0:
96
+ raise RuntimeError("--timeout and --idle-timeout must be >= 0")
97
+ provider = providers.PROVIDERS[args.provider]
98
+ workdir = Path(args.workdir).expanduser().resolve()
99
+ if not workdir.is_dir():
100
+ raise RuntimeError(f"work directory does not exist: {workdir}")
101
+ task = load_task(args)
102
+
103
+ if args.dry_run:
104
+ print(json.dumps(dry_run_summary(provider, args.mode, workdir, permission_profile=args.permissions), indent=2))
105
+ return 0
106
+
107
+ runner = get_runner(provider.runner)
108
+ binary = runner.resolve_binary()
109
+ secret = runtime.credential_key(provider, runner)
110
+ if args.worktree:
111
+ workdir = runtime.create_detached_worktree(workdir, providers.worktrees_root())
112
+
113
+ config = runner.build_config(provider, args.mode, permission_profile=args.permissions)
114
+ env = runtime.build_environment(provider, runner.runner_environment(provider, config))
115
+ logs = providers.logs_root(provider)
116
+ runtime.ensure_private_directory(logs)
117
+ run_id = f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{secrets.token_hex(4)}"
118
+ log_path = logs / f"{run_id}.jsonl"
119
+ stderr_path = logs / f"{run_id}.stderr.log"
120
+ agent = policy.MODE_TO_AGENT[args.mode]
121
+ prompt = runner.format_task_input(task, args.mode)
122
+ command = runner.build_command(binary, provider, args.mode, workdir, run_id, args.session)
123
+
124
+ try:
125
+ renderer = fmt_events.FmtWriter(logs, provider.key, run_id, os.getpid())
126
+ except Exception as exc:
127
+ print(f"note: live log rendering unavailable ({exc})", file=sys.stderr)
128
+ renderer = None
129
+
130
+ started = {
131
+ "type": "worker_runner.started",
132
+ "provider": provider.key,
133
+ "runner": provider.runner,
134
+ "model": provider.model,
135
+ "mode": args.mode,
136
+ "agent": agent,
137
+ "run_id": run_id,
138
+ "workdir": str(workdir),
139
+ "log": str(log_path),
140
+ "stderr_log": str(stderr_path),
141
+ "rendered_log": str(logs / "latest.log") if renderer else None,
142
+ "timeout_s": args.timeout,
143
+ "idle_timeout_s": args.idle_timeout,
144
+ }
145
+ print(json.dumps(started), flush=True)
146
+ if renderer is not None:
147
+ try:
148
+ renderer.write_event(started)
149
+ except Exception as exc:
150
+ print(f"note: live log rendering disabled ({exc})", file=sys.stderr)
151
+ renderer = None
152
+
153
+ result = runtime.run_process(
154
+ command, env, prompt, log_path, stderr_path, secret,
155
+ renderer=renderer, timeout_s=args.timeout, idle_timeout_s=args.idle_timeout,
156
+ runner=runner,
157
+ )
158
+ secret = ""
159
+ summary = {
160
+ "type": "worker_runner.summary",
161
+ "provider": provider.key,
162
+ "runner": provider.runner,
163
+ "model": provider.model,
164
+ "mode": args.mode,
165
+ "agent": agent,
166
+ "run_id": run_id,
167
+ "session_id": result.session_id or args.session,
168
+ "workdir": str(workdir),
169
+ "log": str(log_path),
170
+ "stderr_log": str(stderr_path),
171
+ "rendered_log": started["rendered_log"],
172
+ "timed_out": result.timed_out,
173
+ "idle_timed_out": result.idle_timed_out,
174
+ "interrupted": result.interrupted,
175
+ "exit_code": result.exit_code,
176
+ }
177
+ print(json.dumps(summary))
178
+ if renderer is not None:
179
+ try:
180
+ renderer.write_event(summary)
181
+ renderer.finalize()
182
+ except Exception as exc:
183
+ print(f"note: live log rendering disabled ({exc})", file=sys.stderr)
184
+ return result.exit_code
185
+ except (OSError, RuntimeError, UnicodeError) as exc:
186
+ print(f"error: {exc}", file=sys.stderr)
187
+ return 1
188
+
189
+
190
+ if __name__ == "__main__":
191
+ raise SystemExit(main())
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ """Store worker credentials in provider-isolated auth files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import getpass
8
+ import json
9
+ import os
10
+ import tempfile
11
+
12
+ from pilot_workers.providers import PROVIDERS, workers_root
13
+ from pilot_workers.runners import get_runner
14
+
15
+
16
+ def credential_status(provider_key: str) -> dict[str, object]:
17
+ provider = PROVIDERS[provider_key]
18
+ runner = get_runner(provider.runner)
19
+ path = runner.credential_path(provider)
20
+ configured = False
21
+ secure_mode = False
22
+ error: str | None = None
23
+ if path.is_file():
24
+ secure_mode = (path.stat().st_mode & 0o077) == 0
25
+ try:
26
+ payload = json.loads(path.read_text(encoding="utf-8"))
27
+ try:
28
+ key = runner.parse_credential(provider, payload)
29
+ except (RuntimeError, TypeError, AttributeError) as exc:
30
+ error = str(exc)
31
+ key = ""
32
+ configured = bool(key and key.strip())
33
+ except (OSError, json.JSONDecodeError) as exc:
34
+ error = str(exc)
35
+ return {
36
+ "provider": provider_key,
37
+ "runner": provider.runner,
38
+ "provider_id": provider.provider_id,
39
+ "configured": configured,
40
+ "secure_mode": secure_mode,
41
+ "path": str(path),
42
+ "error": error,
43
+ }
44
+
45
+
46
+ def ensure_private_directories(destination) -> None:
47
+ current = workers_root()
48
+ target = destination.parent
49
+ while True:
50
+ current.mkdir(mode=0o700, parents=True, exist_ok=True)
51
+ current.chmod(0o700)
52
+ if current == target:
53
+ return
54
+ current = current / target.relative_to(current).parts[0]
55
+
56
+
57
+ def configure(provider_key: str) -> None:
58
+ provider = PROVIDERS[provider_key]
59
+ runner = get_runner(provider.runner)
60
+ key = getpass.getpass(f"{provider_key} API key (input hidden): ").strip()
61
+ if not key:
62
+ raise SystemExit(f"error: empty {provider_key} API key; no file was changed")
63
+ destination = runner.credential_path(provider)
64
+ ensure_private_directories(destination)
65
+ payload = runner.credential_payload(provider, key)
66
+ temporary_name: str | None = None
67
+ try:
68
+ with tempfile.NamedTemporaryFile(
69
+ mode="w", encoding="utf-8", dir=destination.parent,
70
+ prefix=".auth.", suffix=".tmp", delete=False,
71
+ ) as temporary:
72
+ temporary_name = temporary.name
73
+ os.chmod(temporary_name, 0o600)
74
+ json.dump(payload, temporary, separators=(",", ":"))
75
+ temporary.write("\n")
76
+ temporary.flush()
77
+ os.fsync(temporary.fileno())
78
+ os.replace(temporary_name, destination)
79
+ os.chmod(destination, 0o600)
80
+ finally:
81
+ key = ""
82
+ if temporary_name and os.path.exists(temporary_name):
83
+ os.unlink(temporary_name)
84
+ print(f"Configured {provider_key} in {destination} (mode 0600); key not displayed")
85
+
86
+
87
+ def main() -> int:
88
+ parser = argparse.ArgumentParser(description="Configure isolated worker credentials.")
89
+ parser.add_argument("provider", choices=[*sorted(PROVIDERS), "all"])
90
+ parser.add_argument("--status", action="store_true", help="Show metadata only; never show keys.")
91
+ args = parser.parse_args()
92
+ keys = sorted(PROVIDERS) if args.provider == "all" else [args.provider]
93
+ if args.status:
94
+ print(json.dumps([credential_status(item) for item in keys], indent=2))
95
+ return 0
96
+ for provider_key in keys:
97
+ configure(provider_key)
98
+ return 0
99
+
100
+
101
+ if __name__ == "__main__":
102
+ raise SystemExit(main())