|
1 | | -#V03172026 |
| 1 | +#V03232026 |
2 | 2 | # ============================================================================= |
3 | 3 | # Chaos AI-OS — OS Control Layer |
4 | 4 | # CPOL-gated system operations with Asimov compliance |
|
21 | 21 | 'network_request': 0.6, # Medium - depends on destination |
22 | 22 | 'file_write': 0.4, # Low-medium - new file creation |
23 | 23 | 'file_read': 0.1, # Low - read only, no state change |
| 24 | + 'semantic_fetch': 0.2, # Read-only web, low risk |
24 | 25 | } |
25 | 26 |
|
26 | 27 | class OSController: |
@@ -140,6 +141,74 @@ def read_file(self, path: str) -> Dict[str, Any]: |
140 | 141 | except Exception as e: |
141 | 142 | return {'status': 'error', 'error': str(e)} |
142 | 143 |
|
| 144 | + def fetch_url(self, url: str, |
| 145 | + extract_mode: str = 'content') -> Dict[str, Any]: |
| 146 | + """ |
| 147 | + Agent-optimized semantic web fetcher. |
| 148 | + Strips visual noise, tooltips, scripts, ads. |
| 149 | + Returns structured content safe for agent consumption. |
| 150 | + No screenshots. No coordinates. No focus hijacking. |
| 151 | +
|
| 152 | + extract_mode: 'content' | 'links' | 'forms' | 'full' |
| 153 | + """ |
| 154 | + gate = self._gate_action('semantic_fetch', url) |
| 155 | + |
| 156 | + if gate['decision'] == 'block': |
| 157 | + self._log_action('semantic_fetch', url, 'blocked') |
| 158 | + return {'status': 'blocked', 'reason': gate['reason']} |
| 159 | + |
| 160 | + try: |
| 161 | + import urllib.request |
| 162 | + import html.parser |
| 163 | + |
| 164 | + # Security: validate URL before fetch |
| 165 | + if not url.startswith(('http://', 'https://')): |
| 166 | + return {'status': 'blocked', |
| 167 | + 'reason': 'Invalid URL scheme'} |
| 168 | + |
| 169 | + # Simple embedded code attack mitigation |
| 170 | + BLOCKED_PATTERNS = [ |
| 171 | + 'javascript:', 'data:', 'vbscript:', |
| 172 | + 'onload=', 'onerror=', 'onclick=' |
| 173 | + ] |
| 174 | + |
| 175 | + headers = { |
| 176 | + 'User-Agent': 'CAIOS-Agent/1.0 (semantic fetch)', |
| 177 | + 'Accept': 'text/html,application/xhtml+xml', |
| 178 | + } |
| 179 | + |
| 180 | + req = urllib.request.Request(url, headers=headers) |
| 181 | + with urllib.request.urlopen(req, timeout=10) as response: |
| 182 | + raw_html = response.read().decode('utf-8', errors='ignore') |
| 183 | + |
| 184 | + # Check for embedded attack patterns |
| 185 | + raw_lower = raw_html.lower() |
| 186 | + detected_threats = [p for p in BLOCKED_PATTERNS |
| 187 | + if p in raw_lower] |
| 188 | + if len(detected_threats) > 2: |
| 189 | + self._log_action('semantic_fetch', url, |
| 190 | + 'blocked_malicious_content') |
| 191 | + return { |
| 192 | + 'status': 'blocked', |
| 193 | + 'reason': f'Potential malicious content: ' |
| 194 | + f'{detected_threats}' |
| 195 | + } |
| 196 | + |
| 197 | + # Strip to semantic content |
| 198 | + result = self._extract_semantic(raw_html, extract_mode) |
| 199 | + self._log_action('semantic_fetch', url, 'allowed') |
| 200 | + |
| 201 | + return { |
| 202 | + 'status': 'success', |
| 203 | + 'url': url, |
| 204 | + 'extract_mode': extract_mode, |
| 205 | + 'content': result |
| 206 | + } |
| 207 | + |
| 208 | + except Exception as e: |
| 209 | + self._log_action('semantic_fetch', url, f'error: {e}') |
| 210 | + return {'status': 'error', 'error': str(e)} |
| 211 | + |
143 | 212 | def write_file(self, path: str, content: str, |
144 | 213 | overwrite: bool = False) -> Dict[str, Any]: |
145 | 214 | """Write file - medium risk if overwrite.""" |
@@ -212,6 +281,71 @@ def execute_script(self, script: str, |
212 | 281 | return {'status': 'error', 'error': str(e)} |
213 | 282 |
|
214 | 283 |
|
| 284 | + def _extract_semantic(self, html: str, |
| 285 | + mode: str = 'content') -> Dict: |
| 286 | + """ |
| 287 | + Strip visual noise. Return semantic structure only. |
| 288 | + No JS. No CSS. No tooltips. No ads. No tracking. |
| 289 | + Pure content for agent consumption. |
| 290 | + """ |
| 291 | + import re |
| 292 | + |
| 293 | + # Remove script tags and contents |
| 294 | + html = re.sub(r'<script[^>]*>.*?</script>', |
| 295 | + '', html, flags=re.DOTALL | re.IGNORECASE) |
| 296 | + # Remove style tags |
| 297 | + html = re.sub(r'<style[^>]*>.*?</style>', |
| 298 | + '', html, flags=re.DOTALL | re.IGNORECASE) |
| 299 | + # Remove comments |
| 300 | + html = re.sub(r'<!--.*?-->', '', html, flags=re.DOTALL) |
| 301 | + # Remove event handlers |
| 302 | + html = re.sub(r'\s+on\w+="[^"]*"', '', html) |
| 303 | + html = re.sub(r"\s+on\w+='[^']*'", '', html) |
| 304 | + |
| 305 | + # Extract by mode |
| 306 | + if mode == 'content': |
| 307 | + # Get text content only |
| 308 | + text = re.sub(r'<[^>]+>', ' ', html) |
| 309 | + text = re.sub(r'\s+', ' ', text).strip() |
| 310 | + return {'text': text[:10000]} # Cap at 10k chars |
| 311 | + |
| 312 | + elif mode == 'links': |
| 313 | + links = re.findall( |
| 314 | + r'<a[^>]+href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', |
| 315 | + html, re.IGNORECASE | re.DOTALL |
| 316 | + ) |
| 317 | + return {'links': [ |
| 318 | + {'url': l[0], |
| 319 | + 'text': re.sub(r'<[^>]+>', '', l[1]).strip()} |
| 320 | + for l in links if l[0].startswith('http') |
| 321 | + ][:50]} # Cap at 50 links |
| 322 | + |
| 323 | + elif mode == 'forms': |
| 324 | + forms = re.findall( |
| 325 | + r'<form[^>]*>(.*?)</form>', |
| 326 | + html, re.IGNORECASE | re.DOTALL |
| 327 | + ) |
| 328 | + inputs = re.findall( |
| 329 | + r'<input[^>]+name=["\']([^"\']+)["\'][^>]*>', |
| 330 | + html, re.IGNORECASE |
| 331 | + ) |
| 332 | + return {'forms': len(forms), 'inputs': inputs[:20]} |
| 333 | + |
| 334 | + elif mode == 'full': |
| 335 | + text = re.sub(r'<[^>]+>', ' ', html) |
| 336 | + text = re.sub(r'\s+', ' ', text).strip() |
| 337 | + links = re.findall( |
| 338 | + r'href=["\']([^"\']+)["\']', |
| 339 | + html, re.IGNORECASE |
| 340 | + ) |
| 341 | + return { |
| 342 | + 'text': text[:10000], |
| 343 | + 'links': [l for l in links |
| 344 | + if l.startswith('http')][:50] |
| 345 | + } |
| 346 | + |
| 347 | + return {'raw': html[:5000]} |
| 348 | + |
215 | 349 | # ============================================================================= |
216 | 350 | # Factory function for orchestrator import |
217 | 351 | # ============================================================================= |
@@ -251,12 +385,20 @@ def create_os_controller(shared_memory: Dict, |
251 | 385 | ) |
252 | 386 | print(f"Status: {result['status']}") |
253 | 387 |
|
254 | | - # Test 3: Delete file (should require confirmation) |
| 388 | + # Test 3: Semantic fetch (should allow) |
| 389 | + print("\n[TEST 3] Semantic Fetch (should allow)") |
| 390 | + result = controller.fetch_url( |
| 391 | + "https://cai-os.com", |
| 392 | + extract_mode='content' |
| 393 | + ) |
| 394 | + print(f"Status: {result['status']}") |
| 395 | + |
| 396 | + # Test 4: Delete file (should require confirmation) |
255 | 397 | print("\n[TEST 3] Delete File (confirmation disabled in test)") |
256 | 398 | result = controller.delete_file("/tmp/caios_test.txt") |
257 | 399 | print(f"Status: {result['status']}") |
258 | 400 |
|
259 | | - # Test 4: High distress context (should increase density) |
| 401 | + # Test 5: High distress context (should increase density) |
260 | 402 | print("\n[TEST 4] High Distress Context (elevated density)") |
261 | 403 | shared_mem['distress_density'] = 0.8 |
262 | 404 | controller2 = OSController(shared_mem, require_confirmation=False) |
|
0 commit comments