fleet-python 0.2.106__tar.gz → 0.2.108__tar.gz

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 (122) hide show
  1. {fleet_python-0.2.106/fleet_python.egg-info → fleet_python-0.2.108}/PKG-INFO +1 -1
  2. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/__init__.py +1 -1
  3. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/__init__.py +1 -1
  4. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/base.py +1 -1
  5. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/client.py +6 -11
  6. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/instance/client.py +14 -0
  7. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/models.py +0 -9
  8. fleet_python-0.2.108/fleet/_async/resources/filesystem.py +397 -0
  9. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/tasks.py +6 -27
  10. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/agent.py +159 -377
  11. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/base.py +1 -1
  12. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/client.py +6 -11
  13. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/client.py +14 -0
  14. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/models.py +74 -0
  15. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/models.py +0 -9
  16. fleet_python-0.2.108/fleet/resources/filesystem.py +397 -0
  17. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/tasks.py +2 -19
  18. {fleet_python-0.2.106 → fleet_python-0.2.108/fleet_python.egg-info}/PKG-INFO +1 -1
  19. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/SOURCES.txt +2 -0
  20. {fleet_python-0.2.106 → fleet_python-0.2.108}/pyproject.toml +1 -1
  21. {fleet_python-0.2.106 → fleet_python-0.2.108}/LICENSE +0 -0
  22. {fleet_python-0.2.106 → fleet_python-0.2.108}/README.md +0 -0
  23. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/diff_example.py +0 -0
  24. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/dsl_example.py +0 -0
  25. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example.py +0 -0
  26. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/exampleResume.py +0 -0
  27. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_account.py +0 -0
  28. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_action_log.py +0 -0
  29. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_client.py +0 -0
  30. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_mcp_anthropic.py +0 -0
  31. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_mcp_openai.py +0 -0
  32. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_sync.py +0 -0
  33. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_task.py +0 -0
  34. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_tasks.py +0 -0
  35. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/example_verifier.py +0 -0
  36. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/export_tasks.py +0 -0
  37. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/export_tasks_filtered.py +0 -0
  38. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/fetch_tasks.py +0 -0
  39. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/gemini_example.py +0 -0
  40. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/import_tasks.py +0 -0
  41. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/iterate_verifiers.py +0 -0
  42. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/json_tasks_example.py +0 -0
  43. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/nova_act_example.py +0 -0
  44. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/openai_example.py +0 -0
  45. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/openai_simple_example.py +0 -0
  46. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/query_builder_example.py +0 -0
  47. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/quickstart.py +0 -0
  48. {fleet_python-0.2.106 → fleet_python-0.2.108}/examples/test_cdp_logging.py +0 -0
  49. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/env/__init__.py +0 -0
  50. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/env/client.py +0 -0
  51. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/exceptions.py +0 -0
  52. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/global_client.py +0 -0
  53. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/instance/__init__.py +0 -0
  54. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/instance/base.py +0 -0
  55. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/__init__.py +0 -0
  56. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/api.py +0 -0
  57. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/base.py +0 -0
  58. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/browser.py +0 -0
  59. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/mcp.py +0 -0
  60. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/resources/sqlite.py +0 -0
  61. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/verifiers/__init__.py +0 -0
  62. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/verifiers/bundler.py +0 -0
  63. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/_async/verifiers/verifier.py +0 -0
  64. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/__init__.py +0 -0
  65. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/Dockerfile +0 -0
  66. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/__init__.py +0 -0
  67. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp/main.py +0 -0
  68. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp_server/__init__.py +0 -0
  69. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp_server/main.py +0 -0
  70. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/mcp_server/tools.py +0 -0
  71. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/requirements.txt +0 -0
  72. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/gemini_cua/start.sh +0 -0
  73. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/orchestrator.py +0 -0
  74. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/types.py +0 -0
  75. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/agent/utils.py +0 -0
  76. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/cli.py +0 -0
  77. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/config.py +0 -0
  78. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/env/__init__.py +0 -0
  79. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/env/client.py +0 -0
  80. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/eval/__init__.py +0 -0
  81. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/eval/uploader.py +0 -0
  82. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/exceptions.py +0 -0
  83. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/global_client.py +0 -0
  84. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/__init__.py +0 -0
  85. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/instance/base.py +0 -0
  86. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/proxy/__init__.py +0 -0
  87. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/proxy/proxy.py +0 -0
  88. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/proxy/whitelist.py +0 -0
  89. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/__init__.py +0 -0
  90. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/api.py +0 -0
  91. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/base.py +0 -0
  92. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/browser.py +0 -0
  93. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/mcp.py +0 -0
  94. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/resources/sqlite.py +0 -0
  95. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/types.py +0 -0
  96. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/__init__.py +0 -0
  97. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/http_logging.py +0 -0
  98. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/logging.py +0 -0
  99. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/utils/playwright.py +0 -0
  100. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/__init__.py +0 -0
  101. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/bundler.py +0 -0
  102. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/code.py +0 -0
  103. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/db.py +0 -0
  104. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/decorator.py +0 -0
  105. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/parse.py +0 -0
  106. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/sql_differ.py +0 -0
  107. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet/verifiers/verifier.py +0 -0
  108. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/dependency_links.txt +0 -0
  109. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/entry_points.txt +0 -0
  110. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/requires.txt +0 -0
  111. {fleet_python-0.2.106 → fleet_python-0.2.108}/fleet_python.egg-info/top_level.txt +0 -0
  112. {fleet_python-0.2.106 → fleet_python-0.2.108}/scripts/fix_sync_imports.py +0 -0
  113. {fleet_python-0.2.106 → fleet_python-0.2.108}/scripts/unasync.py +0 -0
  114. {fleet_python-0.2.106 → fleet_python-0.2.108}/setup.cfg +0 -0
  115. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/__init__.py +0 -0
  116. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_app_method.py +0 -0
  117. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_expect_exactly.py +0 -0
  118. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_expect_only.py +0 -0
  119. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_instance_dispatch.py +0 -0
  120. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_sqlite_resource_dual_mode.py +0 -0
  121. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_sqlite_shared_memory_behavior.py +0 -0
  122. {fleet_python-0.2.106 → fleet_python-0.2.108}/tests/test_verifier_from_string.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fleet-python
3
- Version: 0.2.106
3
+ Version: 0.2.108
4
4
  Summary: Python SDK for Fleet environments
5
5
  Author-email: Fleet AI <nic@fleet.so>
6
6
  License: Apache-2.0
@@ -73,7 +73,7 @@ from . import env
73
73
  from . import global_client as _global_client
74
74
  from ._async import global_client as _async_global_client
75
75
 
76
- __version__ = "0.2.103"
76
+ __version__ = "0.2.108"
77
77
 
78
78
  __all__ = [
79
79
  # Core classes
@@ -44,7 +44,7 @@ from ..types import VerifierFunction
44
44
  from .. import env
45
45
  from . import global_client as _async_global_client
46
46
 
47
- __version__ = "0.2.103"
47
+ __version__ = "0.2.108"
48
48
 
49
49
  __all__ = [
50
50
  # Core classes
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  from .. import __version__
28
28
  except ImportError:
29
- __version__ = "0.2.103"
29
+ __version__ = "0.2.108"
30
30
 
31
31
  logger = logging.getLogger(__name__)
32
32
 
@@ -171,6 +171,7 @@ from .instance.client import ValidatorType
171
171
  from .resources.base import Resource
172
172
  from .resources.sqlite import AsyncSQLiteResource
173
173
  from .resources.browser import AsyncBrowserResource
174
+ from .resources.filesystem import AsyncFilesystemResource
174
175
  from .resources.mcp import AsyncMCPResource
175
176
  from .resources.api import AsyncAPIResource
176
177
 
@@ -386,6 +387,10 @@ class AsyncEnv(EnvironmentBase):
386
387
  def browser(self, name: str = "cdp") -> AsyncBrowserResource:
387
388
  return self.instance.browser(name)
388
389
 
390
+ def fs(self) -> AsyncFilesystemResource:
391
+ """Get a filesystem diff resource for inspecting file changes."""
392
+ return self.instance.fs()
393
+
389
394
  def api(self, name: str = "api") -> AsyncAPIResource:
390
395
  """Get an API resource for making HTTP requests to the app's API.
391
396
 
@@ -1270,8 +1275,6 @@ class AsyncFleet:
1270
1275
  prompt: Optional[str] = None,
1271
1276
  verifier_code: Optional[str] = None,
1272
1277
  metadata: Optional[Dict[str, Any]] = None,
1273
- writer_metadata: Optional[Dict[str, Any]] = None,
1274
- qa_metadata: Optional[Dict[str, Any]] = None,
1275
1278
  ) -> TaskResponse:
1276
1279
  """Update an existing task.
1277
1280
 
@@ -1280,19 +1283,11 @@ class AsyncFleet:
1280
1283
  prompt: New prompt text for the task (optional)
1281
1284
  verifier_code: Python code for task verification (optional)
1282
1285
  metadata: Additional metadata for the task (optional)
1283
- writer_metadata: Metadata filled by task writer (optional)
1284
- qa_metadata: Metadata filled by QA reviewer (optional)
1285
1286
 
1286
1287
  Returns:
1287
1288
  TaskResponse containing the updated task details
1288
1289
  """
1289
- payload = TaskUpdateRequest(
1290
- prompt=prompt,
1291
- verifier_code=verifier_code,
1292
- metadata=metadata,
1293
- writer_metadata=writer_metadata,
1294
- qa_metadata=qa_metadata,
1295
- )
1290
+ payload = TaskUpdateRequest(prompt=prompt, verifier_code=verifier_code, metadata=metadata)
1296
1291
  response = await self.client.request(
1297
1292
  "PUT", f"/v1/tasks/{task_key}", json=payload.model_dump(exclude_none=True)
1298
1293
  )
@@ -10,6 +10,7 @@ from urllib.parse import urlparse
10
10
  from ..resources.sqlite import AsyncSQLiteResource
11
11
  from ..resources.browser import AsyncBrowserResource
12
12
  from ..resources.api import AsyncAPIResource
13
+ from ..resources.filesystem import AsyncFilesystemResource
13
14
  from ..resources.base import Resource
14
15
 
15
16
  from fleet.verifiers import DatabaseSnapshot
@@ -105,6 +106,19 @@ class AsyncInstanceClient:
105
106
  self._resources_state[ResourceType.cdp.value][name], self.client
106
107
  )
107
108
 
109
+ def fs(self) -> AsyncFilesystemResource:
110
+ """Returns a filesystem diff resource for inspecting file changes.
111
+
112
+ Returns:
113
+ An AsyncFilesystemResource for querying filesystem diffs
114
+ """
115
+ resource_model = ResourceModel(
116
+ name="fs",
117
+ type=ResourceType.db, # Reuse existing type; fs is not a registered resource type
118
+ mode=ResourceMode.ro,
119
+ )
120
+ return AsyncFilesystemResource(resource_model, self.client)
121
+
108
122
  def api(self, name: str, base_url: str) -> AsyncAPIResource:
109
123
  """
110
124
  Returns an API resource for making HTTP requests.
@@ -157,9 +157,6 @@ class TaskRequest(BaseModel):
157
157
  version: Optional[str] = Field(None, title="Version")
158
158
  env_variables: Optional[Dict[str, Any]] = Field(None, title="Env Variables")
159
159
  metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
160
- writer_metadata: Optional[Dict[str, Any]] = Field(
161
- None, title="Writer Metadata", description="Metadata filled by task writer"
162
- )
163
160
  output_json_schema: Optional[Dict[str, Any]] = Field(None, title="Output Json Schema")
164
161
 
165
162
 
@@ -167,12 +164,6 @@ class TaskUpdateRequest(BaseModel):
167
164
  prompt: Optional[str] = Field(None, title="Prompt")
168
165
  verifier_code: Optional[str] = Field(None, title="Verifier Code")
169
166
  metadata: Optional[Dict[str, Any]] = Field(None, title="Metadata")
170
- writer_metadata: Optional[Dict[str, Any]] = Field(
171
- None, title="Writer Metadata", description="Metadata filled by task writer"
172
- )
173
- qa_metadata: Optional[Dict[str, Any]] = Field(
174
- None, title="QA Metadata", description="Metadata filled by QA reviewer"
175
- )
176
167
 
177
168
 
178
169
  class VerifierData(BaseModel):
@@ -0,0 +1,397 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from ...instance.models import (
3
+ Resource as ResourceModel,
4
+ FsDiffRequest,
5
+ FsDiffResponse,
6
+ FsFileDiffEntry,
7
+ FileStateRequest,
8
+ FileStateResponse,
9
+ FileStateTextRequest,
10
+ DocTextRequest,
11
+ DocMetadataRequest,
12
+ DocMetadataResponse,
13
+ DocStructuredRequest,
14
+ DocStructuredResponse,
15
+ )
16
+ from .base import Resource
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from ..instance.base import AsyncWrapper
22
+
23
+
24
+ # Document extensions that need /fs/doc/text for readable content extraction
25
+ _DOC_EXTENSIONS = {
26
+ ".docx", ".doc", ".pptx", ".ppt", ".xlsx", ".xls",
27
+ ".odt", ".ods", ".odp", ".rtf", ".pdf", ".epub",
28
+ }
29
+
30
+
31
+ def _is_doc_file(path: str) -> bool:
32
+ lower = path.lower()
33
+ return any(lower.endswith(ext) for ext in _DOC_EXTENSIONS)
34
+
35
+
36
+ class AsyncFilesystemDiff:
37
+ """Wraps a filesystem diff response with assertion helpers."""
38
+
39
+ def __init__(self, response: FsDiffResponse, resource: "AsyncFilesystemResource"):
40
+ self.response = response
41
+ self.files = response.files
42
+ self.total_files = response.total_files
43
+ self.total_size = response.total_size
44
+ self._resource = resource
45
+
46
+ def _files_by_path(self) -> Dict[str, FsFileDiffEntry]:
47
+ return {f.path: f for f in self.files}
48
+
49
+ def expect_no_changes(self) -> "AsyncFilesystemDiff":
50
+ """Assert that no filesystem changes occurred."""
51
+ if self.files:
52
+ paths = [f.path for f in self.files]
53
+ raise AssertionError(
54
+ f"Expected no filesystem changes, but found {len(self.files)} changed file(s):\n"
55
+ + "\n".join(f" - {p}" for p in paths)
56
+ )
57
+ return self
58
+
59
+ async def expect_only(self, allowed_changes: List[Dict[str, Any]]) -> "AsyncFilesystemDiff":
60
+ """Assert that only the specified filesystem changes occurred.
61
+
62
+ Each spec in allowed_changes is a dict with:
63
+ - "path" (required): the file path to expect
64
+ - "content" (optional): expected file content (exact match)
65
+ - "content_contains" (optional): substring that must appear in content
66
+ - "doc_text" (optional): expected extracted text from doc files (exact match)
67
+ - "doc_text_contains" (optional): substring that must appear in doc text
68
+ - "file_type" (optional): expected file_type value
69
+ - "size" (optional): expected file size
70
+
71
+ Use ... (Ellipsis) as a value to accept any value for that field.
72
+
73
+ For document files (docx, pptx, xlsx, etc.), use doc_text / doc_text_contains
74
+ instead of content / content_contains. These call /fs/doc/text to extract
75
+ readable text from binary document formats.
76
+
77
+ Raises:
78
+ AssertionError: if unexpected files changed or specs don't match
79
+ """
80
+ if not allowed_changes:
81
+ return self.expect_no_changes()
82
+
83
+ files_by_path = self._files_by_path()
84
+ allowed_paths = set()
85
+ errors: List[str] = []
86
+
87
+ for spec in allowed_changes:
88
+ path = spec.get("path")
89
+ if path is None:
90
+ raise ValueError("Each allowed change spec must include a 'path' key")
91
+ allowed_paths.add(path)
92
+
93
+ if path not in files_by_path:
94
+ errors.append(f"Expected change at '{path}' but file was not in diff")
95
+ continue
96
+
97
+ entry = files_by_path[path]
98
+ await self._validate_entry(entry, spec, errors)
99
+
100
+ # Check for unexpected changes
101
+ unexpected = set(files_by_path.keys()) - allowed_paths
102
+ if unexpected:
103
+ errors.append(
104
+ f"Unexpected filesystem changes ({len(unexpected)} file(s)):\n"
105
+ + "\n".join(f" - {p}" for p in sorted(unexpected))
106
+ )
107
+
108
+ if errors:
109
+ raise AssertionError(
110
+ f"Filesystem expect_only failed with {len(errors)} error(s):\n"
111
+ + "\n".join(f" {i+1}. {e}" for i, e in enumerate(errors))
112
+ )
113
+
114
+ return self
115
+
116
+ async def expect_exactly(self, expected_changes: List[Dict[str, Any]]) -> "AsyncFilesystemDiff":
117
+ """Assert that EXACTLY the specified filesystem changes occurred.
118
+
119
+ Like expect_only, but also fails if an expected path is missing from the diff.
120
+ See expect_only for the full spec format including doc_text / doc_text_contains.
121
+
122
+ Raises:
123
+ AssertionError: if changes don't match exactly
124
+ """
125
+ if not expected_changes and self.files:
126
+ paths = [f.path for f in self.files]
127
+ raise AssertionError(
128
+ f"Expected no filesystem changes, but found {len(self.files)} changed file(s):\n"
129
+ + "\n".join(f" - {p}" for p in paths)
130
+ )
131
+
132
+ files_by_path = self._files_by_path()
133
+ expected_paths = set()
134
+ errors: List[str] = []
135
+
136
+ for spec in expected_changes:
137
+ path = spec.get("path")
138
+ if path is None:
139
+ raise ValueError("Each expected change spec must include a 'path' key")
140
+ expected_paths.add(path)
141
+
142
+ if path not in files_by_path:
143
+ errors.append(f"Expected change at '{path}' but file was not in diff")
144
+ continue
145
+
146
+ entry = files_by_path[path]
147
+ await self._validate_entry(entry, spec, errors)
148
+
149
+ # Check for unexpected changes
150
+ unexpected = set(files_by_path.keys()) - expected_paths
151
+ if unexpected:
152
+ errors.append(
153
+ f"Unexpected filesystem changes ({len(unexpected)} file(s)):\n"
154
+ + "\n".join(f" - {p}" for p in sorted(unexpected))
155
+ )
156
+
157
+ # Check for missing expected changes
158
+ missing = expected_paths - set(files_by_path.keys())
159
+ if missing:
160
+ errors.append(
161
+ f"Missing expected filesystem changes ({len(missing)} file(s)):\n"
162
+ + "\n".join(f" - {p}" for p in sorted(missing))
163
+ )
164
+
165
+ if errors:
166
+ raise AssertionError(
167
+ f"Filesystem expect_exactly failed with {len(errors)} error(s):\n"
168
+ + "\n".join(f" {i+1}. {e}" for i, e in enumerate(errors))
169
+ )
170
+
171
+ return self
172
+
173
+ async def _validate_entry(
174
+ self, entry: FsFileDiffEntry, spec: Dict[str, Any], errors: List[str]
175
+ ) -> None:
176
+ path = spec["path"]
177
+
178
+ # Plain content checks (for text files)
179
+ if "content" in spec and spec["content"] is not ...:
180
+ if entry.content is None:
181
+ errors.append(f"'{path}': content not available (was content excluded from diff?)")
182
+ elif entry.content != spec["content"]:
183
+ errors.append(
184
+ f"'{path}': content mismatch\n"
185
+ f" expected: {repr(spec['content'][:200])}\n"
186
+ f" actual: {repr(entry.content[:200])}"
187
+ )
188
+
189
+ if "content_contains" in spec and spec["content_contains"] is not ...:
190
+ if entry.content is None:
191
+ errors.append(f"'{path}': content not available for content_contains check")
192
+ elif spec["content_contains"] not in entry.content:
193
+ errors.append(
194
+ f"'{path}': content does not contain expected substring: "
195
+ f"{repr(spec['content_contains'][:200])}"
196
+ )
197
+
198
+ # Document text checks (for docx, pptx, xlsx, etc. via /fs/doc/text)
199
+ if "doc_text" in spec or "doc_text_contains" in spec:
200
+ try:
201
+ doc_text = await self._resource.doc_text(path)
202
+ except Exception as e:
203
+ errors.append(f"'{path}': failed to extract doc text: {e}")
204
+ doc_text = None
205
+
206
+ if doc_text is not None:
207
+ if "doc_text" in spec and spec["doc_text"] is not ...:
208
+ if doc_text != spec["doc_text"]:
209
+ errors.append(
210
+ f"'{path}': doc_text mismatch\n"
211
+ f" expected: {repr(spec['doc_text'][:200])}\n"
212
+ f" actual: {repr(doc_text[:200])}"
213
+ )
214
+
215
+ if "doc_text_contains" in spec and spec["doc_text_contains"] is not ...:
216
+ if spec["doc_text_contains"] not in doc_text:
217
+ errors.append(
218
+ f"'{path}': doc text does not contain expected substring: "
219
+ f"{repr(spec['doc_text_contains'][:200])}"
220
+ )
221
+
222
+ if "file_type" in spec and spec["file_type"] is not ...:
223
+ if entry.file_type != spec["file_type"]:
224
+ errors.append(
225
+ f"'{path}': file_type mismatch (expected {spec['file_type']!r}, got {entry.file_type!r})"
226
+ )
227
+
228
+ if "size" in spec and spec["size"] is not ...:
229
+ if entry.size != spec["size"]:
230
+ errors.append(
231
+ f"'{path}': size mismatch (expected {spec['size']}, got {entry.size})"
232
+ )
233
+
234
+
235
+ class AsyncFilesystemResource(Resource):
236
+ """Filesystem resource that operates via the /diff/fs and /fs/* endpoints."""
237
+
238
+ def __init__(self, resource: ResourceModel, client: "AsyncWrapper"):
239
+ super().__init__(resource)
240
+ self.client = client
241
+
242
+ # ── Diff endpoints ────────────────────────────────────────────────
243
+
244
+ async def diff(
245
+ self,
246
+ include_content: bool = True,
247
+ max_content_size: int = 102400,
248
+ exclude_patterns: Optional[List[str]] = None,
249
+ ) -> AsyncFilesystemDiff:
250
+ """Get filesystem diff from the environment.
251
+
252
+ Args:
253
+ include_content: Whether to include file contents (default True)
254
+ max_content_size: Max file size to include content for (default 100KB)
255
+ exclude_patterns: Optional list of glob patterns to exclude
256
+
257
+ Returns:
258
+ AsyncFilesystemDiff with assertion helpers
259
+ """
260
+ request = FsDiffRequest(
261
+ include_content=include_content,
262
+ max_content_size=max_content_size,
263
+ exclude_patterns=exclude_patterns,
264
+ )
265
+ response = await self.client.request(
266
+ "POST",
267
+ "/diff/fs",
268
+ json=request.model_dump(exclude_none=True),
269
+ )
270
+ result = response.json()
271
+ fs_response = FsDiffResponse(**result)
272
+ if not fs_response.success:
273
+ raise RuntimeError(
274
+ f"Filesystem diff failed: {fs_response.error or fs_response.message}"
275
+ )
276
+ return AsyncFilesystemDiff(fs_response, self)
277
+
278
+ async def diff_simple(
279
+ self,
280
+ include_content: bool = True,
281
+ max_content_size: int = 102400,
282
+ ) -> AsyncFilesystemDiff:
283
+ """Get filesystem diff using the simple GET endpoint.
284
+
285
+ Args:
286
+ include_content: Whether to include file contents (default True)
287
+ max_content_size: Max file size to include content for (default 100KB)
288
+
289
+ Returns:
290
+ AsyncFilesystemDiff with assertion helpers
291
+ """
292
+ params = {
293
+ "include_content": include_content,
294
+ "max_content_size": max_content_size,
295
+ }
296
+ response = await self.client.request("GET", "/diff/fs", params=params)
297
+ result = response.json()
298
+ fs_response = FsDiffResponse(**result)
299
+ if not fs_response.success:
300
+ raise RuntimeError(
301
+ f"Filesystem diff failed: {fs_response.error or fs_response.message}"
302
+ )
303
+ return AsyncFilesystemDiff(fs_response, self)
304
+
305
+ # ── Single file endpoints ─────────────────────────────────────────
306
+
307
+ async def file(
308
+ self,
309
+ path: str,
310
+ include_content: bool = True,
311
+ max_content_size: int = 102400,
312
+ ) -> FileStateResponse:
313
+ """Get current state of a single file.
314
+
315
+ Args:
316
+ path: Absolute path to the file
317
+ include_content: Whether to include file content (default True)
318
+ max_content_size: Max file size to include content for (default 100KB)
319
+
320
+ Returns:
321
+ FileStateResponse with file metadata and optional content
322
+ """
323
+ request = FileStateRequest(
324
+ path=path,
325
+ include_content=include_content,
326
+ max_content_size=max_content_size,
327
+ )
328
+ response = await self.client.request(
329
+ "POST", "/fs/file", json=request.model_dump()
330
+ )
331
+ return FileStateResponse(**response.json())
332
+
333
+ async def file_text(self, path: str, max_content_size: int = 102400) -> str:
334
+ """Get file content as plain text.
335
+
336
+ Args:
337
+ path: Absolute path to the file
338
+ max_content_size: Max file size (default 100KB)
339
+
340
+ Returns:
341
+ File content as string
342
+ """
343
+ request = FileStateTextRequest(
344
+ path=path, max_content_size=max_content_size
345
+ )
346
+ response = await self.client.request(
347
+ "POST", "/fs/file/text", json=request.model_dump()
348
+ )
349
+ return response.text
350
+
351
+ # ── Document extraction endpoints ─────────────────────────────────
352
+
353
+ async def doc_text(self, path: str, max_size: int = 10485760) -> str:
354
+ """Extract plain text from a document file (docx, pptx, xlsx, pdf, etc.).
355
+
356
+ Args:
357
+ path: Absolute path to the document
358
+ max_size: Max document size (default 10MB)
359
+
360
+ Returns:
361
+ Extracted text content as string
362
+ """
363
+ request = DocTextRequest(path=path, max_size=max_size)
364
+ response = await self.client.request(
365
+ "POST", "/fs/doc/text", json=request.model_dump()
366
+ )
367
+ return response.text
368
+
369
+ async def doc_metadata(self, path: str) -> DocMetadataResponse:
370
+ """Extract metadata from a document file.
371
+
372
+ Args:
373
+ path: Absolute path to the document
374
+
375
+ Returns:
376
+ DocMetadataResponse with file_type and metadata dict
377
+ """
378
+ request = DocMetadataRequest(path=path)
379
+ response = await self.client.request(
380
+ "POST", "/fs/doc/metadata", json=request.model_dump()
381
+ )
382
+ return DocMetadataResponse(**response.json())
383
+
384
+ async def doc_structured(self, path: str) -> DocStructuredResponse:
385
+ """Extract structured content from a document file.
386
+
387
+ Args:
388
+ path: Absolute path to the document
389
+
390
+ Returns:
391
+ DocStructuredResponse with file_type and structured data dict
392
+ """
393
+ request = DocStructuredRequest(path=path)
394
+ response = await self.client.request(
395
+ "POST", "/fs/doc/structured", json=request.model_dump()
396
+ )
397
+ return DocStructuredResponse(**response.json())
@@ -346,9 +346,8 @@ def verifier_from_string(
346
346
  return False
347
347
  return target in numbers
348
348
 
349
- # Create a globals namespace with all required imports
350
- exec_globals = globals().copy()
351
- exec_globals.update({
349
+ # Create a local namespace for executing the code
350
+ local_namespace = {
352
351
  "TASK_SUCCESSFUL_SCORE": TASK_SUCCESSFUL_SCORE,
353
352
  "TASK_FAILED_SCORE": TASK_FAILED_SCORE,
354
353
  "IgnoreConfig": IgnoreConfig,
@@ -359,17 +358,10 @@ def verifier_from_string(
359
358
  "json": json,
360
359
  "re": re,
361
360
  "string": string,
362
- })
363
-
364
- # Create a local namespace for executing the code
365
- local_namespace = {}
361
+ }
366
362
 
367
363
  # Execute the cleaned verifier code in the namespace
368
- exec(cleaned_code, exec_globals, local_namespace)
369
-
370
- # Merge local_namespace into exec_globals so helper functions are accessible
371
- # from the main verifier function when it's called
372
- exec_globals.update(local_namespace)
364
+ exec(cleaned_code, globals(), local_namespace)
373
365
 
374
366
  # Find the function that was defined (not imported)
375
367
  # Functions defined via exec have co_filename == '<string>'
@@ -456,12 +448,7 @@ async def load_tasks(
456
448
 
457
449
 
458
450
  async def update_task(
459
- task_key: str,
460
- prompt: Optional[str] = None,
461
- verifier_code: Optional[str] = None,
462
- metadata: Optional[Dict[str, Any]] = None,
463
- writer_metadata: Optional[Dict[str, Any]] = None,
464
- qa_metadata: Optional[Dict[str, Any]] = None,
451
+ task_key: str, prompt: Optional[str] = None, verifier_code: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None
465
452
  ):
466
453
  """Convenience function to update an existing task.
467
454
 
@@ -470,8 +457,6 @@ async def update_task(
470
457
  prompt: New prompt text for the task (optional)
471
458
  verifier_code: Python code for task verification (optional)
472
459
  metadata: Additional metadata for the task (optional)
473
- writer_metadata: Metadata filled by task writer (optional)
474
- qa_metadata: Metadata filled by QA reviewer (optional)
475
460
 
476
461
  Returns:
477
462
  TaskResponse containing the updated task details
@@ -480,18 +465,12 @@ async def update_task(
480
465
  response = await fleet.update_task("my-task", prompt="New prompt text")
481
466
  response = await fleet.update_task("my-task", verifier_code="def verify(env): return True")
482
467
  response = await fleet.update_task("my-task", metadata={"seed": 42, "story": "Updated story"})
483
- response = await fleet.update_task("my-task", writer_metadata={"author": "john"})
484
468
  """
485
469
  from .global_client import get_client
486
470
 
487
471
  client = get_client()
488
472
  return await client.update_task(
489
- task_key=task_key,
490
- prompt=prompt,
491
- verifier_code=verifier_code,
492
- metadata=metadata,
493
- writer_metadata=writer_metadata,
494
- qa_metadata=qa_metadata,
473
+ task_key=task_key, prompt=prompt, verifier_code=verifier_code, metadata=metadata
495
474
  )
496
475
 
497
476