weft-macos-sandbox 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
env/
|
|
28
|
+
.venv
|
|
29
|
+
.weft/broker.db
|
|
30
|
+
|
|
31
|
+
# IDEs
|
|
32
|
+
.vscode/
|
|
33
|
+
.idea/
|
|
34
|
+
*.swp
|
|
35
|
+
*.swo
|
|
36
|
+
*~
|
|
37
|
+
|
|
38
|
+
# Testing
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
htmlcov/
|
|
43
|
+
.tox/
|
|
44
|
+
.nox/
|
|
45
|
+
.mypy_cache/
|
|
46
|
+
.dmypy.json
|
|
47
|
+
dmypy.json
|
|
48
|
+
.ruff_cache/
|
|
49
|
+
.ruff/
|
|
50
|
+
.pytest_cache/
|
|
51
|
+
|
|
52
|
+
# SimpleBroker specific
|
|
53
|
+
*.db-shm
|
|
54
|
+
*.db-wal
|
|
55
|
+
.broker.db*
|
|
56
|
+
test.db
|
|
57
|
+
benchmark_pragma.py
|
|
58
|
+
|
|
59
|
+
# OS
|
|
60
|
+
.DS_Store
|
|
61
|
+
Thumbs.db
|
|
62
|
+
|
|
63
|
+
# Temporary files
|
|
64
|
+
*.tmp
|
|
65
|
+
*.bak
|
|
66
|
+
*.log
|
|
67
|
+
|
|
68
|
+
# Multi-agent
|
|
69
|
+
.claude
|
|
70
|
+
.mcp.json
|
|
71
|
+
agent_history/
|
|
72
|
+
.broker.db
|
|
73
|
+
.broker.db-shm
|
|
74
|
+
.broker.db-wal
|
|
75
|
+
.broker.connection.done
|
|
76
|
+
.broker.connection.lock
|
|
77
|
+
.broker.optimization.done
|
|
78
|
+
.broker.optimization.lock
|
|
79
|
+
*comments*.md
|
|
80
|
+
.code/
|
|
81
|
+
# This is in context for agents but we don't want it to check it in here
|
|
82
|
+
simplebroker/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: weft-macos-sandbox
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: macOS sandbox runner plugin for Weft
|
|
5
|
+
Author-email: Van Lindberg <van@modelmonster.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: weft<1,>=0.6.4
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# weft-macos-sandbox
|
|
14
|
+
|
|
15
|
+
macOS sandbox runner plugin for Weft.
|
|
16
|
+
|
|
17
|
+
This extension adds the `macos-sandbox` runner via the `weft.runners`
|
|
18
|
+
entry-point group. It currently supports one-shot `command` TaskSpecs only and
|
|
19
|
+
uses `sandbox-exec` with a caller-provided profile.
|
|
20
|
+
|
|
21
|
+
Release tag:
|
|
22
|
+
|
|
23
|
+
- `weft_macos_sandbox/vX.Y.Z`
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# weft-macos-sandbox
|
|
2
|
+
|
|
3
|
+
macOS sandbox runner plugin for Weft.
|
|
4
|
+
|
|
5
|
+
This extension adds the `macos-sandbox` runner via the `weft.runners`
|
|
6
|
+
entry-point group. It currently supports one-shot `command` TaskSpecs only and
|
|
7
|
+
uses `sandbox-exec` with a caller-provided profile.
|
|
8
|
+
|
|
9
|
+
Release tag:
|
|
10
|
+
|
|
11
|
+
- `weft_macos_sandbox/vX.Y.Z`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "weft-macos-sandbox"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "macOS sandbox runner plugin for Weft"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Van Lindberg", email = "van@modelmonster.ai"},
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"weft>=0.6.4,<1",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=7.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.entry-points."weft.runners"]
|
|
25
|
+
macos-sandbox = "weft_macos_sandbox.plugin:get_runner_plugin"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build]
|
|
28
|
+
include = [
|
|
29
|
+
"weft_macos_sandbox/**/*.py",
|
|
30
|
+
"README.md",
|
|
31
|
+
]
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""macOS sandbox runner plugin for Weft.
|
|
2
|
+
|
|
3
|
+
Spec references:
|
|
4
|
+
- docs/specifications/01-Core_Components.md [CC-3.1], [CC-3.2], [CC-3.4]
|
|
5
|
+
- docs/specifications/02-TaskSpec.md [TS-1.3]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from simplebroker import BrokerTarget
|
|
19
|
+
from weft.core.runners import RunnerOutcome
|
|
20
|
+
from weft.core.runners.subprocess_runner import (
|
|
21
|
+
prepare_command_invocation,
|
|
22
|
+
run_monitored_subprocess,
|
|
23
|
+
)
|
|
24
|
+
from weft.core.tasks.runner import AgentSession, CommandSession
|
|
25
|
+
from weft.ext import (
|
|
26
|
+
RunnerCapabilities,
|
|
27
|
+
RunnerHandle,
|
|
28
|
+
RunnerPlugin,
|
|
29
|
+
RunnerRuntimeDescription,
|
|
30
|
+
)
|
|
31
|
+
from weft.helpers import kill_process_tree, terminate_process_tree
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MacOSSandboxRunner:
|
|
35
|
+
"""One-shot command runner that wraps commands with sandbox-exec."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
process_target: str | None,
|
|
41
|
+
args: Sequence[Any] | None,
|
|
42
|
+
env: Mapping[str, str] | None,
|
|
43
|
+
working_dir: str | None,
|
|
44
|
+
timeout: float | None,
|
|
45
|
+
limits: Any | None,
|
|
46
|
+
monitor_class: str | None,
|
|
47
|
+
monitor_interval: float | None,
|
|
48
|
+
runner_options: Mapping[str, Any] | None,
|
|
49
|
+
db_path: BrokerTarget | str | None = None,
|
|
50
|
+
config: dict[str, Any] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
if not isinstance(process_target, str) or not process_target.strip():
|
|
53
|
+
raise ValueError("macOS sandbox runner requires spec.process_target")
|
|
54
|
+
|
|
55
|
+
options = dict(runner_options or {})
|
|
56
|
+
profile = options.get("profile")
|
|
57
|
+
if not isinstance(profile, str) or not profile.strip():
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"macOS sandbox runner requires spec.runner.options.profile"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
self._process_target = process_target.strip()
|
|
63
|
+
self._args = list(args or [])
|
|
64
|
+
self._env = {str(key): str(value) for key, value in dict(env or {}).items()}
|
|
65
|
+
self._working_dir = working_dir
|
|
66
|
+
self._timeout = timeout
|
|
67
|
+
self._limits = limits
|
|
68
|
+
self._monitor_class = monitor_class
|
|
69
|
+
self._monitor_interval = monitor_interval or 1.0
|
|
70
|
+
self._profile = str(Path(profile).expanduser())
|
|
71
|
+
self._sandbox_binary = str(options.get("sandbox_binary") or "sandbox-exec")
|
|
72
|
+
self._db_path = db_path
|
|
73
|
+
self._config = config
|
|
74
|
+
|
|
75
|
+
def run(self, work_item: Any) -> RunnerOutcome:
|
|
76
|
+
return self.run_with_hooks(work_item)
|
|
77
|
+
|
|
78
|
+
def run_with_hooks(
|
|
79
|
+
self,
|
|
80
|
+
work_item: Any,
|
|
81
|
+
*,
|
|
82
|
+
cancel_requested: Callable[[], bool] | None = None,
|
|
83
|
+
on_worker_started: Callable[[int | None], None] | None = None,
|
|
84
|
+
on_runtime_handle_started: Callable[[RunnerHandle], None] | None = None,
|
|
85
|
+
) -> RunnerOutcome:
|
|
86
|
+
command, stdin_data = prepare_command_invocation(
|
|
87
|
+
self._process_target,
|
|
88
|
+
work_item,
|
|
89
|
+
args=self._args,
|
|
90
|
+
)
|
|
91
|
+
env_vars = os.environ.copy()
|
|
92
|
+
env_vars.update(self._env)
|
|
93
|
+
process = subprocess.Popen(
|
|
94
|
+
[self._sandbox_binary, "-f", self._profile, *command],
|
|
95
|
+
stdin=subprocess.PIPE if stdin_data is not None else None,
|
|
96
|
+
stdout=subprocess.PIPE,
|
|
97
|
+
stderr=subprocess.PIPE,
|
|
98
|
+
text=True,
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
errors="replace",
|
|
101
|
+
cwd=self._working_dir or None,
|
|
102
|
+
env=env_vars,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _stop_runtime() -> None:
|
|
106
|
+
terminate_process_tree(process.pid or -1, timeout=0.2)
|
|
107
|
+
|
|
108
|
+
def _kill_runtime() -> None:
|
|
109
|
+
kill_process_tree(process.pid or -1, timeout=0.2)
|
|
110
|
+
|
|
111
|
+
runtime_handle = RunnerHandle(
|
|
112
|
+
runner_name="macos-sandbox",
|
|
113
|
+
runtime_id=str(process.pid),
|
|
114
|
+
host_pids=(process.pid,) if process.pid is not None else (),
|
|
115
|
+
metadata={"profile": self._profile},
|
|
116
|
+
)
|
|
117
|
+
return run_monitored_subprocess(
|
|
118
|
+
process=process,
|
|
119
|
+
stdin_data=stdin_data,
|
|
120
|
+
timeout=self._timeout,
|
|
121
|
+
limits=self._limits,
|
|
122
|
+
monitor_class=self._monitor_class,
|
|
123
|
+
monitor_interval=self._monitor_interval,
|
|
124
|
+
monitor=None,
|
|
125
|
+
db_path=self._db_path,
|
|
126
|
+
config=self._config,
|
|
127
|
+
runtime_handle=runtime_handle,
|
|
128
|
+
cancel_requested=cancel_requested,
|
|
129
|
+
on_worker_started=on_worker_started,
|
|
130
|
+
on_runtime_handle_started=on_runtime_handle_started,
|
|
131
|
+
stop_runtime=_stop_runtime,
|
|
132
|
+
kill_runtime=_kill_runtime,
|
|
133
|
+
worker_pid=process.pid,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def start_session(self) -> CommandSession:
|
|
137
|
+
raise ValueError("macOS sandbox runner does not support interactive sessions")
|
|
138
|
+
|
|
139
|
+
def start_agent_session(self) -> AgentSession:
|
|
140
|
+
raise ValueError("macOS sandbox runner does not support agent sessions")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class MacOSSandboxRunnerPlugin:
|
|
144
|
+
"""Runner plugin for macOS sandboxed one-shot command tasks."""
|
|
145
|
+
|
|
146
|
+
name = "macos-sandbox"
|
|
147
|
+
capabilities = RunnerCapabilities(
|
|
148
|
+
supported_types=("command",),
|
|
149
|
+
supports_interactive=False,
|
|
150
|
+
supports_persistent=False,
|
|
151
|
+
supports_agent_sessions=False,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def check_version(self) -> None:
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def validate_taskspec(
|
|
158
|
+
self,
|
|
159
|
+
taskspec_payload: Mapping[str, Any],
|
|
160
|
+
*,
|
|
161
|
+
preflight: bool = False,
|
|
162
|
+
) -> None:
|
|
163
|
+
spec = _require_mapping(taskspec_payload.get("spec"), name="spec")
|
|
164
|
+
if spec.get("type") != "command":
|
|
165
|
+
raise ValueError("macOS sandbox runner supports only spec.type='command'")
|
|
166
|
+
if bool(spec.get("interactive", False)):
|
|
167
|
+
raise ValueError("macOS sandbox runner does not support interactive tasks")
|
|
168
|
+
if bool(spec.get("persistent", False)):
|
|
169
|
+
raise ValueError("macOS sandbox runner does not support persistent tasks")
|
|
170
|
+
|
|
171
|
+
runner = _require_mapping(spec.get("runner"), name="spec.runner")
|
|
172
|
+
options = _require_mapping(runner.get("options"), name="spec.runner.options")
|
|
173
|
+
profile = options.get("profile")
|
|
174
|
+
if not isinstance(profile, str) or not profile.strip():
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"macOS sandbox runner requires spec.runner.options.profile"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if preflight:
|
|
180
|
+
if sys.platform != "darwin":
|
|
181
|
+
raise ValueError("macOS sandbox runner is available only on macOS")
|
|
182
|
+
executable = shutil.which(
|
|
183
|
+
str(options.get("sandbox_binary") or "sandbox-exec")
|
|
184
|
+
)
|
|
185
|
+
if executable is None:
|
|
186
|
+
raise ValueError("sandbox-exec is not available on PATH")
|
|
187
|
+
profile_path = Path(profile).expanduser()
|
|
188
|
+
if not profile_path.exists():
|
|
189
|
+
raise ValueError(f"Sandbox profile does not exist: {profile_path}")
|
|
190
|
+
|
|
191
|
+
def create_runner(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
target_type: str,
|
|
195
|
+
tid: str | None,
|
|
196
|
+
function_target: str | None,
|
|
197
|
+
process_target: str | None,
|
|
198
|
+
agent: Mapping[str, Any] | None,
|
|
199
|
+
args: Sequence[Any] | None,
|
|
200
|
+
kwargs: Mapping[str, Any] | None,
|
|
201
|
+
env: Mapping[str, str] | None,
|
|
202
|
+
working_dir: str | None,
|
|
203
|
+
timeout: float | None,
|
|
204
|
+
limits: Any | None,
|
|
205
|
+
monitor_class: str | None,
|
|
206
|
+
monitor_interval: float | None,
|
|
207
|
+
runner_options: Mapping[str, Any] | None,
|
|
208
|
+
persistent: bool,
|
|
209
|
+
interactive: bool,
|
|
210
|
+
db_path: BrokerTarget | str | None = None,
|
|
211
|
+
config: dict[str, Any] | None = None,
|
|
212
|
+
) -> MacOSSandboxRunner:
|
|
213
|
+
del target_type, tid, function_target, agent, kwargs, persistent, interactive
|
|
214
|
+
return MacOSSandboxRunner(
|
|
215
|
+
process_target=process_target,
|
|
216
|
+
args=args,
|
|
217
|
+
env=env,
|
|
218
|
+
working_dir=working_dir,
|
|
219
|
+
timeout=timeout,
|
|
220
|
+
limits=limits,
|
|
221
|
+
monitor_class=monitor_class,
|
|
222
|
+
monitor_interval=monitor_interval,
|
|
223
|
+
runner_options=runner_options,
|
|
224
|
+
db_path=db_path,
|
|
225
|
+
config=config,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def stop(self, handle: RunnerHandle, *, timeout: float = 2.0) -> bool:
|
|
229
|
+
if not handle.host_pids:
|
|
230
|
+
return False
|
|
231
|
+
for pid in handle.host_pids:
|
|
232
|
+
terminate_process_tree(pid, timeout=timeout, kill_after=False)
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
def kill(self, handle: RunnerHandle, *, timeout: float = 2.0) -> bool:
|
|
236
|
+
if not handle.host_pids:
|
|
237
|
+
return False
|
|
238
|
+
for pid in handle.host_pids:
|
|
239
|
+
kill_process_tree(pid, timeout=timeout)
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
def describe(self, handle: RunnerHandle) -> RunnerRuntimeDescription | None:
|
|
243
|
+
state = "missing"
|
|
244
|
+
for pid in handle.host_pids:
|
|
245
|
+
if _pid_exists(pid):
|
|
246
|
+
state = "running"
|
|
247
|
+
break
|
|
248
|
+
return RunnerRuntimeDescription(
|
|
249
|
+
runner_name="macos-sandbox",
|
|
250
|
+
runtime_id=handle.runtime_id,
|
|
251
|
+
state=state,
|
|
252
|
+
metadata=dict(handle.metadata),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
_PLUGIN = MacOSSandboxRunnerPlugin()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def get_runner_plugin() -> RunnerPlugin:
|
|
260
|
+
return _PLUGIN
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _require_mapping(value: object, *, name: str) -> Mapping[str, Any]:
|
|
264
|
+
if not isinstance(value, Mapping):
|
|
265
|
+
raise ValueError(f"{name} must be an object")
|
|
266
|
+
return value
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _pid_exists(pid: int) -> bool:
|
|
270
|
+
if pid <= 0:
|
|
271
|
+
return False
|
|
272
|
+
try:
|
|
273
|
+
os.kill(pid, 0)
|
|
274
|
+
except OSError:
|
|
275
|
+
return False
|
|
276
|
+
return True
|