prime-agent-runtime: process adoption, descendant ancestry walking, and runtime layout resolution
11 unmodified lines12131415161718192060 unmodified lines8182838485868788899 unmodified lines9910010110210310410510610716 unmodified lines124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258133 unmodified linesExpand all11 unmodified linesfrom rlm.bash import ( BashHandle, HostProfile, _check_command_guards, adopt_process, bash, get_host_profile, is_pid_owned, kill_process,60 unmodified lines
err = str(ctx.exception) self.assertIn("kill_process rejects pid 999999", err) self.assertIn("not owned by this session", err) self.assertIn("rlm.adopt_process", err) self.assertIn("h.kill()", err) self.assertIn("Active owned PIDs", err)
def test_falsifier_cannot_signal_unowned_process(self):9 unmodified lines with self.assertRaises(PermissionError) as ctx: eval(code, {"os": os, "__name__": "__main__"}) self.assertIn("os.kill rejects pid 999999", str(ctx.exception)) self.assertIn("not owned by this session", str(ctx.exception)) self.assertIn("rlm.adopt_process", str(ctx.exception)) self.assertIn("kill_process(pid)", str(ctx.exception))
def test_user_cell_os_kill_on_owned_pid_is_permitted(self): """Direct os.kill from a user cell on an owned PID is permitted."""16 unmodified lines with self.assertRaises(ValueError) as ctx: _check_command_guards(cmd) msg = str(ctx.exception) self.assertIn("kill_process(pid)", msg) self.assertIn("rlm.adopt_process", msg) self.assertIn("h.kill()", msg)
def test_descendant_process_is_owned_via_ancestry_walk(self): """A grandchild process spawned inside an owned process is recognized as owned.""" proc = subprocess.Popen( ["sh", "-c", "sleep 60 & echo $!; wait"], stdout=subprocess.PIPE, text=True, ) try: register_owned_process(proc.pid, "sh -c 'sleep 60 & echo $!; wait'") assert proc.stdout is not None first_line = proc.stdout.readline().strip() self.assertTrue(first_line, "No output received from grandchild spawn") grandchild_pid = int(first_line)
self.assertTrue(is_pid_owned(grandchild_pid)) signalled = kill_process(grandchild_pid, sig=signal.SIGTERM) self.assertTrue(signalled) finally: proc.kill() proc.wait(timeout=5)
def test_pid_1_and_unrelated_process_refused(self): """PID 1 and unrelated system processes are strictly refused.""" self.assertFalse(is_pid_owned(1)) with self.assertRaises(ValueError) as ctx: kill_process(1) self.assertIn("not owned by this session", str(ctx.exception))
fake_pid = 888888 self.assertFalse(is_pid_owned(fake_pid)) with self.assertRaises(ValueError) as ctx: kill_process(fake_pid) self.assertIn("not owned by this session", str(ctx.exception))
def test_adopt_process_records_and_enables_signaling(self): """rlm.adopt_process(pid, *, reason=...) enables signaling for external test subject.""" dummy_proc = subprocess.Popen(["sleep", "60"]) try: pid = dummy_proc.pid self.assertFalse(is_pid_owned(pid))
lease = rlm.adopt_process(pid, reason="private daemon test subject in tmux") self.assertEqual(lease.pid, pid) self.assertEqual(lease.reason, "private daemon test subject in tmux") self.assertTrue(is_pid_owned(pid))
records = list_owned_processes() adopted_record = next((r for r in records if r["pid"] == pid), None) self.assertIsNotNone(adopted_record) self.assertEqual(adopted_record["reason"], "private daemon test subject in tmux")
signalled = kill_process(pid, sig=signal.SIGTERM) self.assertTrue(signalled) dummy_proc.wait(timeout=5) self.assertIsNotNone(dummy_proc.poll()) finally: if dummy_proc.poll() is None: dummy_proc.kill() dummy_proc.wait(timeout=5)
def test_adopt_process_without_reason_is_refused(self): """Adoption without a non-empty reason is refused.""" dummy_proc = subprocess.Popen(["sleep", "10"]) try: with self.assertRaises(ValueError) as ctx: rlm.adopt_process(dummy_proc.pid, reason="") self.assertIn("non-empty reason", str(ctx.exception))
with self.assertRaises(ValueError) as ctx: rlm.adopt_process(dummy_proc.pid, reason=" ") self.assertIn("non-empty reason", str(ctx.exception)) finally: dummy_proc.kill() dummy_proc.wait(timeout=5)
def test_adopt_process_invalid_pid_is_refused(self): """Adoption of PID 1 or invalid PID is refused.""" with self.assertRaises(ValueError) as ctx: rlm.adopt_process(1, reason="cannot adopt init") self.assertIn("valid non-system PID", str(ctx.exception))
with self.assertRaises(ValueError) as ctx: rlm.adopt_process(-5, reason="invalid negative pid") self.assertIn("valid non-system PID", str(ctx.exception))
def test_adoption_is_journaled(self): """Adopting a process appends a record to the session orphan journal.""" dummy_proc = subprocess.Popen(["sleep", "10"]) import tempfile, json with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tf: journal_path = tf.name try: with mock.patch.dict(os.environ, {"PRIME_AGENT_INTERNAL_ORPHAN_PROCESS_JOURNAL": journal_path}): rlm.adopt_process(dummy_proc.pid, reason="auditable daemon adoption")
with open(journal_path) as f: lines = f.readlines() records = [json.loads(line) for line in lines if line.strip()] self.assertTrue(records) rec = records[-1] self.assertEqual(rec["pid"], dummy_proc.pid) self.assertEqual(rec["action"], "adopted") self.assertEqual(rec["reason"], "auditable daemon adoption") finally: dummy_proc.kill() dummy_proc.wait(timeout=5) if os.path.exists(journal_path): os.remove(journal_path)
def test_refusal_messages_name_adopt_process(self): """Refusal messages across kill_process, os.kill, and bash explicitly name adopt_process.""" with self.assertRaises(ValueError) as ctx: kill_process(999999) self.assertIn("rlm.adopt_process", str(ctx.exception))
code = compile("import os; os.kill(999999, 15)", "<cell-foreign-kill-adopt>", "exec") with self.assertRaises(PermissionError) as ctx: eval(code, {"os": os, "__name__": "__main__"}) self.assertIn("rlm.adopt_process", str(ctx.exception))
with self.assertRaises(ValueError) as ctx: _check_command_guards("kill 12345") self.assertIn("rlm.adopt_process", str(ctx.exception))
# ----------------------------------------------------------------------- # 2. Power-Control Command Guards # -----------------------------------------------------------------------
133 unmodified linesExpand all14 unmodified lines151617181920212223898 unmodified linesExpand all92292392492592692792892993049 unmodified lines98098198298398498598698798832 unmodified lines14 unmodified lines BashResult, HostProfile, ProcessLease, Workspace, adopt_process, bash, create_workspace, get_host_profile, is_pid_owned,898 unmodified linesExpand all
async def model_info(self, model_id: str | None = None) -> RLMModelInfo | None: return await get_model_info(model_id)
adopt_process = staticmethod(adopt_process) kill_process = staticmethod(kill_process) signal_process = staticmethod(signal_process) list_owned_processes = staticmethod(list_owned_processes) is_pid_owned = staticmethod(is_pid_owned)49 unmodified lines "create_session", "HostProfile", "ProcessLease", "RefinementEvent", "adopt_process", "bash", "delete_subagent", "get_host_profile", "is_pid_owned",32 unmodified lines32 unmodified lines3334353637383940411638 unmodified linesExpand all1680168116821683168316841684168516861687168816891181 unmodified linesExpand all32 unmodified linesfrom . import _winjobfrom .host_safety import ( HostProfile, ProcessLease, adopt_process, check_power_control, check_vram_command, clear_owned_processes, get_host_profile,1638 unmodified linesExpand all else "" ) return ( f"bash() rejects `{command}`: {reason}.{transport} Kill only what this command owns: " "`h = bash(cmd); h.kill()` or scoped `kill_process(pid)` for a command you started, `kill -TERM $!` or " "`pkill -P $$ name` for a child of this command, or `kill -- -$$` for this command's group." "`h = bash(cmd); h.kill()`, scoped `kill_process(pid)`, or adopt via `rlm.adopt_process(pid, reason=...)` " "for a command you started, `kill -TERM $!` or `pkill -P $$ name` for a child of this command, or `kill -- -$$` for this command's group." )
def _reject_unsafe_kill(words: list[str], *, remote: bool) -> None:1181 unmodified linesExpand all22 unmodified lines23242526272829303132333435363738394020 unmodified lines616263646263656667686970717273747576777879808182838485868788899091929394959697986599100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129711301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032047520520620720820921021121221321421521621735 unmodified lines253254255256126127257258259130260261262263264265266267268136269270271272273331 unmodified linesExpand all22 unmodified lines# ---------------------------------------------------------------------------# Process Ownership Registry & Scoped Process Control# ---------------------------------------------------------------------------
import subprocess
@dataclassclass ProcessLease: pid: int command: str started_at: float handle: Any | None = None reaped: bool = False reason: str | None = None
_OWNED_LEASES: dict[int, ProcessLease] = {}_LEASE_LOCK = threading.Lock()20 unmodified lines if lease is not None: lease.reaped = True
def is_pid_owned(pid: int) -> bool: """True only if the pid was started by bash() in this session."""def _get_parent_pid(pid: int) -> int | None: """Read PPID on Linux (/proc) or POSIX (ps). Returns None on error or for PID <= 1.""" if pid <= 1: return None proc_status = Path(f"/proc/{pid}/status") if proc_status.exists(): try: for line in proc_status.read_text().splitlines(): if line.startswith("PPid:"): return int(line.split()[1]) except (OSError, ValueError, IndexError): pass try: res = subprocess.run( ["ps", "-o", "ppid=", "-p", str(pid)], capture_output=True, text=True, timeout=2.0, ) if res.returncode == 0: val = res.stdout.strip() if val.isdigit(): return int(val) except Exception: pass return None
def is_pid_owned(pid: int, max_hops: int = 8) -> bool: """True if the pid was started by bash(), adopted in this session, or is a descendant.""" if pid <= 1: return False
with _LEASE_LOCK: return pid in _OWNED_LEASES if pid in _OWNED_LEASES: return True
# Walk ancestry up to max_hops to discover if any ancestor is an owned lease current = pid hops = 0 visited: set[int] = {pid} while hops < max_hops: ppid = _get_parent_pid(current) if ppid is None or ppid <= 1 or ppid in visited: break visited.add(ppid) with _LEASE_LOCK: if ppid in _OWNED_LEASES: # Cache descendant ownership for O(1) subsequent lookups _OWNED_LEASES[pid] = ProcessLease( pid=pid, command=f"descendant of {ppid}", started_at=time.time(), reason=f"ancestry walk matched ancestor {ppid}", ) return True current = ppid hops += 1
return False
def get_owned_process(pid: int) -> ProcessLease | None: """Return the ownership record for a pid, or None if foreign.""" with _LEASE_LOCK: return _OWNED_LEASES.get(pid) lease = _OWNED_LEASES.get(pid) if lease is not None: return lease if is_pid_owned(pid): with _LEASE_LOCK: return _OWNED_LEASES.get(pid) return None
def _record_journal_adoption(pid: int, reason: str) -> None: """Append an adoption entry to the session process journal.""" from datetime import datetime, timezone import json
path = os.environ.get("PRIME_AGENT_INTERNAL_ORPHAN_PROCESS_JOURNAL") if not path: return owner = os.environ.get("PRIME_AGENT_KERNEL_OWNER_PID") owner_pid = None if owner: try: owner_pid = int(owner) except ValueError: pass record: dict[str, Any] = { "version": 1, "pid": pid, "action": "adopted", "reason": reason, "ownerPid": owner_pid, "kernelPid": os.getpid(), "active": True, "recordedAt": datetime.now(timezone.utc).isoformat(), } data = (json.dumps(record) + "\n").encode() try: fd = os.open(path, os.O_WRONLY | os.O_APPEND | os.O_CREAT, 0o600) try: view = memoryview(data) while view: written = os.write(fd, view) if written <= 0: break view = view[written:] os.fsync(fd) finally: os.close(fd) except OSError: pass
def adopt_process(pid: int, *, reason: str) -> ProcessLease: """Adopt an externally started process into the session's owned-process lease.""" if not isinstance(pid, int) or pid <= 1: raise ValueError(f"adopt_process requires a valid non-system PID (> 1), got {pid!r}") if not isinstance(reason, str) or not reason.strip(): raise ValueError("adopt_process requires a non-empty reason string")
clean_reason = reason.strip() with _LEASE_LOCK: lease = ProcessLease( pid=pid, command=f"adopted: {clean_reason}", started_at=time.time(), reason=clean_reason, handle=None, reaped=False, ) _OWNED_LEASES[pid] = lease
_record_journal_adoption(pid, clean_reason) return lease
def list_owned_processes() -> list[dict[str, Any]]: """List all processes started by bash() in this session.""" """List all processes started or adopted in this session.""" with _LEASE_LOCK: return [ { "pid": lease.pid, "command": lease.command, "started_at": lease.started_at, "reaped": lease.reaped, "reason": lease.reason, } for lease in _OWNED_LEASES.values() ]
35 unmodified lines
if not isinstance(target, int): raise TypeError(f"target must be an integer PID or BashHandle, got {type(target).__name__}")
lease = get_owned_process(target) if lease is None: if not is_pid_owned(target): raise ValueError( f"kill_process rejects pid {target}: process is not owned by this session. " "Scoped process control only permits signalling processes started by bash() in this session. " "Scoped process control only permits signalling processes started by bash() in this session " "or explicitly adopted via `rlm.adopt_process(pid, reason=...)`. " "If you started the process with `h = bash(...)`, use `h.kill()` or pass `h.pid`. " f"Active owned PIDs: {list_owned_pids()}." )
lease = get_owned_process(target)
# If the lease has an active BashHandle with .kill: if lease.handle is not None and hasattr(lease.handle, "kill"): if lease is not None and lease.handle is not None and hasattr(lease.handle, "kill"): lease.handle.kill(sig=sig, grace=grace) return True
# Fallback to process group signal331 unmodified linesExpand all46 unmodified lines4748495051525354555657585960616263646566676869707172736074757677784 unmodified lines838485867387888990911 unmodified line9394959683979899100101138 unmodified linesExpand all2402412422432302442452462472482495 unmodified lines25525625725824425926026126226326410 unmodified lines46 unmodified linesos._prime_orig_system = _ORIG_OS_SYSTEMos._prime_orig_kill = _ORIG_OS_KILLos._prime_orig_killpg = _ORIG_OS_KILLPG
_RLM_PKG_DIR = str(Path(__file__).resolve().parent)
def _is_runtime_file(filename: str) -> bool: if not filename: return False if "/prime-agent-runtime/" in filename: return True try: resolved = str(Path(filename).resolve()) return resolved.startswith(_RLM_PKG_DIR) except Exception: return _RLM_PKG_DIR in filename
def _is_exempt_caller(frame: Any) -> bool: f = frame while f: mod = f.f_globals.get("__name__", "") filename = f.f_code.co_filename or ""
# If we reached a user cell, __main__ (outside runtime), stdin, or string evaluation: if ( (mod == "__main__" and "/prime-agent-runtime/" not in filename) (mod == "__main__" and not _is_runtime_file(filename)) or filename.startswith("<cell-") or filename in ("<stdin>", "<string>") ): return False4 unmodified lines if caller: c_fn = caller.f_code.co_filename or "" c_mod = caller.f_globals.get("__name__", "") if ( (c_mod == "__main__" and "/prime-agent-runtime/" not in c_fn) (c_mod == "__main__" and not _is_runtime_file(c_fn)) or c_fn.startswith("<cell-") or c_fn in ("<stdin>", "<string>") ): return False1 unmodified line if mod in _EXEMPT_MODULE_NAMES or (mod and any(mod.startswith(m + ".") for m in _EXEMPT_MODULE_NAMES)): return True if ( "/skills/" in filename or "/prime-agent-runtime/" in filename or _is_runtime_file(filename) or "/site-packages/" in filename or "/lib/python" in filename ): return True138 unmodified linesExpand all from .host_safety import is_pid_owned if pid != os.getpid() and not is_pid_owned(pid): raise PermissionError( f"os.kill rejects pid {pid}: process is not owned by this session. " "Scoped process control only permits signalling processes started by bash() in this session. " "Scoped process control only permits signalling processes started by bash() in this session " "or explicitly adopted via `rlm.adopt_process(pid, reason=...)`. " "Use `kill_process(pid)` or `h.kill()` for an owned process." ) return _ORIG_OS_KILL(pid, sig)
5 unmodified lines from .host_safety import is_pid_owned if pgid != os.getpgrp() and not is_pid_owned(pgid): raise PermissionError( f"os.killpg rejects pgid {pgid}: process group is not owned by this session. " "Scoped process control only permits signalling processes started by bash() in this session. " "Scoped process control only permits signalling processes started by bash() in this session " "or explicitly adopted via `rlm.adopt_process(pid, reason=...)`. " "Use `kill_process(pid)` or `h.kill()` for an owned process." ) return _ORIG_OS_KILLPG(pgid, sig)
10 unmodified lines