diff --git a/src/nightshift/task.py b/src/nightshift/task.py index 6087b23..01310a9 100644 --- a/src/nightshift/task.py +++ b/src/nightshift/task.py @@ -56,8 +56,15 @@ async def run_task( # Workspace from agent config takes priority, then platform config. # Empty workspace gets a minimal temp dir (cwd would be "/" under systemd). workspace = agent.config.workspace or config.workspace + workspace_is_temp = False if not workspace: + if agent.config.stateful: + raise RuntimeError( + "Stateful agents require a configured workspace " + "(set agent.workspace or NIGHTSHIFT_WORKSPACE)." + ) workspace = tempfile.mkdtemp(prefix="nightshift-empty-ws-") + workspace_is_temp = True try: # Package agent source code and manifest for VM injection @@ -120,6 +127,8 @@ async def run_task( cleanup_package(pkg_dir) if staging_dir: shutil.rmtree(staging_dir, ignore_errors=True) + if workspace_is_temp and workspace: + shutil.rmtree(workspace, ignore_errors=True) await log.cleanup(run_id) diff --git a/src/nightshift/vm/pool.py b/src/nightshift/vm/pool.py index 461a92b..0894b1c 100644 --- a/src/nightshift/vm/pool.py +++ b/src/nightshift/vm/pool.py @@ -35,6 +35,7 @@ class _PoolEntry: pkg_dir: str staging_dir: str workspace_dest: str # original workspace path on host + workspace_is_temp: bool = False stateful: bool busy: bool = False idle_task: asyncio.Task | None = field(default=None, repr=False) @@ -91,16 +92,24 @@ async def checkout( # 2. No idle entry but under the limit → create placeholder if len(entries) < effective_max: workspace = agent.config.workspace or config.workspace + workspace_is_temp = False if not workspace: + if agent.config.stateful: + raise RuntimeError( + "Stateful agents require a configured workspace " + "(set agent.workspace or NIGHTSHIFT_WORKSPACE)." + ) # Empty workspace: create a minimal temp dir rather than - # falling back to cwd (which is "/" under systemd). + # falling back to cwd (which is '/' under systemd). workspace = tempfile.mkdtemp(prefix="nightshift-empty-ws-") + workspace_is_temp = True entry = _PoolEntry( vm=None, agent_id=agent_id, pkg_dir="", staging_dir="", workspace_dest=workspace, + workspace_is_temp=workspace_is_temp, stateful=agent.config.stateful, busy=True, ) @@ -306,6 +315,8 @@ async def _destroy_entry(self, entry: _PoolEntry) -> None: cleanup_package(entry.pkg_dir) if entry.staging_dir: shutil.rmtree(entry.staging_dir, ignore_errors=True) + if entry.workspace_is_temp and entry.workspace_dest: + shutil.rmtree(entry.workspace_dest, ignore_errors=True) logger.info("Pool entry cleanup complete for agent %s", entry.agent_id)