| """RECEPT Safety Bridge — Executes RECEPT programs through TerminalSafetyBroker. |
| |
| RECEPT's run() and rm() calls are routed through the TerminalSafetyBroker |
| instead of raw subprocess. This means: |
| - Destructive commands are blocked without @approved |
| - All commands are logged |
| - Violations are tracked |
| - No arbitrary shell execution |
| """ |
|
|
| import os |
| import re |
| import subprocess |
| import hashlib |
| import json |
| import uuid |
| from datetime import datetime |
| from typing import Dict, List, Optional, Any |
|
|
|
|
| class TerminalSafetyError(Exception): |
| pass |
|
|
|
|
| class SafeExecutionBroker: |
| """Safety broker for RECEPT execution. |
| |
| Checks every command against blocked patterns before execution. |
| Tracks violations and logs all commands. |
| |
| This replaces raw subprocess.run() in the RECEPT interpreter. |
| """ |
|
|
| BLOCKED_PATTERNS = [ |
| r'\brm\s+-rf?\b', |
| r'\brm\s+-r\b', |
| r'\bdd\b', |
| r'\bmkfs\b', |
| r'\bchmod\s+777\b', |
| r'\bchown\b', |
| r'\bsudo\b', |
| r'\bkill\s+-9\b', |
| r'\bpkill\b', |
| r'>\s*/dev/', |
| r'curl\s+.*\|\s*(bash|sh)', |
| r'wget\s+.*\|\s*(bash|sh)', |
| r'\bpip\s+install\b', |
| r'\bnpm\s+install\b', |
| r'\bbrew\s+install\b', |
| ] |
|
|
| SAFE_PATTERNS = [ |
| r'^ls\b', r'^cat\b', r'^head\b', r'^tail\b', |
| r'^grep\b', r'^find\b', r'^echo\b', r'^mkdir\b', |
| r'^test\b', r'^python3\s+-m\s+pytest\b', |
| r'^python3\s+-c\b', r'^git\s+(status|log|diff)\b', |
| r'^which\b', r'^wc\b', r'^sort\b', r'^uniq\b', |
| r'^diff\b', r'^file\b', r'^stat\b', |
| r'^screencapture\b', r'^shasum\b', |
| ] |
|
|
| def __init__(self, approved: bool = False): |
| self.approved = approved |
| self.violation_count = 0 |
| self.command_log: List[Dict] = [] |
| self.work_dir: Optional[str] = None |
|
|
| def set_work_dir(self, path: str): |
| self.work_dir = path |
|
|
| def check(self, command: str) -> Dict: |
| """Check command against safety rules.""" |
| classification = 'unknown' |
|
|
| for pattern in self.SAFE_PATTERNS: |
| if re.match(pattern, command): |
| classification = 'safe' |
| return {'allowed': True, 'reason': 'auto-approved', 'needs_approval': False} |
|
|
| for pattern in self.BLOCKED_PATTERNS: |
| if re.search(pattern, command): |
| classification = 'blocked' |
| if self.approved: |
| return {'allowed': True, 'reason': 'pre-approved by @approved', |
| 'needs_approval': False} |
| return {'allowed': False, 'reason': f'blocked: {pattern}', |
| 'needs_approval': True} |
|
|
| classification = 'unknown' |
| return {'allowed': True, 'reason': 'unknown, allowed by default', |
| 'needs_approval': False} |
|
|
| def execute(self, command: str, timeout: int = 30) -> Dict: |
| """Execute a command safely. Raises TerminalSafetyError if blocked.""" |
| check = self.check(command) |
|
|
| self.command_log.append({ |
| 'command': command, |
| 'timestamp': datetime.now().isoformat(), |
| 'classification': check['reason'], |
| 'allowed': check['allowed'], |
| }) |
|
|
| if not check['allowed']: |
| self.violation_count += 1 |
| raise TerminalSafetyError( |
| f"Command blocked: {check['reason']}. " |
| f"Add @approved annotation to allow. Command: {command[:80]}" |
| ) |
|
|
| try: |
| r = subprocess.run( |
| command, shell=True, |
| cwd=self.work_dir, |
| capture_output=True, text=True, timeout=timeout |
| ) |
| return { |
| 'returncode': r.returncode, |
| 'stdout': r.stdout, |
| 'stderr': r.stderr, |
| 'blocked': False, |
| } |
| except subprocess.TimeoutExpired: |
| return { |
| 'returncode': -1, |
| 'stdout': '', |
| 'stderr': f'TIMEOUT after {timeout}s', |
| 'blocked': False, |
| } |
|
|
| def safe_rm(self, path: str) -> Dict: |
| """Safely remove a file. Only works if @approved.""" |
| if not self.approved: |
| raise TerminalSafetyError( |
| f"rm() requires @approved annotation. Path: {path}" |
| ) |
| |
| if '..' in path or path.startswith('/'): |
| raise TerminalSafetyError( |
| f"rm() blocked: path traversal or absolute path not allowed: {path}" |
| ) |
| full_path = os.path.join(self.work_dir or '.', path) |
| try: |
| os.remove(full_path) |
| return {'returncode': 0, 'stdout': f'removed: {path}', 'stderr': '', 'blocked': False} |
| except Exception as e: |
| return {'returncode': 1, 'stdout': '', 'stderr': str(e), 'blocked': False} |
|
|
| def audit(self) -> List[Dict]: |
| return self.command_log |
|
|
| def violation_count(self) -> int: |
| return self.violation_count |
|
|
|
|
| class RECEPTSafeRunner: |
| """Runs RECEPT programs with safe execution. |
| |
| Integrates RECEPT interpreter with: |
| - SafeExecutionBroker (replaces raw subprocess) |
| - SQLiteReceiptStore (durable receipts) |
| - SelfImprovementLedger (false positive tracking) |
| |
| The terminal is the primary control channel. |
| Screen vision is observation only. |
| """ |
|
|
| def __init__(self, receipts_db: str = None, improvement_db: str = None): |
| from .receipt_store import SQLiteReceiptStore |
| from .improvement import SelfImprovementLedger |
|
|
| self.receipt_store = SQLiteReceiptStore(receipts_db) |
| self.improvement_ledger = SelfImprovementLedger(improvement_db) |
| self.broker: Optional[SafeExecutionBroker] = None |
|
|
| def run(self, source: str, work_dir: str = None) -> Dict: |
| """Run a RECEPT program safely. |
| |
| Args: |
| source: RECEPT source code |
| work_dir: Working directory for file operations |
| |
| Returns: |
| Execution result with receipts |
| """ |
| from membra_gpt.recept.interpreter import Interpreter, ReceptRuntimeError |
|
|
| work_dir = work_dir or os.path.join(os.getcwd(), 'questionos', 'recept_runs') |
| os.makedirs(work_dir, exist_ok=True) |
|
|
| |
| self.broker = SafeExecutionBroker() |
| self.broker.set_work_dir(work_dir) |
|
|
| |
| interp = Interpreter(receipts_dir=os.path.join(work_dir, 'receipts')) |
|
|
| |
| original_call_builtin = interp.call_builtin |
|
|
| def safe_call_builtin(name: str, args: List[Any]) -> Any: |
| if name == 'run': |
| cmd = args[0] if args else "" |
| result = self.broker.execute(cmd) |
| return result.get('stdout', '')[:1000] |
|
|
| elif name == 'write_file': |
| filename = args[0] if args else "" |
| content = args[1] if len(args) > 1 else "" |
| filepath = os.path.join(work_dir, filename) |
| os.makedirs(os.path.dirname(filepath), exist_ok=True) if os.path.dirname(filename) else None |
| with open(filepath, 'w') as f: |
| f.write(content) |
| return f"wrote {len(content)} bytes to {filename}" |
|
|
| elif name == 'read_file': |
| filename = args[0] if args else "" |
| filepath = os.path.join(work_dir, filename) |
| try: |
| with open(filepath) as f: |
| return f.read() |
| except Exception as e: |
| return f"read error: {e}" |
|
|
| elif name == 'rm': |
| path = args[0] if args else "" |
| result = self.broker.safe_rm(path) |
| return result.get('stdout', '') |
|
|
| elif name == 'rmdir': |
| path = args[0] if args else "" |
| if not self.broker.approved: |
| raise TerminalSafetyError( |
| f"rmdir() requires @approved annotation. Path: {path}" |
| ) |
| try: |
| os.rmdir(os.path.join(work_dir, path)) |
| return f"removed dir: {path}" |
| except Exception as e: |
| return f"rmdir error: {e}" |
|
|
| else: |
| return original_call_builtin(name, args) |
|
|
| interp.call_builtin = safe_call_builtin |
| interp.call_function = self._safe_call_function(interp) |
|
|
| |
| try: |
| result = interp.run(source) |
|
|
| |
| receipt = self.receipt_store.write( |
| agent='RECEPT', |
| action='program_executed', |
| artifact_path=work_dir, |
| commands_run=[cmd['command'] for cmd in self.broker.command_log], |
| result='success' if not result.get('errors') else 'partial', |
| details={ |
| 'capsule': result.get('capsule', 'unknown'), |
| 'endpoints': result.get('endpoints', []), |
| 'workflows': result.get('workflows', []), |
| 'functions': result.get('functions', []), |
| 'errors': result.get('errors', []), |
| 'broker_violations': self.broker.violation_count, |
| 'broker_log': self.broker.command_log[:20], |
| }, |
| ) |
|
|
| result['receipt'] = receipt |
| result['broker_audit'] = self.broker.audit() |
| result['work_dir'] = work_dir |
| return result |
|
|
| except TerminalSafetyError as e: |
| receipt = self.receipt_store.write( |
| agent='RECEPT', |
| action='program_blocked', |
| artifact_path=None, |
| commands_run=[cmd['command'] for cmd in self.broker.command_log], |
| result='blocked', |
| details={'error': str(e), 'violations': self.broker.violation_count}, |
| ) |
| return { |
| 'errors': [str(e)], |
| 'receipt': receipt, |
| 'broker_audit': self.broker.audit(), |
| 'blocked': True, |
| } |
|
|
| def _safe_call_function(self, interp): |
| """Wrap interpreter's call_function to respect @approved.""" |
| original = interp.call_function |
|
|
| def safe_call(name: str, args: List[Any]) -> Any: |
| if name in interp.functions: |
| fn = interp.functions[name] |
| if name in interp.DESTRUCTIVE_FUNCS and not fn.approved: |
| raise TerminalSafetyError( |
| f"Function '{name}' is destructive but not @approved." |
| ) |
| |
| if self.broker: |
| old_approved = self.broker.approved |
| self.broker.approved = fn.approved |
| result = original(name, args) |
| self.broker.approved = old_approved |
| return result |
| return original(name, args) |
|
|
| return safe_call |
|
|