|
17 | 17 | 'file_delete': 1.0, # Maximum contradiction density |
18 | 18 | 'file_overwrite': 0.9, # High - data loss possible |
19 | 19 | 'send_message': 0.85, # High - external communication |
| 20 | + 'form_submit': 0.85, # High - irreversible external action |
20 | 21 | 'execute_script': 0.8, # High - unknown side effects |
| 22 | + 'browser_interact': 0.7, # Medium-high - external state change |
21 | 23 | 'network_request': 0.6, # Medium - depends on destination |
22 | 24 | 'file_write': 0.4, # Low-medium - new file creation |
23 | 25 | 'file_read': 0.1, # Low - read only, no state change |
@@ -280,6 +282,125 @@ def execute_script(self, script: str, |
280 | 282 | except Exception as e: |
281 | 283 | return {'status': 'error', 'error': str(e)} |
282 | 284 |
|
| 285 | + def browser_interact(self, url: str, |
| 286 | + action: str, |
| 287 | + selector: str = None, |
| 288 | + value: str = None, |
| 289 | + wait_for: str = None, |
| 290 | + timeout: int = 30) -> Dict[str, Any]: |
| 291 | + """ |
| 292 | + Full headless browser control via Playwright. |
| 293 | + No screenshots. No coordinates. DOM selector based. |
| 294 | + CPOL-gated by action type. |
| 295 | +
|
| 296 | + action: 'navigate' | 'click' | 'fill' | 'select' | |
| 297 | + 'submit' | 'scrape' | 'screenshot' |
| 298 | + selector: CSS selector, input name, or button text |
| 299 | + value: text to fill (for 'fill' action) |
| 300 | + wait_for: CSS selector to wait for before acting |
| 301 | + timeout: max seconds for operation |
| 302 | +
|
| 303 | + Requires: pip install playwright |
| 304 | + playwright install chromium |
| 305 | + Falls back to urllib POST for simple forms if unavailable. |
| 306 | + """ |
| 307 | + # Gate by action type |
| 308 | + action_type = 'form_submit' if action == 'submit' \ |
| 309 | + else 'browser_interact' |
| 310 | + gate = self._gate_action(action_type, url) |
| 311 | + |
| 312 | + if gate['decision'] == 'block': |
| 313 | + self._log_action(action_type, url, 'blocked') |
| 314 | + return {'status': 'blocked', 'reason': gate['reason']} |
| 315 | + |
| 316 | + if gate['decision'] == 'confirm_required': |
| 317 | + if not self._confirm(action_type, f"{action} on {url}"): |
| 318 | + self._log_action(action_type, url, 'denied_by_user') |
| 319 | + return {'status': 'denied', |
| 320 | + 'reason': 'User denied confirmation'} |
| 321 | + |
| 322 | + # Build Playwright script |
| 323 | + wait_line = f"page.wait_for_selector('{wait_for}')" \ |
| 324 | + if wait_for else "page.wait_for_load_state('networkidle')" |
| 325 | + |
| 326 | + if action == 'navigate': |
| 327 | + action_line = "" |
| 328 | + elif action == 'click': |
| 329 | + action_line = f"page.click('{selector}')" |
| 330 | + elif action == 'fill': |
| 331 | + action_line = f"page.fill('{selector}', '{value}')" |
| 332 | + elif action == 'select': |
| 333 | + action_line = f"page.select_option('{selector}', '{value}')" |
| 334 | + elif action == 'submit': |
| 335 | + action_line = f"page.click('{selector or \"button[type=submit]\"}')" |
| 336 | + elif action == 'scrape': |
| 337 | + action_line = "result = page.inner_text('body')" |
| 338 | + elif action == 'screenshot': |
| 339 | + action_line = "page.screenshot(path='/tmp/caios_browser.png')" |
| 340 | + else: |
| 341 | + return {'status': 'error', |
| 342 | + 'error': f'Unknown action: {action}'} |
| 343 | + |
| 344 | + script = f""" |
| 345 | +import sys |
| 346 | +try: |
| 347 | + from playwright.sync_api import sync_playwright |
| 348 | + with sync_playwright() as p: |
| 349 | + browser = p.chromium.launch(headless=True) |
| 350 | + context = browser.new_context( |
| 351 | + user_agent='CAIOS-Agent/1.0' |
| 352 | + ) |
| 353 | + page = context.new_page() |
| 354 | + page.goto('{url}', timeout={timeout * 1000}) |
| 355 | + {wait_line} |
| 356 | + result = '' |
| 357 | + {action_line} |
| 358 | + if not result: |
| 359 | + result = page.url |
| 360 | + browser.close() |
| 361 | + print(result) |
| 362 | +except ImportError: |
| 363 | + print('PLAYWRIGHT_UNAVAILABLE') |
| 364 | +except Exception as e: |
| 365 | + print(f'ERROR:{{e}}') |
| 366 | +""" |
| 367 | + try: |
| 368 | + output = subprocess.run( |
| 369 | + ['python3', '-c', script], |
| 370 | + capture_output=True, |
| 371 | + text=True, |
| 372 | + timeout=timeout + 5 |
| 373 | + ) |
| 374 | + |
| 375 | + stdout = output.stdout.strip() |
| 376 | + |
| 377 | + # Playwright not installed — fallback to urllib |
| 378 | + if stdout == 'PLAYWRIGHT_UNAVAILABLE': |
| 379 | + print("[BROWSER] Playwright unavailable — " |
| 380 | + "falling back to urllib") |
| 381 | + return self.fetch_url(url, extract_mode='full') |
| 382 | + |
| 383 | + if stdout.startswith('ERROR:'): |
| 384 | + self._log_action(action_type, url, |
| 385 | + f'error: {stdout}') |
| 386 | + return {'status': 'error', |
| 387 | + 'error': stdout[6:]} |
| 388 | + |
| 389 | + self._log_action(action_type, url, |
| 390 | + f'executed: {action}') |
| 391 | + return { |
| 392 | + 'status': 'success', |
| 393 | + 'action': action, |
| 394 | + 'url': url, |
| 395 | + 'selector': selector, |
| 396 | + 'result': stdout[:5000] |
| 397 | + } |
| 398 | + |
| 399 | + except subprocess.TimeoutExpired: |
| 400 | + return {'status': 'timeout', |
| 401 | + 'error': f'Exceeded {timeout}s'} |
| 402 | + except Exception as e: |
| 403 | + return {'status': 'error', 'error': str(e)} |
283 | 404 |
|
284 | 405 | def _extract_semantic(self, html: str, |
285 | 406 | mode: str = 'content') -> Dict: |
@@ -398,7 +519,15 @@ def create_os_controller(shared_memory: Dict, |
398 | 519 | result = controller.delete_file("/tmp/caios_test.txt") |
399 | 520 | print(f"Status: {result['status']}") |
400 | 521 |
|
401 | | - # Test 5: High distress context (should increase density) |
| 522 | + # Test 5: Browser interact - navigate (requires Playwright) |
| 523 | + print("\n[TEST 5] Browser Navigate (Playwright or urllib fallback)") |
| 524 | + result = controller.browser_interact( |
| 525 | + url="https://cai-os.com", |
| 526 | + action="navigate" |
| 527 | + ) |
| 528 | + print(f"Status: {result['status']}") |
| 529 | + |
| 530 | + # Test 6: High distress context (should increase density) |
402 | 531 | print("\n[TEST 4] High Distress Context (elevated density)") |
403 | 532 | shared_mem['distress_density'] = 0.8 |
404 | 533 | controller2 = OSController(shared_mem, require_confirmation=False) |
|
0 commit comments