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,5 @@
1
+ """macOS sandbox runner plugin for Weft."""
2
+
3
+ from .plugin import get_runner_plugin
4
+
5
+ __all__ = ["get_runner_plugin"]
@@ -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