fleet-python 0.2.66b2__py3-none-any.whl → 0.2.105__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. examples/export_tasks.py +16 -5
  2. examples/export_tasks_filtered.py +245 -0
  3. examples/fetch_tasks.py +230 -0
  4. examples/import_tasks.py +140 -8
  5. examples/iterate_verifiers.py +725 -0
  6. fleet/__init__.py +128 -5
  7. fleet/_async/__init__.py +27 -3
  8. fleet/_async/base.py +24 -9
  9. fleet/_async/client.py +938 -41
  10. fleet/_async/env/client.py +60 -3
  11. fleet/_async/instance/client.py +52 -7
  12. fleet/_async/models.py +15 -0
  13. fleet/_async/resources/api.py +200 -0
  14. fleet/_async/resources/sqlite.py +1801 -46
  15. fleet/_async/tasks.py +122 -25
  16. fleet/_async/verifiers/bundler.py +22 -21
  17. fleet/_async/verifiers/verifier.py +25 -19
  18. fleet/agent/__init__.py +32 -0
  19. fleet/agent/gemini_cua/Dockerfile +45 -0
  20. fleet/agent/gemini_cua/__init__.py +10 -0
  21. fleet/agent/gemini_cua/agent.py +759 -0
  22. fleet/agent/gemini_cua/mcp/main.py +108 -0
  23. fleet/agent/gemini_cua/mcp_server/__init__.py +5 -0
  24. fleet/agent/gemini_cua/mcp_server/main.py +105 -0
  25. fleet/agent/gemini_cua/mcp_server/tools.py +178 -0
  26. fleet/agent/gemini_cua/requirements.txt +5 -0
  27. fleet/agent/gemini_cua/start.sh +30 -0
  28. fleet/agent/orchestrator.py +854 -0
  29. fleet/agent/types.py +49 -0
  30. fleet/agent/utils.py +34 -0
  31. fleet/base.py +34 -9
  32. fleet/cli.py +1061 -0
  33. fleet/client.py +1060 -48
  34. fleet/config.py +1 -1
  35. fleet/env/__init__.py +16 -0
  36. fleet/env/client.py +60 -3
  37. fleet/eval/__init__.py +15 -0
  38. fleet/eval/uploader.py +231 -0
  39. fleet/exceptions.py +8 -0
  40. fleet/instance/client.py +53 -8
  41. fleet/instance/models.py +1 -0
  42. fleet/models.py +303 -0
  43. fleet/proxy/__init__.py +25 -0
  44. fleet/proxy/proxy.py +453 -0
  45. fleet/proxy/whitelist.py +244 -0
  46. fleet/resources/api.py +200 -0
  47. fleet/resources/sqlite.py +1845 -46
  48. fleet/tasks.py +113 -20
  49. fleet/utils/__init__.py +7 -0
  50. fleet/utils/http_logging.py +178 -0
  51. fleet/utils/logging.py +13 -0
  52. fleet/utils/playwright.py +440 -0
  53. fleet/verifiers/bundler.py +22 -21
  54. fleet/verifiers/db.py +985 -1
  55. fleet/verifiers/decorator.py +1 -1
  56. fleet/verifiers/verifier.py +25 -19
  57. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/METADATA +28 -1
  58. fleet_python-0.2.105.dist-info/RECORD +115 -0
  59. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/WHEEL +1 -1
  60. fleet_python-0.2.105.dist-info/entry_points.txt +2 -0
  61. tests/test_app_method.py +85 -0
  62. tests/test_expect_exactly.py +4148 -0
  63. tests/test_expect_only.py +2593 -0
  64. tests/test_instance_dispatch.py +607 -0
  65. tests/test_sqlite_resource_dual_mode.py +263 -0
  66. tests/test_sqlite_shared_memory_behavior.py +117 -0
  67. fleet_python-0.2.66b2.dist-info/RECORD +0 -81
  68. tests/test_verifier_security.py +0 -427
  69. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/licenses/LICENSE +0 -0
  70. {fleet_python-0.2.66b2.dist-info → fleet_python-0.2.105.dist-info}/top_level.txt +0 -0
fleet/tasks.py CHANGED
@@ -36,9 +36,16 @@ class Task(BaseModel):
36
36
  )
37
37
  verifier_id: Optional[str] = Field(None, description="Verifier identifier")
38
38
  verifier_sha: Optional[str] = Field(None, description="Verifier SHA256 hash")
39
+ verifier_runtime_version: Optional[str] = Field(None, description="Verifier runtime version")
39
40
  metadata: Optional[Dict[str, Any]] = Field(
40
41
  default_factory=dict, description="Additional task metadata"
41
42
  )
43
+ writer_metadata: Optional[Dict[str, Any]] = Field(
44
+ None, description="Metadata filled by task writer"
45
+ )
46
+ qa_metadata: Optional[Dict[str, Any]] = Field(
47
+ None, description="Metadata filled by QA reviewer"
48
+ )
42
49
  output_json_schema: Optional[Dict[str, Any]] = Field(
43
50
  None, description="JSON schema for expected output format"
44
51
  )
@@ -199,26 +206,37 @@ class Task(BaseModel):
199
206
  verifier_id=verifier_id,
200
207
  verifier_key=self.key,
201
208
  sha256=self.verifier_sha or "",
209
+ verifier_runtime_version=self.verifier_runtime_version or "",
202
210
  )
203
211
  self.verifier = verifier
204
212
 
205
- def make_env(self, region: Optional[str] = None):
213
+ def make_env(
214
+ self,
215
+ region: Optional[str] = None,
216
+ image_type: Optional[str] = None,
217
+ ttl_seconds: Optional[int] = None,
218
+ run_id: Optional[str] = None,
219
+ heartbeat_interval: Optional[int] = None,
220
+ ):
206
221
  """Create an environment instance for this task's environment.
207
222
 
208
- Uses the task's env_id (and version if present) to create the env.
223
+ Alias for make() method. Uses the task's env_id (and version if present) to create the env.
209
224
  """
210
- if not self.env_id:
211
- raise ValueError("Task has no env_id defined")
212
- # Deferred import to avoid circular dependencies
213
- from .client import Fleet
214
-
215
- return Fleet().make(env_key=self.env_key, region=region)
225
+ return self.make(
226
+ region=region,
227
+ image_type=image_type,
228
+ ttl_seconds=ttl_seconds,
229
+ run_id=run_id,
230
+ heartbeat_interval=heartbeat_interval,
231
+ )
216
232
 
217
233
  def make(
218
234
  self,
219
235
  region: Optional[str] = None,
220
236
  image_type: Optional[str] = None,
221
237
  ttl_seconds: Optional[int] = None,
238
+ run_id: Optional[str] = None,
239
+ heartbeat_interval: Optional[int] = None,
222
240
  ):
223
241
  """Create an environment instance with task's configuration.
224
242
 
@@ -226,11 +244,15 @@ class Task(BaseModel):
226
244
  - env_key (env_id + version)
227
245
  - data_key (data_id + data_version, if present)
228
246
  - env_variables (if present)
247
+ - run_id (if present)
248
+ - heartbeat_interval (if present)
229
249
 
230
250
  Args:
231
251
  region: Optional AWS region for the environment
232
252
  image_type: Optional image type for the environment
233
253
  ttl_seconds: Optional TTL in seconds for the instance
254
+ run_id: Optional run ID to group instances
255
+ heartbeat_interval: Optional heartbeat interval in seconds (30-3600)
234
256
 
235
257
  Returns:
236
258
  Environment instance configured for this task
@@ -238,7 +260,7 @@ class Task(BaseModel):
238
260
  Example:
239
261
  task = fleet.Task(key="my-task", prompt="...", env_id="my-env",
240
262
  data_id="my-data", data_version="v1.0")
241
- env = task.make(region="us-west-2")
263
+ env = task.make(region="us-west-2", run_id="my-batch-123", heartbeat_interval=60)
242
264
  """
243
265
  if not self.env_id:
244
266
  raise ValueError("Task has no env_id defined")
@@ -253,11 +275,13 @@ class Task(BaseModel):
253
275
  env_variables=self.env_variables if self.env_variables else None,
254
276
  image_type=image_type,
255
277
  ttl_seconds=ttl_seconds,
278
+ run_id=run_id,
279
+ heartbeat_interval=heartbeat_interval,
256
280
  )
257
281
 
258
282
 
259
283
  def verifier_from_string(
260
- verifier_func: str, verifier_id: str, verifier_key: str, sha256: str = ""
284
+ verifier_func: str, verifier_id: str, verifier_key: str, sha256: str = "", verifier_runtime_version: str = ""
261
285
  ) -> "VerifierFunction":
262
286
  """Create a verifier function from string code.
263
287
 
@@ -266,20 +290,62 @@ def verifier_from_string(
266
290
  verifier_id: Unique identifier for the verifier
267
291
  verifier_key: Key/name for the verifier
268
292
  sha256: SHA256 hash of the verifier code
293
+ verifier_runtime_version: Verifier runtime version
269
294
 
270
295
  Returns:
271
296
  VerifierFunction instance that can be used to verify tasks
272
297
  """
273
298
  try:
274
299
  import inspect
300
+ import re
301
+ import json
302
+ import string
275
303
  from .verifiers import SyncVerifierFunction
276
304
  from .verifiers.code import TASK_SUCCESSFUL_SCORE, TASK_FAILED_SCORE
277
305
  from .verifiers.db import IgnoreConfig
278
- from .verifiers.parsing import parse_and_validate_verifier
279
306
 
280
- # Validate the code and extract function name
281
- # This ensures no arbitrary code execution during import
282
- func_name = parse_and_validate_verifier(verifier_func)
307
+ # Strip @verifier decorator if present to avoid double-wrapping
308
+ # Remove lines like: @verifier(key="...")
309
+ cleaned_code = re.sub(r"@verifier\([^)]*\)\s*\n", "", verifier_func)
310
+ # Also remove the verifier import if present
311
+ # Use MULTILINE flag to match beginning of lines with ^
312
+ cleaned_code = re.sub(r"^from fleet\.verifiers.*import.*verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
313
+ cleaned_code = re.sub(r"^from fleet import verifier.*$\n?", "", cleaned_code, flags=re.MULTILINE)
314
+ cleaned_code = re.sub(r"^import fleet\.verifiers.*$\n?", "", cleaned_code, flags=re.MULTILINE)
315
+ cleaned_code = re.sub(r"^import fleet$\n?", "", cleaned_code, flags=re.MULTILINE)
316
+
317
+ # Define helper functions for verifier execution
318
+ _TRANSLATOR = str.maketrans(string.punctuation, " " * len(string.punctuation))
319
+
320
+ def _normalize_text(value: str) -> str:
321
+ text = value.lower().translate(_TRANSLATOR)
322
+ return "".join(text.split())
323
+
324
+ def _stringify_content(content: Any) -> str:
325
+ if isinstance(content, (dict, list)):
326
+ return json.dumps(content, sort_keys=True)
327
+ return str(content)
328
+
329
+ def normalized_contains(target: str, blob: Any) -> bool:
330
+ normalized_target = _normalize_text(target)
331
+ normalized_blob = _normalize_text(_stringify_content(blob))
332
+ return normalized_target in normalized_blob
333
+
334
+ def extract_numbers(text: str) -> list:
335
+ cleaned_text = text.replace(',', '')
336
+ pattern = r'-?\d+\.?\d*'
337
+ matches = re.findall(pattern, cleaned_text)
338
+ return [float(num) for num in matches]
339
+
340
+ def contains_number(text: str, target_number) -> bool:
341
+ numbers = extract_numbers(text)
342
+ try:
343
+ if isinstance(target_number, str):
344
+ target_number = target_number.replace(',', '')
345
+ target = float(target_number)
346
+ except (ValueError, AttributeError):
347
+ return False
348
+ return target in numbers
283
349
 
284
350
  # Create a globals namespace with all required imports
285
351
  exec_globals = globals().copy()
@@ -289,15 +355,24 @@ def verifier_from_string(
289
355
  "TASK_FAILED_SCORE": TASK_FAILED_SCORE,
290
356
  "IgnoreConfig": IgnoreConfig,
291
357
  "Environment": object, # Add Environment type if needed
358
+ "normalized_contains": normalized_contains,
359
+ "extract_numbers": extract_numbers,
360
+ "contains_number": contains_number,
361
+ "json": json,
362
+ "re": re,
363
+ "string": string,
292
364
  }
293
365
  )
294
366
 
295
367
  # Create a local namespace for executing the code
296
368
  local_namespace = {}
297
369
 
298
- # Execute the verifier code in the namespace
299
- # This is now safe because we validated it contains only declarative code
300
- exec(verifier_func, exec_globals, local_namespace)
370
+ # Execute the cleaned verifier code in the namespace
371
+ exec(cleaned_code, exec_globals, local_namespace)
372
+
373
+ # Merge local_namespace into exec_globals so helper functions are accessible
374
+ # from the main verifier function when it's called
375
+ exec_globals.update(local_namespace)
301
376
 
302
377
  # Find the function that was defined (not imported)
303
378
  # Functions defined via exec have co_filename == '<string>'
@@ -318,6 +393,7 @@ def verifier_from_string(
318
393
  verifier_id=verifier_id,
319
394
  sha256=sha256,
320
395
  raw_code=verifier_func,
396
+ verifier_runtime_version=verifier_runtime_version if verifier_runtime_version else None,
321
397
  )
322
398
 
323
399
  # Store additional metadata
@@ -387,7 +463,12 @@ def load_tasks(
387
463
 
388
464
 
389
465
  def update_task(
390
- task_key: str, prompt: Optional[str] = None, verifier_code: Optional[str] = None
466
+ task_key: str,
467
+ prompt: Optional[str] = None,
468
+ verifier_code: Optional[str] = None,
469
+ metadata: Optional[Dict[str, Any]] = None,
470
+ writer_metadata: Optional[Dict[str, Any]] = None,
471
+ qa_metadata: Optional[Dict[str, Any]] = None,
391
472
  ):
392
473
  """Convenience function to update an existing task.
393
474
 
@@ -395,6 +476,9 @@ def update_task(
395
476
  task_key: The key of the task to update
396
477
  prompt: New prompt text for the task (optional)
397
478
  verifier_code: Python code for task verification (optional)
479
+ metadata: Additional metadata for the task (optional)
480
+ writer_metadata: Metadata filled by task writer (optional)
481
+ qa_metadata: Metadata filled by QA reviewer (optional)
398
482
 
399
483
  Returns:
400
484
  TaskResponse containing the updated task details
@@ -402,16 +486,25 @@ def update_task(
402
486
  Examples:
403
487
  response = fleet.update_task("my-task", prompt="New prompt text")
404
488
  response = fleet.update_task("my-task", verifier_code="def verify(env): return True")
489
+ response = fleet.update_task("my-task", metadata={"seed": 42, "story": "Updated story"})
490
+ response = fleet.update_task("my-task", writer_metadata={"author": "john"})
405
491
  """
406
492
  from .global_client import get_client
407
493
 
408
494
  client = get_client()
409
495
  return client.update_task(
410
- task_key=task_key, prompt=prompt, verifier_code=verifier_code
496
+ task_key=task_key,
497
+ prompt=prompt,
498
+ verifier_code=verifier_code,
499
+ metadata=metadata,
500
+ writer_metadata=writer_metadata,
501
+ qa_metadata=qa_metadata,
411
502
  )
412
503
 
413
504
 
414
- def get_task(task_key: str, version_id: Optional[str] = None, team_id: Optional[str] = None):
505
+ def get_task(
506
+ task_key: str, version_id: Optional[str] = None, team_id: Optional[str] = None
507
+ ):
415
508
  """Convenience function to get a task by key and optional version.
416
509
 
417
510
  Args:
@@ -0,0 +1,7 @@
1
+ """Fleet utilities - shared helpers and browser control."""
2
+
3
+ from .playwright import PlaywrightComputer, map_key, is_modifier
4
+ from .logging import log_verbose, VERBOSE
5
+
6
+ __all__ = ["PlaywrightComputer", "map_key", "is_modifier", "log_verbose", "VERBOSE"]
7
+
@@ -0,0 +1,178 @@
1
+ """HTTP traffic logging via httpx event hooks.
2
+
3
+ Captures request/response pairs and writes to JSONL file.
4
+ Works with any httpx-based client (including google-genai).
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import time
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Optional
13
+ import threading
14
+
15
+ # Sensitive headers to redact
16
+ SENSITIVE_HEADERS = {
17
+ "authorization", "x-api-key", "api-key", "x-goog-api-key",
18
+ "x-gemini-api-key", "x-openai-api-key", "x-anthropic-api-key",
19
+ "cookie", "set-cookie", "x-auth-token", "x-access-token",
20
+ }
21
+
22
+
23
+ def _redact_headers(headers: dict) -> dict:
24
+ """Redact sensitive headers."""
25
+ redacted = {}
26
+ for k, v in headers.items():
27
+ if k.lower() in SENSITIVE_HEADERS:
28
+ if len(str(v)) > 12:
29
+ redacted[k] = str(v)[:8] + "...[REDACTED]"
30
+ else:
31
+ redacted[k] = "[REDACTED]"
32
+ else:
33
+ redacted[k] = str(v)
34
+ return redacted
35
+
36
+
37
+ class HttpTrafficLogger:
38
+ """Logs HTTP traffic to JSONL file."""
39
+
40
+ _instance: Optional["HttpTrafficLogger"] = None
41
+ _lock = threading.Lock()
42
+
43
+ def __init__(self, log_file: Optional[Path] = None):
44
+ if log_file is None:
45
+ log_dir = Path.home() / ".fleet" / "proxy_logs"
46
+ log_dir.mkdir(parents=True, exist_ok=True)
47
+ log_file = log_dir / f"traffic_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jsonl"
48
+
49
+ self.log_file = log_file
50
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
51
+ self._file = open(log_file, "a")
52
+ self._write_lock = threading.Lock()
53
+ self._request_times: dict = {} # Track request start times
54
+
55
+ @classmethod
56
+ def get(cls, log_file: Optional[Path] = None) -> "HttpTrafficLogger":
57
+ """Get singleton instance."""
58
+ with cls._lock:
59
+ if cls._instance is None:
60
+ cls._instance = cls(log_file)
61
+ return cls._instance
62
+
63
+ @classmethod
64
+ def set_log_file(cls, log_file: Path):
65
+ """Set log file for singleton (creates new instance if needed)."""
66
+ with cls._lock:
67
+ if cls._instance is not None:
68
+ cls._instance.close()
69
+ cls._instance = cls(log_file)
70
+ return cls._instance
71
+
72
+ def log_request(self, request) -> str:
73
+ """Log request, return request ID for matching response."""
74
+ request_id = f"{id(request)}_{time.time()}"
75
+ self._request_times[request_id] = time.time()
76
+ return request_id
77
+
78
+ def log_response(self, request, response, request_id: Optional[str] = None):
79
+ """Log complete request/response pair."""
80
+ start_time = self._request_times.pop(request_id, None) if request_id else None
81
+ duration_ms = int((time.time() - start_time) * 1000) if start_time else None
82
+
83
+ # Extract host
84
+ host = str(request.url.host) if hasattr(request.url, 'host') else str(request.url).split('/')[2]
85
+
86
+ # Build request entry
87
+ request_headers = dict(request.headers) if hasattr(request, 'headers') else {}
88
+ request_body = None
89
+ if hasattr(request, 'content') and request.content:
90
+ try:
91
+ body_bytes = request.content if isinstance(request.content, bytes) else bytes(request.content)
92
+ if len(body_bytes) < 50000:
93
+ request_body = body_bytes.decode('utf-8', errors='replace')
94
+ except:
95
+ pass
96
+
97
+ # Build response entry
98
+ response_headers = dict(response.headers) if hasattr(response, 'headers') else {}
99
+ response_body = None
100
+ if hasattr(response, 'content') and response.content:
101
+ try:
102
+ if len(response.content) < 50000:
103
+ response_body = response.content.decode('utf-8', errors='replace')
104
+ except:
105
+ pass
106
+
107
+ entry = {
108
+ "type": "http",
109
+ "timestamp": datetime.now().isoformat(),
110
+ "host": host,
111
+ "duration_ms": duration_ms,
112
+ "logged_at": datetime.now().isoformat(),
113
+ "request": {
114
+ "method": str(request.method),
115
+ "url": str(request.url),
116
+ "headers": _redact_headers(request_headers),
117
+ "body": request_body,
118
+ "body_length": len(request.content) if hasattr(request, 'content') and request.content else 0,
119
+ },
120
+ "response": {
121
+ "status_code": response.status_code,
122
+ "headers": _redact_headers(response_headers),
123
+ "body": response_body,
124
+ "body_length": len(response.content) if hasattr(response, 'content') and response.content else 0,
125
+ },
126
+ }
127
+
128
+ with self._write_lock:
129
+ self._file.write(json.dumps(entry) + "\n")
130
+ self._file.flush()
131
+
132
+ def close(self):
133
+ """Close the log file."""
134
+ with self._write_lock:
135
+ if self._file:
136
+ self._file.close()
137
+ self._file = None
138
+
139
+
140
+ def install_httpx_hooks():
141
+ """Install global httpx event hooks for traffic logging.
142
+
143
+ This patches httpx to log all HTTP traffic automatically.
144
+ """
145
+ try:
146
+ import httpx
147
+ except ImportError:
148
+ return
149
+
150
+ logger = HttpTrafficLogger.get()
151
+ original_send = httpx.Client.send
152
+ original_async_send = httpx.AsyncClient.send
153
+
154
+ def patched_send(self, request, **kwargs):
155
+ request_id = logger.log_request(request)
156
+ response = original_send(self, request, **kwargs)
157
+ logger.log_response(request, response, request_id)
158
+ return response
159
+
160
+ async def patched_async_send(self, request, **kwargs):
161
+ request_id = logger.log_request(request)
162
+ response = await original_async_send(self, request, **kwargs)
163
+ logger.log_response(request, response, request_id)
164
+ return response
165
+
166
+ httpx.Client.send = patched_send
167
+ httpx.AsyncClient.send = patched_async_send
168
+
169
+
170
+ def setup_logging(log_file: Optional[Path] = None):
171
+ """Setup HTTP traffic logging.
172
+
173
+ Args:
174
+ log_file: Path to JSONL log file. If None, creates timestamped file in ~/.fleet/proxy_logs/
175
+ """
176
+ if log_file:
177
+ HttpTrafficLogger.set_log_file(log_file)
178
+ install_httpx_hooks()
fleet/utils/logging.py ADDED
@@ -0,0 +1,13 @@
1
+ """Logging utilities for Fleet SDK."""
2
+
3
+ import os
4
+
5
+ # Verbose logging flag - check once at import time
6
+ VERBOSE = os.environ.get("FLEET_VERBOSE", "false").lower() in ("true", "1", "yes")
7
+
8
+
9
+ def log_verbose(*args, **kwargs):
10
+ """Print only if FLEET_VERBOSE is enabled."""
11
+ if VERBOSE:
12
+ print(*args, **kwargs)
13
+