From 4b07f00f9b5a06bfdbcc1759fd17fe4c8428a9bf Mon Sep 17 00:00:00 2001 From: Sean Evans Date: Tue, 21 Apr 2026 12:04:26 -0400 Subject: [PATCH] Refactor sandbox reset argument plumbing --- pyisolate/runtime/thread.py | 60 ++++++++++++++++++++++++++++++------- pyisolate/supervisor.py | 53 ++++++++++++-------------------- 2 files changed, 68 insertions(+), 45 deletions(-) diff --git a/pyisolate/runtime/thread.py b/pyisolate/runtime/thread.py index 8634391..b14fc21 100644 --- a/pyisolate/runtime/thread.py +++ b/pyisolate/runtime/thread.py @@ -478,6 +478,40 @@ def snapshot(self) -> dict: "child_work_max": self.child_work_max, } + def reset_config(self) -> dict[str, Any]: + """Return the runtime options consumed by ``reset`` for reconfiguration.""" + return { + "policy": self.policy, + "cpu_ms": self.cpu_quota_ms, + "mem_bytes": self.mem_quota_bytes, + "wall_time_ms": self.wall_time_ms, + "open_files_max": self.open_files_max, + "network_ops_max": self.network_ops_max, + "output_bytes_max": self.output_bytes_max, + "child_work_max": self.child_work_max, + "allowed_imports": sorted(self.allowed_imports) + if self.allowed_imports is not None + else None, + "numa_node": self.numa_node, + "capabilities": serialize_capabilities(self._capabilities), + } + + def apply_reset_config(self, config: dict[str, Any]) -> None: + """Apply a serialized runtime config produced by ``reset_config``.""" + self.policy = config.get("policy") + self.cpu_quota_ms = config.get("cpu_ms") + self.mem_quota_bytes = config.get("mem_bytes") + self.wall_time_ms = config.get("wall_time_ms") + self.open_files_max = config.get("open_files_max") + self.network_ops_max = config.get("network_ops_max") + self.output_bytes_max = config.get("output_bytes_max") + self.child_work_max = config.get("child_work_max") + self.numa_node = config.get("numa_node") + self.allowed_imports = self._merge_allowed_imports( + self.policy, config.get("allowed_imports") + ) + self._capabilities = deserialize_capabilities(config.get("capabilities")) + @staticmethod def _estimate_output_size(item: Any) -> int: if isinstance(item, bytes): @@ -619,17 +653,22 @@ def reset( """Reuse this thread for a new sandbox.""" old_path = getattr(self, "_cgroup_path", None) self.name = name - self.policy = policy - self.cpu_quota_ms = cpu_ms - self.mem_quota_bytes = mem_bytes - self.wall_time_ms = wall_time_ms - self.open_files_max = open_files_max - self.network_ops_max = network_ops_max - self.output_bytes_max = output_bytes_max - self.child_work_max = child_work_max - self.numa_node = numa_node + self.apply_reset_config( + { + "policy": policy, + "cpu_ms": cpu_ms, + "mem_bytes": mem_bytes, + "wall_time_ms": wall_time_ms, + "open_files_max": open_files_max, + "network_ops_max": network_ops_max, + "output_bytes_max": output_bytes_max, + "child_work_max": child_work_max, + "allowed_imports": allowed_imports, + "numa_node": numa_node, + "capabilities": capabilities, + } + ) self._bound_numa_node = None - self.allowed_imports = self._merge_allowed_imports(policy, allowed_imports) self._cpu_time = 0.0 self._mem_peak = 0 self._ops = 0 @@ -640,7 +679,6 @@ def reset( self._syscall_log = [] self._start_time = None self._cgroup_path = cgroup_path - self._capabilities = deserialize_capabilities(capabilities) self.termination_reason = None self._open_files = 0 self._network_ops = 0 diff --git a/pyisolate/supervisor.py b/pyisolate/supervisor.py index 1cadb4f..7830f2d 100644 --- a/pyisolate/supervisor.py +++ b/pyisolate/supervisor.py @@ -67,21 +67,12 @@ def quarantine(self, reason: str = "manual quarantine") -> None: self._supervisor.quarantine(self._thread.name, reason) def reset(self) -> None: + reset_config = self._thread.reset_config() + # Keep this reset path aligned with Supervisor.spawn warm-pool reuse. self._thread.reset( self._thread.name, - policy=self._thread.policy, - cpu_ms=self._thread.cpu_quota_ms, - mem_bytes=self._thread.mem_quota_bytes, - wall_time_ms=self._thread.wall_time_ms, - open_files_max=self._thread.open_files_max, - network_ops_max=self._thread.network_ops_max, - output_bytes_max=self._thread.output_bytes_max, - child_work_max=self._thread.child_work_max, - allowed_imports=sorted(self._thread.allowed_imports) - if self._thread.allowed_imports is not None - else None, - numa_node=self._thread.numa_node, cgroup_path=self._thread._cgroup_path, + **reset_config, ) def recycle(self) -> "Sandbox": @@ -254,42 +245,36 @@ def spawn( try: cg_path = cgroup.create(name, cpu_ms, mem_bytes) temp_dir = recovery.allocate_temp_dir(name) + reset_config = { + "policy": policy, + "cpu_ms": cpu_ms, + "mem_bytes": mem_bytes, + "wall_time_ms": wall_time_ms, + "open_files_max": open_files_max, + "network_ops_max": network_ops_max, + "output_bytes_max": output_bytes_max, + "child_work_max": child_work_max, + "allowed_imports": allowed_imports, + "numa_node": numa_node, + "capabilities": capabilities, + } if self._warm_pool: thread = self._warm_pool.pop() + # Intentionally mirrors Sandbox.reset by sharing reset_config. thread.reset( name, - policy=policy, - cpu_ms=cpu_ms, - mem_bytes=mem_bytes, - wall_time_ms=wall_time_ms, - open_files_max=open_files_max, - network_ops_max=network_ops_max, - output_bytes_max=output_bytes_max, - child_work_max=child_work_max, - allowed_imports=allowed_imports, - numa_node=numa_node, cgroup_path=cg_path, - capabilities=capabilities, + **reset_config, ) thread._on_violation = self._alerts.notify thread._tracer = self._tracer else: thread = SandboxThread( name=name, - policy=policy, - cpu_ms=cpu_ms, - mem_bytes=mem_bytes, - wall_time_ms=wall_time_ms, - open_files_max=open_files_max, - network_ops_max=network_ops_max, - output_bytes_max=output_bytes_max, - child_work_max=child_work_max, - allowed_imports=allowed_imports, + **reset_config, on_violation=self._alerts.notify, tracer=self._tracer, - numa_node=numa_node, cgroup_path=cg_path, - capabilities=capabilities, ) thread.start() thread._temp_dir = temp_dir